비밀번호 찾기 폼 (ForgotPasswordForm)
비밀번호를 잊은 사용자가 임시 비밀번호를 SMS로 받는 화면입니다. 옵션이 하나도 없는 컴포넌트라, 볼 것은 네 가지 상태와 notice 세 자리입니다.
Alpine 컴포넌트 · Web · 진행 중(WIP) · 문서 포맷 시범 적용
Interactive playground
forgot-password-form.html 마크업 그대로예요. 옵션이 하나도 없어서 코드는 늘 같습니다 — 아래 항목은 전부 서버·사용자가 만드는 상태입니다. <!-- 옵션이 없다 — 제목·부제는 init() 이 로케일에서 채운다 -->
<div x-data="forgotPasswordForm">
<!-- include "../../packages/components/html/forgot-password-form.html" -->
</div>기본 — 위에 안내 notice 가 상시로 붙어 있습니다(오류와 자리가 다릅니다)
오류 — .ds-notice--danger 는 입력과 버튼 사이에 들어갑니다
발송 중 — 버튼 안 아이콘 스피너
발송 완료 — 폼이 사라지고 성공 notice + 돌아가기 버튼만 남습니다
--info), 버튼 위 오류(--danger), 성공 뒤 완료(--success). 같은 박스처럼 보이지만 자리와 변형이 각각 정해져 있습니다. 넘길 옵션이 하나도 없어서 코드는 늘 같습니다. 팩토리가 인자를 받지 않고 제목·부제도 init()이 로케일에서 채웁니다 — 그래서 아홉 앱의 호출이 글자 하나까지 같습니다.
규칙 (강제) · 코딩 에이전트용
ForgotPasswordForm 강제 규칙. 상세는 아래 본문.
마크업
- 호스트가
x-data="forgotPasswordForm"으로 감싸고packages/components/html/forgot-password-form.html을 include 합니다. - 옵션을 넘기지 마세요. 팩토리가 인자를 받지 않아
forgotPasswordForm({ … })는 조용히 무시됩니다.
⚠️ 트랩 ① 링크가 아니라 임시 비밀번호를 보냅니다 흔한 「재설정 링크 메일」 방식이 아닙니다. 서버가 임시 비밀번호를 만들어 SMS로 보내고, 사용자는 그것으로 로그인한 뒤 비밀번호 변경 화면으로 강제 이동합니다. 안내 문구를 링크 방식으로 고쳐 쓰지 마세요.
⚠️ 트랩 ② notice 가 세 자리에 서로 다른 변형으로 있습니다 맨 위 상시 안내는 --info, 버튼 위 오류는 --danger, 발송 뒤 완료는 --success입니다. 같은 박스처럼 보이지만 자리와 변형이 각각 정해져 있으니 하나로 합치지 마세요.
⚠️ 트랩 ③ 오류 문구는 서버 message 가 그대로 뜹니다err.message를 그대로 화면에 넣습니다. API에서 던지는 message는 한글 · 해요체여야 합니다.
필드 규격은 로그인과 같습니다 라벨 text-sm + mb-1.5(6px), 입력 ds-input--48(48px · 모서리 4 · 좌우 16 · 14px — 2026-10-01 게이트 화면 통일). 2026-09-08 에 네 인증 폼을 로그인 기준으로 통일했습니다 — 예전에 이 파일만 갖고 있던 손수 짠 유틸과 인라인 style="height: 40px" 는 그때 걷어냈습니다.
- 다크 모드(2026-10-19) — 위 열쇠 아이콘 원은
bg-status-warning-bg(전bg-yellow-50고정 — 다크에서 밝은 노란 원으로 떠 보임)로, 글자 · 링크는 label 토큰으로 바꿨다. 바깥 상자는 로그인과 같다. 확인: Playground 「미리보기 테마 → 다크」.
무슨 흐름인가
링크를 보내는 방식이 아닙니다. 임시 비밀번호를 SMS로 보냅니다.
- 사용자가 사번 또는 이메일을 넣고 「임시 비밀번호 발송」
- 서버가 임시 비밀번호를 만들어 등록된 휴대폰으로 SMS 발송
- 사용자가 그 비밀번호로 로그인
- 스토어의
requiresPasswordChange가 켜져 라우터가 비밀번호 변경으로 강제 이동
그래서 이 화면은 혼자 완결되지 않습니다. 4단계까지 가야 끝이고, 세 화면(찾기 → 로그인 → 변경)이 한 흐름입니다.
어디로 들어오나
로그인 화면의 「비밀번호 찾기」 링크입니다. 그 링크는 서버가 이 기능을 켰을 때만 뜹니다(sdk.auth.passwordResetAvailable()) — 앱이 옵션으로 켜고 끄는 것이 아닙니다.
goBack()은 두 자리(폼 아래, 그리고 발송 완료 뒤)에서 /signin으로 돌려보냅니다.
notice 세 자리
이 작은 폼에 notice가 세 개 있고, 자리와 변형이 각각 다릅니다.
| 자리 | 변형 | 언제 |
|---|---|---|
| 폼 맨 위 | --info | 항상 — 「사번 또는 이메일을 잊으신 경우 관리자에게 문의해 주세요」 |
| 입력과 버튼 사이 | --danger | 요청 실패 |
| 폼 대신 | --success | 발송 완료 (폼이 통째로 사라짐) |
맨 위 안내는 오류가 아니라 상시 안내입니다. 오류가 났을 때 그 자리를 덮어쓰지 않고 아래에 따로 뜹니다.
상태
| 상태 | 화면 |
|---|---|
| 기본 | 열쇠 배지 + 제목·부제 + 안내 notice + ID 입력 + 버튼 + 돌아가기 |
| 오류 | 위와 같고 입력과 버튼 사이에 --danger notice |
| 발송 중 | 버튼 안 아이콘 스피너 + 「발송 중...」 |
| 발송 완료 | 폼이 통째로 사라지고 --success notice + 「로그인으로 돌아가기」 버튼 |
발송 완료에 버튼이 남아 있는 것이 비밀번호 변경과 다른 점입니다. 변경 화면은 자동으로 홈으로 가지만, 여기는 사용자가 SMS를 확인한 뒤 스스로 로그인하러 가야 하므로 버튼을 남깁니다.
아이콘 배지
상단의 노란 원형 배지(w-12 h-12 bg-yellow-50 rounded-full)에 열쇠(ic-key)가 들어갑니다. 비밀번호 변경은 **같은 배지에 자물쇠(ic-lock)**입니다 — 두 화면을 구분하는 유일한 시각 신호이므로 아이콘을 바꾸지 마세요.
검증
공백을 뺀 ID가 비어 있는지 하나만 봅니다(auth.identifier_required). 형식(이메일인지 사번인지)은 서버가 판단합니다 — 화면에서 갈라내지 마세요.
버튼은 입력이 비면 죽습니다(:disabled="isLoading || !identifier.trim()"). 비밀번호 변경은 반대로 항상 눌리므로, 두 화면의 방식이 다릅니다.
호출 패턴
<!-- 옵션이 없다 — 제목·부제는 init() 이 로케일에서 채운다 -->
<div x-data="forgotPasswordForm">
<!-- include "../../packages/components/html/forgot-password-form.html" -->
</div>아홉 앱의 호출이 글자 하나까지 같습니다.
함께 필요한 것
- 라우터 스토어 —
goBack()이navigate('/signin')을 부릅니다. 없으면 조용히 아무 데도 안 갑니다(옵셔널 체이닝이라 에러도 안 납니다). hereby스토어 — 문구(t())와sdk.auth.requestPasswordReset(identifier)./forgot-password라우트 — 라우터의 인증 경로 목록(AUTH_ROUTES)에 있어야 로그인 전에 열립니다.- 아이콘 레지스트리 —
ic-key·ic-spinner·ic-info-circle·ic-check-circle·ic-exclamation-circle.
API
forgotPasswordForm Alpine.data factory. 인자를 받지 않습니다.
노출하는 상태
| 멤버 | 설명 |
|---|---|
title · subtitle | init()이 로케일에서 채웁니다 (「비밀번호 찾기」 / 「SMS를 받지 못한 경우 관리자에게 문의해 주세요.」) |
identifier | 사번 또는 이메일 |
error | 서버 오류 문구 (notice) |
isLoading · success | 발송 중 · 발송 완료 |
메서드
| 멤버 | 설명 |
|---|---|
init() | 제목·부제를 로케일에서 채웁니다. 옵션 폴백이 아니라 항상 덮어씁니다 |
submit() | 공백 검사 후 sdk.auth.requestPasswordReset(identifier.trim()) → success = true |
goBack() | router.navigate('/signin') |
init() 이 제목을 항상 덮어씁니다
ChangePasswordForm의 init()은 if (!this.title)로 비어 있을 때만 채우는데, 여기는 조건 없이 대입합니다. 그래서 옵션을 넘길 수 있게 바꾸더라도 init()을 함께 고치지 않으면 값이 덮입니다.
사용처
아홉 앱이 같은 partial을 인라인합니다.
admin · drive · employee-app · general-affairs · hr-system · inventory · management-dashboard · module-manager · team-manager
ChangePasswordForm보다 한 곳 많습니다 — module-manager는 찾기 화면은 있는데 변경 화면이 없습니다.
단일 출처
| 파트 | 파일 |
|---|---|
| Alpine 컴포넌트 | packages/components/src/components/ForgotPasswordForm.ts |
| HTML partial | packages/components/html/forgot-password-form.html |
| Alpine 등록 | forgotPasswordForm (plugin.ts) |
검토 필요 사항
- module-manager 는 찾기 화면만 있고 변경 화면이 없습니다. 임시 비밀번호를 받아 로그인하면 강제 변경 상태가 되는데, 그 앱에는 갈 화면이 없습니다.
init()이 제목·부제를 조건 없이 덮어씁니다. ChangePasswordForm은 비어 있을 때만 채웁니다. 두 컴포넌트가 서로 다른 방식이라 하나로 맞출지 결정이 필요합니다.
변경 이력
| 날짜 | 내용 |
|---|---|
| 2026-09-08 | ForgotPasswordForm 페이지 신규 작성 — Playground(상태 4종) + 모아보기 4상태 |