diff --git a/package.json b/package.json
index 3df975e..0ebdbda 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"axios": "^1.12.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "framer-motion": "^12.23.24",
"lucide-react": "^0.544.0",
"next": "15.5.3",
"next-seo": "^6.8.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 148530e..d7fced6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -28,6 +28,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ framer-motion:
+ specifier: ^12.23.24
+ version: 12.23.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
lucide-react:
specifier: ^0.544.0
version: 0.544.0(react@19.1.0)
@@ -2247,6 +2250,23 @@ packages:
}
engines: { node: ">= 6" }
+ framer-motion@12.23.24:
+ resolution:
+ {
+ integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==,
+ }
+ peerDependencies:
+ "@emotion/is-prop-valid": "*"
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@emotion/is-prop-valid":
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+
function-bind@1.1.2:
resolution:
{
@@ -3114,6 +3134,18 @@ packages:
engines: { node: ">=10" }
hasBin: true
+ motion-dom@12.23.23:
+ resolution:
+ {
+ integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==,
+ }
+
+ motion-utils@12.23.6:
+ resolution:
+ {
+ integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==,
+ }
+
ms@2.1.3:
resolution:
{
@@ -5657,6 +5689,15 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
+ framer-motion@12.23.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+ dependencies:
+ motion-dom: 12.23.23
+ motion-utils: 12.23.6
+ tslib: 2.8.1
+ optionalDependencies:
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+
function-bind@1.1.2: {}
function.prototype.name@1.1.8:
@@ -6114,6 +6155,12 @@ snapshots:
mkdirp@3.0.1: {}
+ motion-dom@12.23.23:
+ dependencies:
+ motion-utils: 12.23.6
+
+ motion-utils@12.23.6: {}
+
ms@2.1.3: {}
nano-spawn@1.0.3: {}
diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx
index 4e12985..3cd0c2d 100644
--- a/src/app/(auth)/forgot-password/page.tsx
+++ b/src/app/(auth)/forgot-password/page.tsx
@@ -1,14 +1,24 @@
+import { AuthMain } from "@/components/auth/AuthMain";
+import { ForgotPasswordForm } from "@/components/auth/forgotPassword/ForgotPasswordForm";
+import { ForgotPasswordHeader } from "@/components/auth/forgotPassword/ForgotPasswordHeader";
import { makePageMetadata } from "@/seo/metadata";
export const metadata = {
...makePageMetadata({
title: "비밀번호 찾기",
- description: "PlanMate 비밀번호 찾기 1단계: 이메일 입력 페이지",
+ description: "PlanMate 비밀번호 찾기 페이지",
canonical: "/forgot-password",
}),
robots: { index: false, follow: false },
};
export default function ForgotPasswordPage() {
- return
비밀번호 찾기 - 이메일 입력 페이지
;
+ return (
+ <>
+
+
+
+
+ >
+ );
}
diff --git a/src/app/(auth)/forgot-password/reset/page.tsx b/src/app/(auth)/forgot-password/reset/page.tsx
deleted file mode 100644
index 36e36dd..0000000
--- a/src/app/(auth)/forgot-password/reset/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { makePageMetadata } from "@/seo/metadata";
-
-export const metadata = {
- ...makePageMetadata({
- title: "비밀번호 찾기 — 비밀번호 재설정",
- description: "PlanMate 비밀번호 찾기 3단계: 새 비밀번호 설정 페이지",
- canonical: "/forgot-password/reset",
- }),
- robots: { index: false, follow: false },
-};
-
-export default function ForgotPasswordResetPage() {
- return 비밀번호 찾기 - 비밀번호 재설정 페이지
;
-}
diff --git a/src/app/(auth)/forgot-password/verify/page.tsx b/src/app/(auth)/forgot-password/verify/page.tsx
deleted file mode 100644
index 2784c3a..0000000
--- a/src/app/(auth)/forgot-password/verify/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { makePageMetadata } from "@/seo/metadata";
-
-export const metadata = {
- ...makePageMetadata({
- title: "비밀번호 찾기 — 인증번호 입력",
- description: "PlanMate 비밀번호 찾기 2단계: 이메일 인증번호 입력 페이지",
- canonical: "/forgot-password/verify",
- }),
- robots: { index: false, follow: false },
-};
-
-export default function ForgotPasswordVerifyPage() {
- return 비밀번호 찾기 - 인증번호 입력 페이지
;
-}
diff --git a/src/app/(auth)/signup/basic/page.tsx b/src/app/(auth)/signup/basic/page.tsx
new file mode 100644
index 0000000..216e494
--- /dev/null
+++ b/src/app/(auth)/signup/basic/page.tsx
@@ -0,0 +1,24 @@
+import { AuthMain } from "@/components/auth/AuthMain";
+import { SignupForm } from "@/components/auth/signup/SignupForm";
+import { SignupHeader } from "@/components/auth/signup/SignupHeader";
+import { makePageMetadata } from "@/seo/metadata";
+
+export const metadata = {
+ ...makePageMetadata({
+ title: "회원가입",
+ description: "MyPlanMate 회원가입페이지",
+ canonical: "/signup/basic",
+ }),
+ robots: { index: false, follow: false },
+};
+
+export default function SignupEmailPage() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/(auth)/signup/email/page.tsx b/src/app/(auth)/signup/email/page.tsx
deleted file mode 100644
index 3c13f44..0000000
--- a/src/app/(auth)/signup/email/page.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { AuthHeader } from "@/components/auth/AuthHeader";
-import { AuthMain } from "@/components/auth/AuthMain";
-import { SubTitle, Title } from "@/components/auth/AuthTitle";
-import { SignupForm } from "@/components/auth/signup/SignupForm";
-import { makePageMetadata } from "@/seo/metadata";
-
-export const metadata = {
- ...makePageMetadata({
- title: "회원가입 — 이메일 입력",
- description: "MyPlanMate 회원가입 2단계: 이메일 입력 페이지",
- canonical: "/signup/email",
- }),
- robots: { index: false, follow: false },
-};
-
-export default function SignupEmailPage() {
- return (
- <>
-
- 이메일
- 계정으로 사용할 이메일을 알려주세요.
-
-
-
-
-
- >
- );
-}
diff --git a/src/app/(auth)/signup/name/page.tsx b/src/app/(auth)/signup/name/page.tsx
deleted file mode 100644
index b74a13f..0000000
--- a/src/app/(auth)/signup/name/page.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { AuthHeader } from "@/components/auth/AuthHeader";
-import { AuthMain } from "@/components/auth/AuthMain";
-import { SubTitle, Title } from "@/components/auth/AuthTitle";
-import { SignupForm } from "@/components/auth/signup/SignupForm";
-import { makePageMetadata } from "@/seo/metadata";
-
-export const metadata = {
- ...makePageMetadata({
- title: "회원가입 — 이름 입력",
- description: "MyPlanMate 회원가입 1단계: 이름 입력 페이지",
- canonical: "/signup/name",
- }),
- robots: { index: false, follow: false },
-};
-
-export default function SignupNamePage() {
- return (
- <>
-
- 이름
- 이름을 알려주세요.
-
-
-
-
-
- >
- );
-}
diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx
index 229f6bf..4ed937b 100644
--- a/src/app/(auth)/signup/page.tsx
+++ b/src/app/(auth)/signup/page.tsx
@@ -7,7 +7,7 @@ import { makePageMetadata } from "@/seo/metadata";
export const metadata = {
...makePageMetadata({
title: "회원가입",
- description: "PlanMate 회원가입 페이지",
+ description: "MyPlanMate 회원가입 페이지",
canonical: "/signup",
}),
robots: { index: false, follow: false }, // 인증 관련 페이지는 검색 제외
diff --git a/src/app/(auth)/signup/password/page.tsx b/src/app/(auth)/signup/password/page.tsx
deleted file mode 100644
index 683491e..0000000
--- a/src/app/(auth)/signup/password/page.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { AuthHeader } from "@/components/auth/AuthHeader";
-import { AuthMain } from "@/components/auth/AuthMain";
-import { SubTitle, Title } from "@/components/auth/AuthTitle";
-import { SignupForm } from "@/components/auth/signup/SignupForm";
-import { makePageMetadata } from "@/seo/metadata";
-
-export const metadata = {
- ...makePageMetadata({
- title: "회원가입 — 비밀번호 설정",
- description: "PlanMate 회원가입 3단계: 비밀번호 설정 페이지",
- canonical: "/signup/password",
- }),
- robots: { index: false, follow: false },
-};
-
-export default function SignupPasswordPage() {
- return (
- <>
-
- 비밀번호
- 8자 이상 / 특수문자 포함
-
-
-
-
-
- >
- );
-}
diff --git a/src/app/(auth)/signup/terms/page.tsx b/src/app/(auth)/signup/terms/page.tsx
deleted file mode 100644
index 0034491..0000000
--- a/src/app/(auth)/signup/terms/page.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { AuthHeader } from "@/components/auth/AuthHeader";
-import { AuthMain } from "@/components/auth/AuthMain";
-import { SubTitle, Title } from "@/components/auth/AuthTitle";
-import { SignupForm } from "@/components/auth/signup/SignupForm";
-import { makePageMetadata } from "@/seo/metadata";
-
-export const metadata = {
- ...makePageMetadata({
- title: "회원가입 — 약관 동의",
- description: "PlanMate 회원가입 4단계: 약관 동의 페이지",
- canonical: "/signup/terms",
- }),
- robots: { index: false, follow: false },
-};
-
-export default function SignupTermsPage() {
- return (
- <>
-
- 약관동의
- 서비스 이용을 위한 필수 약관입니다.
-
-
-
-
-
- >
- );
-}
diff --git a/src/components/auth/AuthMain.tsx b/src/components/auth/AuthMain.tsx
index 24892fe..b50c5ea 100644
--- a/src/components/auth/AuthMain.tsx
+++ b/src/components/auth/AuthMain.tsx
@@ -1,6 +1,23 @@
-import type { AuthCommonProps } from "@/types/auth";
+"use client";
+
+import { useForgotPasswordStepStore } from "@/stores/forgotPasswordStepStore";
+import { useSignupStepStore } from "@/stores/signupStepStore";
+import type { AuthMainProps } from "@/types/auth";
+import { useEffect } from "react";
+
+export function AuthMain({ children, flow }: AuthMainProps) {
+ const resetSignupStep = useSignupStepStore((s) => s.reset);
+ const resetForgotStep = useForgotPasswordStepStore((s) => s.reset);
+
+ useEffect(() => {
+ if (flow === "signup") {
+ resetSignupStep();
+ }
+ if (flow === "forgot") {
+ resetForgotStep();
+ }
+ }, [flow, resetSignupStep, resetForgotStep]);
-export function AuthMain({ children }: AuthCommonProps) {
return (
{children}
diff --git a/src/components/auth/AuthStepTransition.tsx b/src/components/auth/AuthStepTransition.tsx
new file mode 100644
index 0000000..73a83ef
--- /dev/null
+++ b/src/components/auth/AuthStepTransition.tsx
@@ -0,0 +1,22 @@
+import { authStepSlide, authTransition } from "@/lib/variants/motion.auth";
+import type { AuthStepTransitionProps } from "@/types/auth";
+import { AnimatePresence, motion } from "framer-motion";
+
+export function AuthStepTransition({ stepKey, direction, children }: AuthStepTransitionProps) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/components/auth/signup/SignupStepIndicator.tsx b/src/components/auth/StepIndicator.tsx
similarity index 88%
rename from src/components/auth/signup/SignupStepIndicator.tsx
rename to src/components/auth/StepIndicator.tsx
index b232a11..245160d 100644
--- a/src/components/auth/signup/SignupStepIndicator.tsx
+++ b/src/components/auth/StepIndicator.tsx
@@ -1,13 +1,9 @@
"use client";
import { cn } from "@/lib/utils";
-import type { SignupStepIndicatorProps } from "@/types/auth";
+import type { StepIndicatorProps } from "@/types/auth";
-export function SignupStepIndicator({
- currentStep,
- totalSteps = 4,
- className,
-}: SignupStepIndicatorProps) {
+export function StepIndicator({ currentStep, totalSteps = 4, className }: StepIndicatorProps) {
const steps = Array.from({ length: totalSteps }, (_, index) => index + 1);
return (
diff --git a/src/components/auth/forgotPassword/ForgotPasswordEmailStep.tsx b/src/components/auth/forgotPassword/ForgotPasswordEmailStep.tsx
new file mode 100644
index 0000000..0db64b8
--- /dev/null
+++ b/src/components/auth/forgotPassword/ForgotPasswordEmailStep.tsx
@@ -0,0 +1,85 @@
+"use client";
+
+import { useState } from "react";
+
+import { useAuthFormSubmit } from "@/hooks/useAuthFormSubmit";
+import { cn } from "@/lib/utils";
+import { isValidEmail } from "@/lib/validators";
+import { useForgotPasswordFormStore } from "@/stores/forgotPasswordFormStore";
+import { useForgotPasswordStepStore } from "@/stores/forgotPasswordStepStore";
+
+import { Button } from "@/shared/button";
+import { Input } from "@/shared/input";
+import type { StepFieldMeta } from "@/types/auth";
+
+export function ForgotPasswordEmailStep({ fieldId, fieldName }: StepFieldMeta) {
+ // ✅ 1) 전역 스토어에서 이메일 값 + setter 가져오기
+ const { email, setEmail } = useForgotPasswordFormStore();
+
+ // ✅ 2) 다음 스텝으로 이동하는 액션
+ const { goNext } = useForgotPasswordStepStore();
+
+ // ✅ 3) 이메일 에러 메시지 (UI 전용 로컬 상태)
+ const [emailError, setEmailError] = useState("");
+
+ // ✅ 4) 공통 submit 훅으로 form 기본 동작 막기 + 콜백 실행
+ const handleSubmit = useAuthFormSubmit(() => {
+ let hasError = false;
+
+ // 이메일 형식 검증
+ if (!isValidEmail(email)) {
+ setEmailError("올바른 이메일을 입력해주세요.");
+ hasError = true;
+ }
+
+ // 하나라도 실패하면 이 스텝에 머무르기
+ if (hasError) return;
+
+ // 디버깅용 로그 (나중에 실제 API 요청으로 대체)
+ console.log("📨 Forgot Password Email Step:", { email });
+
+ // 검증 통과 시 다음 스텝으로 이동
+ goNext();
+ });
+
+ return (
+
+ );
+}
diff --git a/src/components/auth/forgotPassword/ForgotPasswordForm.tsx b/src/components/auth/forgotPassword/ForgotPasswordForm.tsx
new file mode 100644
index 0000000..e8d45bd
--- /dev/null
+++ b/src/components/auth/forgotPassword/ForgotPasswordForm.tsx
@@ -0,0 +1,63 @@
+"use client";
+
+import { FORGOT_PASSWORD_STEP_FIELD_META, FORGOT_PASSWORD_STEP_ORDER } from "@/lib/constants";
+import { cn } from "@/lib/utils";
+import { useForgotPasswordStepStore } from "@/stores/forgotPasswordStepStore";
+
+import Link from "next/link";
+
+import { AuthStepTransition } from "../AuthStepTransition";
+import { StepIndicator } from "../StepIndicator";
+import { ForgotPasswordEmailStep } from "./ForgotPasswordEmailStep";
+import { ForgotPasswordResetStep } from "./ForgotPasswordResetStep";
+import { ForgotPasswordVerifyStep } from "./ForgotPasswordVerifyStep";
+
+export function ForgotPasswordForm() {
+ // ✅ 1) 현재 스텝 이름 + 전환 방향 가져오기
+ const { step, direction } = useForgotPasswordStepStore();
+
+ const isEmailStep = step === "email";
+ const isVerifyStep = step === "verify";
+ const isResetStep = step === "reset";
+
+ // ✅ 2) 현재 스텝이 몇 번째인지 계산 (1부터 시작)
+ const currentStepIndex = FORGOT_PASSWORD_STEP_ORDER.indexOf(step);
+ const currentStepNumber = currentStepIndex + 1;
+
+ // ✅ 3) 스텝별 필드 메타 (id / name)
+ const { fieldId, fieldName } = FORGOT_PASSWORD_STEP_FIELD_META[step];
+
+ return (
+ // 스텝이 바뀌면 슬라이드 전환
+
+
+ {/* 상단 단계 인디케이터 (3단계) */}
+
+
+ {/* 스텝별 폼 */}
+
+ {isEmailStep && }
+ {isVerifyStep && }
+ {isResetStep && }
+
+
+ {/* 하단 로그인 링크 */}
+
+ 비밀번호를 이미 재설정하셨나요?{" "}
+
+ 로그인하기
+
+
+
+
+ );
+}
diff --git a/src/components/auth/forgotPassword/ForgotPasswordHeader.tsx b/src/components/auth/forgotPassword/ForgotPasswordHeader.tsx
new file mode 100644
index 0000000..8d56fd9
--- /dev/null
+++ b/src/components/auth/forgotPassword/ForgotPasswordHeader.tsx
@@ -0,0 +1,18 @@
+"use client";
+
+import { AuthHeader } from "@/components/auth/AuthHeader";
+import { SubTitle, Title } from "@/components/auth/AuthTitle";
+import { FORGOT_PASSWORD_STEP_COPY } from "@/lib/constants";
+import { useForgotPasswordStepStore } from "@/stores/forgotPasswordStepStore";
+
+export function ForgotPasswordHeader() {
+ const step = useForgotPasswordStepStore((s) => s.step);
+ const { title, subtitle } = FORGOT_PASSWORD_STEP_COPY[step];
+
+ return (
+
+ {title}
+ {subtitle}
+
+ );
+}
diff --git a/src/components/auth/forgotPassword/ForgotPasswordResetStep.tsx b/src/components/auth/forgotPassword/ForgotPasswordResetStep.tsx
new file mode 100644
index 0000000..c1c39ef
--- /dev/null
+++ b/src/components/auth/forgotPassword/ForgotPasswordResetStep.tsx
@@ -0,0 +1,158 @@
+"use client";
+
+import { useState } from "react";
+
+import { useAuthFormSubmit } from "@/hooks/useAuthFormSubmit";
+import { usePasswordVisibility } from "@/hooks/usePasswordVisibility";
+import { cn } from "@/lib/utils";
+import { isValidPassword } from "@/lib/validators";
+import { useForgotPasswordFormStore } from "@/stores/forgotPasswordFormStore";
+import { useForgotPasswordStepStore } from "@/stores/forgotPasswordStepStore";
+
+import { Button } from "@/shared/button";
+import { Icon } from "@/shared/Icon";
+import { Input } from "@/shared/input";
+import type { StepFieldMeta } from "@/types/auth";
+
+export function ForgotPasswordResetStep({ fieldId, fieldName }: StepFieldMeta) {
+ // ✅ 1) 전역 스토어에서 새 비밀번호 값 + setter 가져오기
+ const { email, code, newPassword, setNewPassword, reset } = useForgotPasswordFormStore();
+
+ // ✅ 2) 스텝 이동 액션
+ const { goPrev } = useForgotPasswordStepStore();
+
+ // ✅ 3) 로컬 상태: 에러 + 비밀번호 확인 값
+ const [passwordError, setPasswordError] = useState("");
+ const [passwordConfirm, setPasswordConfirm] = useState("");
+ const [passwordConfirmError, setPasswordConfirmError] = useState("");
+
+ // ✅ 4) 비밀번호 보기/숨기기 토글 훅
+ const { inputType, iconName, ariaLabel, toggleVisibility } = usePasswordVisibility(false);
+
+ // ✅ 5) submit 핸들러 (공통 훅 사용)
+ const handleSubmit = useAuthFormSubmit(() => {
+ let hasError = false;
+
+ // 1) 비밀번호 규칙 검증 (길이 + 특수문자)
+ if (!isValidPassword(newPassword)) {
+ setPasswordError("8자리 이상, 특수문자를 포함해야 합니다.");
+ hasError = true;
+ }
+
+ // 2) 비밀번호 확인 일치 여부 검증
+ if (newPassword !== passwordConfirm) {
+ setPasswordConfirmError("비밀번호가 일치하지 않습니다.");
+ hasError = true;
+ }
+
+ if (hasError) return;
+
+ // 최종 payload 생성 (이메일 + 코드 + 새 비밀번호)
+ const forgotPayload = { email, code, newPassword };
+
+ // 디버깅용 출력 (나중에 Supabase 비밀번호 재설정 API로 교체)
+ console.log("🔑 Forgot Password Reset Step:", forgotPayload);
+
+ // 전체 입력값 초기화
+ reset();
+ });
+
+ return (
+
+ );
+}
diff --git a/src/components/auth/forgotPassword/ForgotPasswordVerifyStep.tsx b/src/components/auth/forgotPassword/ForgotPasswordVerifyStep.tsx
new file mode 100644
index 0000000..fadd6a3
--- /dev/null
+++ b/src/components/auth/forgotPassword/ForgotPasswordVerifyStep.tsx
@@ -0,0 +1,113 @@
+"use client";
+
+import { useState } from "react";
+
+import { useAuthFormSubmit } from "@/hooks/useAuthFormSubmit";
+import { useOtpCode } from "@/hooks/useOtpCode";
+import { CODE_LENGTH } from "@/lib/constants";
+import { cn } from "@/lib/utils";
+import { isValidCode } from "@/lib/validators";
+import { useForgotPasswordFormStore } from "@/stores/forgotPasswordFormStore";
+import { useForgotPasswordStepStore } from "@/stores/forgotPasswordStepStore";
+
+import { Button } from "@/shared/button";
+import { Input } from "@/shared/input";
+import type { StepFieldMeta } from "@/types/auth";
+
+export function ForgotPasswordVerifyStep({ fieldId, fieldName }: StepFieldMeta) {
+ // ✅ 1) 전역 스토어에서 code setter 가져오기
+ const { setCode } = useForgotPasswordFormStore();
+
+ // ✅ 2) 스텝 이동 액션
+ const { goNext, goPrev } = useForgotPasswordStepStore();
+
+ // ✅ 3) OTP 훅: 4자리 코드 입력 UX 관리
+ const { values, inputRefs, handleChange, handleKeyDown, codeValue } = useOtpCode(CODE_LENGTH);
+
+ // ✅ 4) 코드 에러 메시지 (로컬 상태)
+ const [codeError, setCodeError] = useState("");
+
+ // ✅ 5) submit 핸들러 (공통 훅 사용)
+ const handleSubmit = useAuthFormSubmit(() => {
+ let hasError = false;
+
+ // 숫자 4자리 검증
+ if (!isValidCode(codeValue)) {
+ setCodeError("4자리 숫자 인증번호를 정확히 입력해주세요.");
+ hasError = true;
+ }
+
+ if (hasError) return;
+
+ // 전역 스토어에 최종 코드 저장
+ setCode(codeValue);
+
+ // 디버깅용 로그
+ console.log("✅ Forgot Password Verify Step:", { code: codeValue });
+
+ // 다음 스텝(비밀번호 재설정)으로 이동
+ goNext();
+ });
+
+ return (
+
+ );
+}
diff --git a/src/components/auth/login/LoginForm.tsx b/src/components/auth/login/LoginForm.tsx
index f287c07..8fbd95d 100644
--- a/src/components/auth/login/LoginForm.tsx
+++ b/src/components/auth/login/LoginForm.tsx
@@ -1,138 +1,154 @@
"use client";
-import { useFadeSlideInOnMount } from "@/hooks/useMotionPresets";
+import { useAuthFormSubmit } from "@/hooks/useAuthFormSubmit";
+import { usePasswordVisibility } from "@/hooks/usePasswordVisibility";
import { cn } from "@/lib/utils";
+import { isValidEmail, isValidPassword } from "@/lib/validators";
+import { authFadeSlideUp, authTransition } from "@/lib/variants/motion.auth";
+import { useLoginFormStore } from "@/stores/loginFormStore";
+
import { Button } from "@/shared/button";
import { Icon } from "@/shared/Icon";
import { Input } from "@/shared/input";
-import { useAuthFormStore } from "@/stores/authForm.store";
+
+import { motion } from "framer-motion";
import Link from "next/link";
-import type { ChangeEvent, FormEvent } from "react";
+import { useState } 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;
+ // ✅ 1) 전역 스토어: 이메일/비밀번호 값 + 액션
+ const { email, password, setEmail, setPassword, reset } = useLoginFormStore();
+
+ // ✅ 2) 로컬 에러 상태: UI 전용
+ const [emailError, setEmailError] = useState("");
+ const [passwordError, setPasswordError] = useState("");
+
+ // ✅ 3) 비밀번호 토글 훅
+ const { inputType, iconName, ariaLabel, toggleVisibility } = usePasswordVisibility(false);
+
+ // ✅ 4) 공통 submit 훅으로 preventDefault 처리
+ const handleSubmit = useAuthFormSubmit(() => {
+ let hasError = false;
+
+ // 이메일 검증
+ if (!isValidEmail(email)) {
+ setEmailError("올바른 이메일을 입력해주세요.");
+ hasError = true;
}
- // TODO: 실제 로그인 요청 로직
- // ex) await login({ email, password });
- };
+ // 비밀번호 검증 (8자리 이상 + 특수문자)
+ if (!isValidPassword(password)) {
+ setPasswordError("8자리 이상, 특수문자를 포함해야 합니다.");
+ hasError = true;
+ }
+
+ if (hasError) {
+ return; // ❌ 에러가 하나라도 있으면 제출 중단
+ }
- const handleEmailChange = (event: ChangeEvent) => {
- setEmail(event.target.value);
- };
+ // ✅ 5) 검증 통과 시: 로그인 데이터 콘솔 출력
+ // (나중에 이 자리에서 Supabase Auth 요청으로 교체)
- const handlePasswordChange = (event: ChangeEvent) => {
- setPassword(event.target.value);
- };
+ console.log("🟢 Login submit:", { email, password });
- const fadeClass = useFadeSlideInOnMount("up");
+ // ✅ 6) 성공 후 인풋 값 초기화
+ reset();
+ });
return (
-