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 (
+
+ );
+}
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;
+}