Skip to content

Commit 9a14524

Browse files
committed
CDP-217 feat✨ (forgotpassword): 비밀번호 찾기 페이지 스타일 및 애니메이션 상태 추가
1 parent 2096f26 commit 9a14524

10 files changed

Lines changed: 360 additions & 30 deletions

File tree

src/app/(auth)/forgot-password/page.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { AuthMain } from "@/components/auth/AuthMain";
2+
import { ForgotPasswordForm } from "@/components/auth/forgotPassword/ForgotPasswordForm";
3+
import { ForgotPasswordHeader } from "@/components/auth/forgotPassword/ForgotPasswordHeader";
14
import { makePageMetadata } from "@/seo/metadata";
25

36
export const metadata = {
@@ -10,5 +13,12 @@ export const metadata = {
1013
};
1114

1215
export default function ForgotPasswordPage() {
13-
return <div>비밀번호 찾기 - 이메일 입력 페이지</div>;
16+
return (
17+
<>
18+
<ForgotPasswordHeader />
19+
<AuthMain flow="forgot">
20+
<ForgotPasswordForm />
21+
</AuthMain>
22+
</>
23+
);
1424
}

src/components/auth/AuthMain.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
"use client";
22

3+
import { useForgotPasswordStepStore } from "@/stores/forgotPasswordStepStore";
34
import { useSignupStepStore } from "@/stores/signupStepStore";
45
import type { AuthMainProps } from "@/types/auth";
56
import { useEffect } from "react";
67

78
export function AuthMain({ children, flow }: AuthMainProps) {
89
const resetSignupStep = useSignupStepStore((s) => s.reset);
10+
const resetForgotStep = useForgotPasswordStepStore((s) => s.reset);
911

1012
useEffect(() => {
1113
if (flow === "signup") {
1214
resetSignupStep();
1315
}
14-
}, [flow, resetSignupStep]);
16+
if (flow === "forgot") {
17+
resetForgotStep();
18+
}
19+
}, [flow, resetSignupStep, resetForgotStep]);
20+
1521
return (
1622
<main className="w-full" aria-label="인증 본문">
1723
{children}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use client";
2+
3+
import { Input } from "@/shared/input";
4+
import type { StepFieldMeta } from "@/types/auth";
5+
6+
export function ForgotPasswordEmailStep({ fieldId, fieldName }: StepFieldMeta) {
7+
return (
8+
<div className="flex flex-col gap-3">
9+
<label htmlFor={fieldId} className="t-14-m text-[var(--color-gray-700)]">
10+
이메일
11+
</label>
12+
<Input
13+
id={fieldId}
14+
name={fieldName}
15+
type="email"
16+
placeholder="이메일을 입력해주세요"
17+
status="default"
18+
autoComplete="email"
19+
/>
20+
<p className="t-12-m text-[var(--color-gray-500)]">
21+
입력하신 이메일 주소로 비밀번호 재설정 링크 또는 인증 코드를 보내드릴게요.
22+
</p>
23+
</div>
24+
);
25+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"use client";
2+
3+
import { FORGOT_PASSWORD_STEP_FIELD_META, FORGOT_PASSWORD_STEP_ORDER } from "@/lib/constants";
4+
import { cn } from "@/lib/utils";
5+
import { Button } from "@/shared/button";
6+
import { useForgotPasswordStepStore } from "@/stores/forgotPasswordStepStore";
7+
import Link from "next/link";
8+
import { AuthStepTransition } from "../AuthStepTransition";
9+
import { StepIndicator } from "../StepIndicator";
10+
import { ForgotPasswordEmailStep } from "./ForgotPasswordEmailStep";
11+
import { ForgotPasswordResetStep } from "./ForgotPasswordResetStep";
12+
import { ForgotPasswordVerifyStep } from "./ForgotPasswordVerifyStep";
13+
14+
export function ForgotPasswordForm() {
15+
const { step, direction, goNext, goPrev } = useForgotPasswordStepStore();
16+
17+
const isEmailStep = step === "email";
18+
const isVerifyStep = step === "verify";
19+
const isResetStep = step === "reset";
20+
21+
// 현재 스텝 번호 (1부터 시작)
22+
const currentStepIndex = FORGOT_PASSWORD_STEP_ORDER.indexOf(step);
23+
const currentStepNumber = currentStepIndex + 1;
24+
25+
// 스텝별 필드 메타 (id / name)
26+
const { fieldId, fieldName } = FORGOT_PASSWORD_STEP_FIELD_META[step];
27+
28+
return (
29+
<AuthStepTransition stepKey={step} direction={direction}>
30+
<form
31+
noValidate
32+
aria-label="비밀번호 재설정 폼"
33+
className={cn(
34+
"flex flex-col items-center gap-10 rounded-2xl border border-[var(--color-gray-200)] bg-[var(--color-white)] px-4 py-6",
35+
"md:px-6 md:py-8",
36+
)}
37+
>
38+
{/* 진행 단계 인디케이터 (3단계) */}
39+
<StepIndicator currentStep={currentStepNumber} totalSteps={3} className="mb-3" />
40+
41+
{/* 메인 필드 영역 */}
42+
<div className="mx-auto flex w-full max-w-[36.6rem] flex-col gap-12">
43+
{isEmailStep && <ForgotPasswordEmailStep fieldId={fieldId} fieldName={fieldName} />}
44+
{isVerifyStep && <ForgotPasswordVerifyStep fieldId={fieldId} fieldName={fieldName} />}
45+
{isResetStep && <ForgotPasswordResetStep fieldId={fieldId} fieldName={fieldName} />}
46+
</div>
47+
48+
{/* 다음 / 이전 버튼 영역 */}
49+
<div className="mx-auto flex w-full max-w-[36.6rem] flex-col gap-4">
50+
{!isResetStep && (
51+
<Button type="button" preset="auth" bg="basic" className="w-full" onClick={goNext}>
52+
다음
53+
</Button>
54+
)}
55+
56+
{isResetStep && (
57+
<Button type="button" preset="auth" bg="basic" className="w-full">
58+
비밀번호 재설정 완료
59+
</Button>
60+
)}
61+
62+
{!isEmailStep && (
63+
<Button type="button" preset="auth" bg="white" className="w-full" onClick={goPrev}>
64+
이전
65+
</Button>
66+
)}
67+
</div>
68+
69+
{/* 하단 로그인 링크 */}
70+
<p className="t-14-m text-center text-[var(--color-gray-600)]">
71+
비밀번호를 이미 재설정하셨나요?{" "}
72+
<Link
73+
href="/login"
74+
className="t-14-m text-[var(--color-gray-900)] underline-offset-2 hover:underline"
75+
>
76+
로그인하기
77+
</Link>
78+
</p>
79+
</form>
80+
</AuthStepTransition>
81+
);
82+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"use client";
2+
3+
import { AuthHeader } from "@/components/auth/AuthHeader";
4+
import { SubTitle, Title } from "@/components/auth/AuthTitle";
5+
import { FORGOT_PASSWORD_STEP_COPY } from "@/lib/constants";
6+
import { useForgotPasswordStepStore } from "@/stores/forgotPasswordStepStore";
7+
8+
export function ForgotPasswordHeader() {
9+
const step = useForgotPasswordStepStore((s) => s.step);
10+
const { title, subtitle } = FORGOT_PASSWORD_STEP_COPY[step];
11+
12+
return (
13+
<AuthHeader>
14+
<Title>{title}</Title>
15+
<SubTitle>{subtitle}</SubTitle>
16+
</AuthHeader>
17+
);
18+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"use client";
2+
3+
import { Input } from "@/shared/input";
4+
import type { StepFieldMeta } from "@/types/auth";
5+
6+
export function ForgotPasswordResetStep({ fieldId, fieldName }: StepFieldMeta) {
7+
return (
8+
<>
9+
<div className="flex flex-col gap-2">
10+
<label htmlFor={`${fieldId}-password`} className="t-14-m text-[var(--color-gray-700)]">
11+
새 비밀번호
12+
</label>
13+
<Input
14+
id={`${fieldId}-password`}
15+
name={fieldName}
16+
type="password"
17+
placeholder="새 비밀번호를 입력해주세요"
18+
status="default"
19+
autoComplete="new-password"
20+
/>
21+
</div>
22+
23+
<div className="flex flex-col gap-2">
24+
<label htmlFor={`${fieldId}-confirm`} className="t-14-m text-[var(--color-gray-700)]">
25+
새 비밀번호 확인
26+
</label>
27+
<Input
28+
id={`${fieldId}-confirm`}
29+
name={`${fieldName}Confirm`}
30+
type="password"
31+
placeholder="새 비밀번호를 한 번 더 입력해주세요"
32+
status="default"
33+
autoComplete="new-password"
34+
/>
35+
</div>
36+
</>
37+
);
38+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"use client";
2+
3+
import { cn } from "@/lib/utils";
4+
import type { StepFieldMeta } from "@/types/auth";
5+
6+
const CODE_LENGTH = 4;
7+
8+
export function ForgotPasswordVerifyStep({ fieldId, fieldName }: StepFieldMeta) {
9+
return (
10+
<div className="flex flex-col gap-6 items-center">
11+
{/* 라벨 + 4자리 코드 입력 */}
12+
<div className="flex flex-col gap-3">
13+
<label htmlFor={fieldId} className="t-14-m text-[var(--color-gray-700)] text-center">
14+
인증번호 (4자리)
15+
</label>
16+
17+
<div className="flex gap-4">
18+
{Array.from({ length: CODE_LENGTH }).map((_, index) => (
19+
<input
20+
key={index}
21+
id={index === 0 ? fieldId : undefined}
22+
name={`${fieldName}[${index}]`}
23+
type="text"
24+
inputMode="numeric"
25+
pattern="[0-9]*"
26+
maxLength={1}
27+
className={cn(
28+
"h-16 w-16 rounded-2xl border border-[var(--color-gray-200)] bg-[var(--color-white)]",
29+
"text-center t-20-b text-[var(--color-gray-900)] outline-none",
30+
"focus:border-[var(--color-gray-900)] focus:ring-2 focus:ring-[var(--color-gray-900)]/10",
31+
)}
32+
aria-label={`인증번호 ${index + 1}번째 숫자`}
33+
/>
34+
))}
35+
</div>
36+
</div>
37+
38+
{/* 재발송 안내 */}
39+
<p className="t-12-m text-center text-[var(--color-gray-500)]">
40+
인증번호를 받지 못하셨나요?{" "}
41+
<button
42+
type="button"
43+
className="underline-offset-2 hover:underline text-[var(--color-gray-700)]"
44+
>
45+
재발송
46+
</button>
47+
</p>
48+
</div>
49+
);
50+
}

src/lib/constants.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,12 @@ export const SPECIAL_FEATURES: SpecialFeatureItem[] = [
116116
🧭 회원가입 단계 — 순서 정의 (Zustand + Router 공용)
117117
---------------------------------------------- */
118118

119-
import type { SignupStepFieldMeta, SignupStepKey } from "@/types/auth";
119+
import type { ForgotPasswordStepKey, SignupStepKey, StepFieldMeta } from "@/types/auth";
120120

121121
export const SIGNUP_STEP_ORDER: SignupStepKey[] = ["email", "name", "password", "terms"];
122122

123123
/** 스텝별 필드 id/name 메타 */
124-
export const SIGNUP_STEP_FIELD_META: Record<SignupStepKey, SignupStepFieldMeta> = {
124+
export const SIGNUP_STEP_FIELD_META: Record<SignupStepKey, StepFieldMeta> = {
125125
email: {
126126
fieldId: "email",
127127
fieldName: "email",
@@ -160,3 +160,44 @@ export const SIGNUP_STEP_COPY: Record<SignupStepKey, { title: string; subtitle:
160160
subtitle: "서비스 이용약관과 개인정보 처리방침에 동의해 주세요.",
161161
},
162162
};
163+
164+
/* ----------------------------------------------
165+
⚙️ 비밀번호 찾기 — 순서 정의 (Zustand + Router 공용)
166+
---------------------------------------------- */
167+
/** ✅ 비밀번호 재설정 스텝 순서 (3단계) */
168+
export const FORGOT_PASSWORD_STEP_ORDER: ForgotPasswordStepKey[] = ["email", "verify", "reset"];
169+
170+
/** ✅ 비밀번호 재설정 스텝별 타이틀/서브카피 */
171+
export const FORGOT_PASSWORD_STEP_COPY: Record<
172+
ForgotPasswordStepKey,
173+
{ title: string; subtitle: string }
174+
> = {
175+
email: {
176+
title: "이메일 입력",
177+
subtitle: "계정을 찾기 위해 가입하신 이메일을 입력해주세요.",
178+
},
179+
verify: {
180+
title: "이메일 인증",
181+
subtitle: "메일로 전송된 인증 코드를 입력해주세요.",
182+
},
183+
reset: {
184+
title: "비밀번호 재설정",
185+
subtitle: "새 비밀번호를 설정해 주세요.",
186+
},
187+
};
188+
189+
/** 스텝별 필드 id/name 메타 */
190+
export const FORGOT_PASSWORD_STEP_FIELD_META: Record<ForgotPasswordStepKey, StepFieldMeta> = {
191+
email: {
192+
fieldId: "forgot-email",
193+
fieldName: "email",
194+
},
195+
verify: {
196+
fieldId: "verify",
197+
fieldName: "verificationCode",
198+
},
199+
reset: {
200+
fieldId: "new-password",
201+
fieldName: "newPassword",
202+
},
203+
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"use client";
2+
3+
import { FORGOT_PASSWORD_STEP_ORDER } from "@/lib/constants";
4+
import type { ForgotPasswordStepState, StepDirection } from "@/types/auth";
5+
import { create } from "zustand";
6+
7+
export const useForgotPasswordStepStore = create<ForgotPasswordStepState>((set, get) => ({
8+
step: "email",
9+
direction: "forward",
10+
11+
goTo: (next) => {
12+
const current = get().step;
13+
if (current === next) return;
14+
15+
const currentIndex = FORGOT_PASSWORD_STEP_ORDER.indexOf(current);
16+
const nextIndex = FORGOT_PASSWORD_STEP_ORDER.indexOf(next);
17+
18+
const direction: StepDirection = nextIndex > currentIndex ? "forward" : "backward";
19+
20+
set({ step: next, direction });
21+
},
22+
23+
goNext: () => {
24+
const current = get().step;
25+
const currentIndex = FORGOT_PASSWORD_STEP_ORDER.indexOf(current);
26+
const nextIndex = Math.min(currentIndex + 1, FORGOT_PASSWORD_STEP_ORDER.length - 1);
27+
const next = FORGOT_PASSWORD_STEP_ORDER[nextIndex];
28+
29+
if (next !== current) {
30+
set({ step: next, direction: "forward" });
31+
}
32+
},
33+
34+
goPrev: () => {
35+
const current = get().step;
36+
const currentIndex = FORGOT_PASSWORD_STEP_ORDER.indexOf(current);
37+
const prevIndex = Math.max(currentIndex - 1, 0);
38+
const prev = FORGOT_PASSWORD_STEP_ORDER[prevIndex];
39+
40+
if (prev !== current) {
41+
set({ step: prev, direction: "backward" });
42+
}
43+
},
44+
45+
reset: () => {
46+
set({
47+
step: "email",
48+
direction: "forward",
49+
});
50+
},
51+
}));

0 commit comments

Comments
 (0)