Password Reset
사용자용 컴포넌트서비스를 쓰는 일반 사용자가 보는 화면에 쓰입니다.
이메일 링크로 비밀번호를 다시 정하는 비밀번호 찾기·재설정 화면입니다.
비밀번호를 잊은 사용자가 이메일로 받은 링크를 통해 새 비밀번호를 정하는 기능입니다. 두 화면으로 이루어집니다: 이메일을 입력받는 ForgotPasswordForm(로그인 모달 안)과, 메일 링크로 들어오는 ResetPasswordForm(/reset-password 페이지).
Flow
① 로그인 화면에서 "비밀번호를 잊으셨나요?" 클릭
↓
② 이메일 입력 → POST /auth/password/forgot
↓
③ "메일을 확인해 주세요" 안내 (가입 여부와 관계없이 같은 화면)
↓
④ 사용자가 메일의 링크 클릭 → /reset-password?token=...
↓
⑤ 새 비밀번호 입력 → POST /auth/password/reset { token, password }
↓
⑥ "비밀번호를 변경했습니다" → 로그인Forgot password
로그인 모달에서 비밀번호를 잊으셨나요?를 누르면 이 폼으로 바뀝니다.
tsx
<ForgotPasswordForm
onSubmit={forgot.mutate}
pending={forgot.isPending}
sent={forgot.isSuccess}
onBack={() => setMode("login")}
/>- 이메일을 보내면 입력한 주소와 함께 "메일을 확인해 주세요" 안내로 부드럽게 바뀝니다.
- 가입된 이메일인지 여부는 알려 주지 않습니다. 서버가 항상 같은 응답을 주기 때문에 화면도 항상 같은 안내를 보여 줍니다 (계정 존재 여부 노출 방지).
Reset password
메일 링크로 들어오는 /reset-password?token=... 페이지에서 쓰는 폼입니다.
tsx
<ResetPasswordForm
onSubmit={({ password }) => reset.mutate({ token, password })}
pending={reset.isPending}
done={reset.isSuccess}
onLogin={() => openDialog("login")}
/>| 상황 | 화면 |
|---|---|
| 8자 미만 | 비밀번호는 8자 이상이어야 합니다. |
| 두 입력이 다름 | 비밀번호가 일치하지 않습니다. |
| 서버가 거절 (만료·이미 사용한 토큰) | 서버의 detail 메시지를 그대로 보여 줍니다. |
| 성공 | "비밀번호를 변경했습니다"와 로그인 버튼으로 바뀝니다. |
| 주소에 token이 없음 | "유효하지 않은 링크입니다" 안내만 보여 줍니다. |
API
POST /auth/password/forgot
// request
{ "email": "user@example.com" }
// response 202 (가입 여부와 관계없이 항상 동일)POST /auth/password/reset
// request
{ "token": "메일 링크의 토큰", "password": "새 비밀번호" }
// response 204 | 400 { "detail": "링크가 만료되었거나 이미 사용되었습니다." }Server rules (planned)
- 토큰: 추측할 수 없는 긴 난수. DB에는 해시만 저장하고, 메일에는 원본을 보냅니다.
- 만료: 짧게(예: 15~30분). 만료된 토큰은 거절합니다.
- 단일 사용: 한 번 쓰면 폐기합니다. 새 요청 시 이전 토큰도 폐기하는 것을 권장합니다.
- 횟수 제한: 같은 이메일·IP의 재요청을 제한해 메일 폭탄을 막습니다.
- 변경 후: 기존 로그인 세션(리프레시 토큰)을 폐기하고, 가능하면 변경 알림 메일을 보냅니다.
메일을 실제로 보내는 부분은 서비스가 준비해야 합니다. 로컬에서는 메일 캐처(예: Mailpit)를 docker-compose에 두고 확인하는 방식을 권장합니다.
Change the text
화면의 문구(제목, 안내, 버튼, 메시지)는 컴포넌트 파일 안에 그대로 적혀 있어서 해당 파일을 열어 글자만 바꾸면 됩니다. 어디에 무엇이 있는지 정리했습니다.
| 바꾸고 싶은 문구 | 파일 | 찾는 방법 |
|---|---|---|
| 모달 제목과 설명 ("비밀번호 찾기", "가입한 이메일로 재설정 링크를…") | auth/components/AuthDialog.tsx | copy.forgot의 title, desc |
| 이메일 입력 라벨·안내, [재설정 링크 받기] 버튼, "메일을 확인해 주세요" 안내 | auth/components/ForgotPasswordForm.tsx | 화면에 보이는 한글을 그대로 검색 |
| 로그인 화면의 "비밀번호를 잊으셨나요?" 링크 | auth/components/LoginForm.tsx | onForgotPassword 근처의 버튼 글자 |
| 재설정 화면의 제목·설명 ("비밀번호 재설정", "새로 사용할 비밀번호를…"), 잘못된 링크 안내 | auth/ResetPasswordPage.tsx | h1과 안내 문단 |
| 새 비밀번호 입력 라벨, 8자·불일치 오류, "변경했습니다" 완료 안내 | auth/components/ResetPasswordForm.tsx | 오류 문구와 완료 화면의 글자 |
| 서버가 돌려주는 오류 문구 (링크 만료 등) | 서버 | 응답의 detail 값을 그대로 보여 줍니다 |
| 받는 사람이 보는 메일의 제목·본문 | 서버 (메일 템플릿) | 메일은 서버가 만들어 보냅니다 |
auth/components/AuthDialog.tsx
const copy = {
login: { title: "로그인", desc: "아이디와 비밀번호를 입력하세요." },
signup: { title: "회원가입", desc: "정보를 입력해 계정을 만드세요." },
forgot: { title: "비밀번호 찾기", desc: "가입한 이메일로 재설정 링크를 보내 드립니다." }, // ← 여기를 고칩니다
} as const;auth/components/ForgotPasswordForm.tsx
<p className="text-sm font-semibold">메일을 확인해 주세요</p> // 안내 제목
...
<Button type="submit">{pending ? "보내는 중…" : "재설정 링크 받기"}</Button> // 버튼 글자킷 폴더 안의 파일을 직접 고치면 킷을 업데이트할 때 충돌할 수 있습니다. 문구를 많이 바꿔야 한다면 해당 컴포넌트를 복사해 서비스 폴더(
app/(service)/)에 두고 쓰는 것을 권장합니다.Customization
- 메일 링크 주소는
meerkitConfig.routes.resetPassword(/reset-password)입니다. 서버가 메일 본문에 이 주소를 만들 때 같은 값을 쓰세요. - 비밀번호 최소 길이(8자)는
ResetPasswordForm.tsx에서 바꿉니다. 서버 정책과 같게 유지하세요. - 로그인 화면의 "비밀번호를 잊으셨나요?" 링크를 없애려면
LoginForm에onForgotPassword를 넘기지 않으면 됩니다.
Props
ForgotPasswordForm
| Prop | Type | 설명 |
|---|---|---|
onSubmit필수 | (input: { email }) => void | 이메일을 제출할 때 호출합니다. |
sent | boolean | true면 "메일을 확인해 주세요" 안내로 바뀝니다. |
pending | boolean | 요청 중 여부. |
error | string | null | 서버 오류 메시지. |
onBack | () => void | 안내 화면의 "로그인으로 돌아가기" 동작. |
ResetPasswordForm
| Prop | Type | 설명 |
|---|---|---|
onSubmit필수 | (input: { password }) => void | 검증을 통과하면 새 비밀번호를 전달합니다. 토큰은 부모가 붙입니다. |
done | boolean | true면 "변경했습니다" 안내로 바뀝니다. |
pending | boolean | 요청 중 여부. |
error | string | null | 서버 오류 메시지. |
onLogin | () => void | 완료 화면의 로그인 버튼 동작. |
Related API
이 컴포넌트가 호출하는 API입니다. 누르면 요청·응답 명세로 이동합니다.
