diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx index 71bbb2f..a231a95 100644 --- a/src/app/(auth)/layout.tsx +++ b/src/app/(auth)/layout.tsx @@ -9,7 +9,7 @@ export const metadata = makePageMetadata({ export default function AuthLayout({ children }: AuthCommonProps) { return (
-
{children}
+
{children}
); } diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 9c77db3..092a607 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -1,4 +1,6 @@ import { AuthHeader } from "@/components/auth/AuthHeader"; +import { AuthMain } from "@/components/auth/AuthMain"; +import { LoginForm } from "@/components/auth/login/LoginForm"; import { LoginPageTitle } from "@/components/auth/login/LoginPageTitle"; import { LoginSubtitle } from "@/components/auth/login/LoginSubtitle"; import { makePageMetadata } from "@/seo/metadata"; @@ -20,14 +22,9 @@ export default function LoginPage() { -
- {/* TODO: 나중에 LoginMain + AuthCard + LoginForm 구조로 확장 */} -
-

- 여기에는 로그인 폼이 들어갈 예정입니다. -

-
-
+ + + ); } diff --git a/src/components/auth/AuthHeader.tsx b/src/components/auth/AuthHeader.tsx index ce79df0..c7ca35a 100644 --- a/src/components/auth/AuthHeader.tsx +++ b/src/components/auth/AuthHeader.tsx @@ -3,9 +3,9 @@ import type { AuthCommonProps } from "@/types/auth"; export function AuthHeader({ children }: AuthCommonProps) { return ( -
+
-
{children}
+
{children}
); } diff --git a/src/components/auth/AuthMain.tsx b/src/components/auth/AuthMain.tsx new file mode 100644 index 0000000..24892fe --- /dev/null +++ b/src/components/auth/AuthMain.tsx @@ -0,0 +1,9 @@ +import type { AuthCommonProps } from "@/types/auth"; + +export function AuthMain({ children }: AuthCommonProps) { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/auth/login/LoginForm.tsx b/src/components/auth/login/LoginForm.tsx new file mode 100644 index 0000000..286f994 --- /dev/null +++ b/src/components/auth/login/LoginForm.tsx @@ -0,0 +1,172 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/shared/button"; +import { Icon } from "@/shared/Icon"; +import { Input } from "@/shared/input"; +import { useAuthFormStore } from "@/stores/authForm.store"; +import Link from "next/link"; +import type { ChangeEvent, FormEvent } from "react"; + +export function LoginForm() { + const { + email, + password, + emailError, + passwordError, + isPasswordVisible, + setEmail, + setPassword, + clearEmailError, + clearPasswordError, + togglePasswordVisible, + validateLogin, + } = useAuthFormStore(); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); // 기본 form submit 동작 막기 + + const isValid = validateLogin(); + if (!isValid) { + // 에러 상태/값 초기화는 스토어에서 이미 처리 + return; + } + + // TODO: 실제 로그인 요청 로직 + // ex) await login({ email, password }); + }; + + const handleEmailChange = (event: ChangeEvent) => { + setEmail(event.target.value); + }; + + const handlePasswordChange = (event: ChangeEvent) => { + setPassword(event.target.value); + }; + + return ( +
+ {/* 이메일 필드 */} +
+
+ + + {emailError && ( + 이메일 정보를 확인해 주세요. + )} +
+ +
+ + {/* 비밀번호 필드 + 표시 토글 */} +
+
+ + + {passwordError && ( + 비밀번호를 확인해 주세요. + )} +
+
+ + {/* 비밀번호 표시/숨김 토글 아이콘 */} + +
+
+ + {/* 로그인 버튼 */} +
+ +
+ + {/* 헬퍼 링크: 비밀번호 찾기 / 회원가입 */} +
+

+ 비밀번호를 잊으셨나요?{" "} + + 비밀번호 찾기 + +

+

+ 회원이 아니신가요?{" "} + + 회원가입하기 + +

+
+ + {/* 간편 로그인 + 소셜 버튼 */} +
+
+
+ 간편 로그인 +
+
+ +
+ + + +
+
+ + ); +} diff --git a/src/lib/variants/button.signup.ts b/src/lib/variants/button.signup.ts index 59fb7e4..2e78665 100644 --- a/src/lib/variants/button.signup.ts +++ b/src/lib/variants/button.signup.ts @@ -4,7 +4,7 @@ export const signupButtonVariants = cva( [ // Layout (공통) "inline-flex items-center justify-center", - "w-[36.6rem] h-[6.2rem] px-[3rem] rounded-lg cursor-pointer", + "max-w-[36.6rem] h-[4.6rem] px-[3rem] rounded-lg cursor-pointer", // Typography (공통) "t-16-m", diff --git a/src/lib/variants/input.auth.ts b/src/lib/variants/input.auth.ts index 11dd7c0..b67529e 100644 --- a/src/lib/variants/input.auth.ts +++ b/src/lib/variants/input.auth.ts @@ -1,17 +1,9 @@ -// src/lib/variants/input.auth.ts import { cva, type VariantProps } from "class-variance-authority"; -/** - * 로그인 인풋 컴포넌트의 스타일 variant - * - 기본: 1px + gray-300 - * - 포커스: 1.5px + gray-900 - * - 에러: 1.5px + danger-600 - * - 에러 상태에서 포커스 시: 포커스 규칙(gray-900)로 복귀 - */ export const authInputVariants = cva( [ // === 기본 레이아웃 === - "w-[36.6rem] h-[4.6rem] px-6 rounded-lg bg-white t-14-m outline-none", + "max-w-[36.6rem] h-[4.6rem] px-6 rounded-lg bg-white t-14-m outline-none", // === 플레이스홀더 === "placeholder:text-[var(--color-gray-500)]", diff --git a/src/stores/authForm.store.ts b/src/stores/authForm.store.ts new file mode 100644 index 0000000..b0783c6 --- /dev/null +++ b/src/stores/authForm.store.ts @@ -0,0 +1,55 @@ +import type { AuthFormState } from "@/types/authForm"; +import { create } from "zustand"; + +export const useAuthFormStore = create((set, get) => ({ + email: "", + password: "", + emailError: false, + passwordError: false, + isPasswordVisible: false, + + setEmail: (value) => set({ email: value }), + setPassword: (value) => set({ password: value }), + + clearEmailError: () => set({ emailError: false }), + clearPasswordError: () => set({ passwordError: false }), + + togglePasswordVisible: () => set((state) => ({ isPasswordVisible: !state.isPasswordVisible })), + + validateLogin: () => { + const { email, password } = get(); + + let emailError = false; + let passwordError = false; + + // 이메일: 비어 있거나 형식이 올바르지 않으면 에러 + const trimmedEmail = email.trim(); + const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmedEmail); + + if (!trimmedEmail || !isValidEmail) { + emailError = true; + } + + // 비밀번호: 6자리 이상 + 특수문자 1개 이상 + const hasMinLength = password.length >= 6; + const hasSpecialChar = /[^A-Za-z0-9]/.test(password); + + if (!hasMinLength || !hasSpecialChar) { + passwordError = true; + } + + // 하나라도 에러가 있으면 해당 필드 값 초기화 + 에러 플래그 세팅 + if (emailError || passwordError) { + set({ + email: emailError ? "" : email, + password: passwordError ? "" : password, + emailError, + passwordError, + }); + return false; + } + + set({ emailError: false, passwordError: false }); + return true; + }, +})); diff --git a/src/types/authForm.ts b/src/types/authForm.ts new file mode 100644 index 0000000..6593a65 --- /dev/null +++ b/src/types/authForm.ts @@ -0,0 +1,17 @@ +export interface AuthFormState { + email: string; + password: string; + emailError: boolean; + passwordError: boolean; + isPasswordVisible: boolean; + + setEmail: (value: string) => void; + setPassword: (value: string) => void; + + clearEmailError: () => void; + clearPasswordError: () => void; + + togglePasswordVisible: () => void; + + validateLogin: () => boolean; +}