비밀번호 변경 폼 (ChangePasswordForm)
임시 비밀번호로 들어온 사용자에게 강제로 새 비밀번호를 받는 화면입니다. 현재 비밀번호 칸이 왜 없는지, 성공 뒤에 무슨 일이 일어나는지 확인해 보세요.
Alpine 컴포넌트 · Web · 진행 중(WIP) · 문서 포맷 시범 적용
Interactive playground
change-password-form.html 마크업 그대로예요. 현재 비밀번호 칸이 없는 것이 규격입니다 — 로그인 흐름이 임시 비밀번호를 이미 갖고 있어서, 사용자는 새 비밀번호만 두 번 넣습니다. <!-- 넘길 것이 없으면 인자 없이 쓴다 — 제목·부제는 init() 이 로케일에서 채운다 -->
<div x-data="changePasswordForm">
<!-- include "../../packages/components/html/change-password-form.html" -->
</div>기본 — 제목·부제는 init() 이 로케일에서 채웁니다
필드 검증 오류 — 두 칸이 각각 다른 이유로 걸립니다(강도 · 불일치)
서버 오류 — .ds-notice--danger. 서버가 던진 문구가 그대로 뜹니다
처리 중 — 버튼 안 아이콘 스피너
성공 — 폼이 통째로 사라지고 notice 만 남습니다. 0.5초 뒤 자동으로 홈으로 갑니다
setTimeout 이 0.5초 뒤 / 로 보냅니다. 같은 뼈대인 ForgotPasswordForm 은 성공 뒤에 돌아가기 버튼이 있다는 점만 다릅니다. 제목·부제만 앱이 정하고 나머지는 전부 상태입니다. 넘기지 않으면 init()이 로케일에서 채우므로 여덟 앱 모두 인자 없이 씁니다.
규칙 (강제) · 코딩 에이전트용
ChangePasswordForm 강제 규칙. 상세는 아래 본문.
마크업
- 호스트가
x-data="changePasswordForm"으로 감싸고packages/components/html/change-password-form.html을 include 합니다.
⚠️ 트랩 ① 현재 비밀번호 칸을 추가하지 마세요 이 폼은 새 비밀번호 두 칸뿐입니다. 현재(임시) 비밀번호는 로그인할 때 SDK 스토어가 메모리에 잡아 두었다가(_loginPassword) changePassword()가 알아서 넘깁니다. 칸을 추가하면 사용자가 방금 넣은 값을 또 넣게 됩니다.
⚠️ 트랩 ② 그 저장된 비밀번호는 메모리에만 있습니다_loginPassword는 모듈 스코프 변수라 새로고침하면 사라집니다. /change-password에서 F5를 누르면 세션은 복원되지만(requiresPasswordChange는 다시 true) 현재 비밀번호는 빈 문자열이 되어 그대로 전송됩니다 — SDK 주석도 이 상황을 「현재 비밀번호 값 없이 진행된다」고 적어 두고 있습니다. 이 화면에 새로고침으로 진입하는 경로를 만들지 마세요.
⚠️ 트랩 ③ 이 화면은 라우터가 강제하는 곳입니다requiresPasswordChange가 true면 라우터의 handleAuthRedirect()가 어느 경로에서든 /change-password로 보내고, false가 되면 즉시 /로 되돌립니다. 화면 안에 「나중에 하기」 같은 탈출구를 만들지 마세요 — 라우터가 곧바로 되돌립니다.
⚠️ 트랩 ④ 오류 문구는 서버 message 가 그대로 뜹니다err.message를 그대로 화면에 넣습니다. API에서 던지는 message는 한글 · 해요체여야 합니다.
⚠️ 트랩 ⑤ 오류가 두 종류이고 자리가 다릅니다 검증 오류(강도 · 불일치)는 입력 아래 붉은 한 줄, 서버 오류는 .ds-notice--danger 한 덩어리입니다.
- 다크 모드(2026-10-19) — 위 자물쇠 아이콘 원은
bg-status-warning-bg(전bg-yellow-50고정), 설명 · 링크는 label 토큰. 바깥 상자는 로그인과 같다. 확인: Playground 「미리보기 테마 → 다크」.
언제 뜨나
로그인은 됐지만 비밀번호를 반드시 바꿔야 하는 상태일 때입니다. 두 경우입니다.
- 관리자가 비밀번호를 초기화해 임시 비밀번호를 발급한 뒤
- 비밀번호 찾기로 SMS 임시 비밀번호를 받아 로그인한 뒤
서버가 사용자에 mustChangePassword(또는 로그인 응답의 requiresPasswordChange)를 실어 주면 스토어의 requiresPasswordChange가 켜지고, 라우터가 /change-password로 보냅니다.
현재 비밀번호를 왜 안 받나
방금 그 비밀번호로 로그인했기 때문입니다. 로그인 성공 시 스토어가 그 값을 메모리에 잡아 두었다가(_loginPassword) changePassword()를 부를 때 currentPassword로 넘깁니다. 그래서 사용자는 새 비밀번호만 두 번 넣습니다.
그 값은 새로고침하면 사라집니다
_loginPassword는 모듈 스코프 변수라 디스크에 남지 않습니다. /change-password에서 새로고침하면 세션은 복원돼 화면은 그대로 뜨지만 현재 비밀번호는 빈 문자열이 되어 그대로 전송됩니다. SDK 주석도 이 상황을 알고 있다고 적어 두고 있습니다 — 이 화면으로 새로고침 진입하는 경로를 만들지 마세요.
빠져나갈 수 없는 화면입니다
라우터의 handleAuthRedirect()가 이렇게 돕니다.
| 상태 | 동작 |
|---|---|
로그인됨 + requiresPasswordChange + 다른 경로 | /change-password로 보냄 |
로그인됨 + !requiresPasswordChange + /change-password | /로 되돌림 |
그래서 화면 안에 「나중에 하기」·「건너뛰기」를 넣어도 소용없습니다 — 라우터가 곧바로 되돌립니다. 뒤로 가기도 같습니다.
상태
| 상태 | 화면 |
|---|---|
| 기본 | 자물쇠 배지 + 제목·부제 + 새 비밀번호 2칸 + 버튼 |
| 규칙 변경 안내 | 새 비밀번호 칸 아래 .ds-form-help 「비밀번호 규칙은 이후 설정에서 변경할 수 있어요.」(auth.password_rule_change_later) — 규칙을 바꿀 수 있는 사람(setting:update.* 또는 시스템 관리자, canChangePasswordRule)에게만. 일반 직원에겐 바꿀 수 없는 설정이라 숨긴다 |
| 필드 검증 오류 | 해당 입력에 border-red-400, 아래 붉은 한 줄 |
| 서버 오류 | 버튼 위 .ds-notice--danger |
| 처리 중 | 버튼 안 아이콘 스피너 + 「변경 중...」 |
| 성공 | 폼이 통째로 사라지고 .ds-notice--success 만. 0.5초 뒤 자동으로 / |
성공 상태에 버튼이 없는 것은 의도입니다 — 사용자가 누를 것이 없고 setTimeout이 0.5초 뒤 홈으로 보냅니다.
검증 규칙
| 필드 | 규칙 |
|---|---|
| 새 비밀번호 | 병원 설정의 일반 비밀번호 규칙(settings.userPasswordPolicy) — 최소 길이(4~20, 기본 8)와 필수 포함 문자(대문자·소문자·숫자·특수문자, 기본 전부). 특수문자는 영문·숫자가 아닌 모든 문자. 입력칸 안내(placeholder)도 같은 설정으로 만든다(describeUserPasswordPolicy) |
| 새 비밀번호 확인 | 위와 일치 |
RegisterForm의 비밀번호 규칙과 같은 정규식입니다. 한쪽만 바꾸면 가입과 변경의 기준이 어긋나므로 함께 봅니다.
제출 버튼은 항상 눌립니다
로그인과 달리 여기 버튼은 :disabled="isLoading" 뿐입니다 — 입력이 비어 있어도 눌리고, 그때 validate()가 붉은 줄을 띄웁니다. 두 화면의 방식이 다른 것이 현재 상태입니다.
아이콘 배지
상단의 노란 원형 배지(w-12 h-12 bg-yellow-50 rounded-full)에 자물쇠(ic-lock)가 들어갑니다. 비밀번호 찾기는 **같은 배지에 열쇠(ic-key)**입니다 — 두 화면을 구분하는 유일한 시각 신호이므로 아이콘을 바꾸지 마세요.
호출 패턴
<!-- 넘길 것이 없으면 인자 없이 쓴다 — 제목·부제는 init() 이 로케일에서 채운다 -->
<div x-data="changePasswordForm">
<!-- include "../../packages/components/html/change-password-form.html" -->
</div>여덟 앱 모두 이 형태입니다. 제목·부제를 바꿔야 할 때만 옵션을 넘깁니다.
<div x-data="changePasswordForm({ title: '비밀번호 재설정', subtitle: '…' })">함께 필요한 것
- 라우터 스토어 — 성공 뒤
navigate('/')를 부릅니다.router스토어가 없으면 조용히 아무 데도 안 갑니다(옵셔널 체이닝이라 에러도 안 납니다). hereby스토어 — 문구(t())와changePassword(newPassword, confirmPassword)./change-password라우트 — 라우터의 알려진 경로 목록에 있어야 합니다. 여덟 앱 모두isKnownPath에 이 경로를 넣어 두고 있습니다.- 아이콘 레지스트리 —
ic-lock·ic-eye·ic-eye-off·ic-spinner·ic-check-circle·ic-exclamation-circle.
API
changePasswordForm(opts: ChangePasswordFormOptions) Alpine.data factory.
ChangePasswordFormOptions (모두 optional)
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
title | string | "" → auth.change_password_title | 비우면 init()이 「비밀번호 변경」을 넣습니다 |
subtitle | string | "" → auth.change_password_subtitle | 비우면 init()이 「보안을 위해 새 비밀번호를 설정해주세요.」를 넣습니다 |
onSuccess | () => void | — | 변경 성공 직후, 홈으로 이동하기 전에 호출 |
노출하는 상태
| 멤버 | 설명 |
|---|---|
title · subtitle | 옵션 값 또는 init()이 채운 로케일 문구 |
newPassword · confirmPassword | 입력값 |
showNewPassword | 새 비밀번호 표시 토글 (확인 칸에는 토글이 없습니다) |
error | 서버 오류 문구 (notice) |
fieldErrors | 필드별 검증 오류 맵 (newPassword · confirmPassword) |
isLoading · success | 제출 중 · 성공 |
메서드
| 멤버 | 설명 |
|---|---|
init() | 제목·부제 폴백을 로케일에서 채웁니다 |
validate() | 강도와 일치를 검사해 fieldErrors를 채우고 통과 여부 반환 |
submit() | store.changePassword(new, confirm) 호출 → success = true → onSuccess() → 0.5초 뒤 / |
성공 뒤 순서
success = true— 폼이 사라지고 성공 notice가 뜹니다onSuccess?.()호출setTimeout(…, 500)뒤router.navigate("/")
스토어 쪽에서는 changePassword()가 성공하면 requiresPasswordChange = false로 바꾸고 저장해 둔 비밀번호를 지웁니다. 그래서 홈으로 간 뒤 라우터가 다시 이 화면으로 되돌리지 않습니다.
사용처
여덟 앱이 같은 partial을 인라인하고, 모두 인자 없이 씁니다.
admin · drive · employee-app · general-affairs · hr-system · inventory · management-dashboard · team-manager
module-manager 에는 이 화면이 없습니다
라우터의 isKnownPath에는 /change-password가 있지만 뷰를 include 하지 않습니다. 다른 여덟 앱과 다른 점이라 확인이 필요합니다.
단일 출처
| 파트 | 파일 |
|---|---|
| Alpine 컴포넌트 | packages/components/src/components/ChangePasswordForm.ts |
| HTML partial | packages/components/html/change-password-form.html |
| 현재 비밀번호 보관 | packages/sdk/src/alpine/plugin.ts (_loginPassword · changePassword) |
| Alpine 등록 | changePasswordForm (plugin.ts) |
검토 필요 사항
- 새로고침하면 현재 비밀번호가 빈 채로 전송됩니다.
_loginPassword가 메모리에만 있어서 그렇습니다. SDK 주석이 이 상황을 인지하고 있지만 화면에는 아무 안내가 없습니다 — 새로고침 뒤에는 다시 로그인하라고 알려 주거나, 그때만 현재 비밀번호 칸을 띄우는 방법이 있습니다. - module-manager 에 이 화면이 없습니다. 라우터는
/change-password를 알려진 경로로 두는데 뷰가 없어, 강제 변경 상태가 되면 갈 곳이 없습니다. - 제출 버튼의 disabled 기준이 로그인과 다릅니다. 로그인은 입력이 비면 버튼이 죽고, 여기는 눌린 뒤 붉은 줄로 알려 줍니다. 어느 쪽으로 통일할지는 결정이 필요합니다.
변경 이력
| 날짜 | 내용 |
|---|---|
| 2026-09-08 | ChangePasswordForm 페이지 신규 작성 — Playground(제목·부제 × 상태 5종) + 모아보기 5상태 |