Skip to content

비밀번호 변경 폼 (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()이 로케일에서 채우므로 여덟 앱 모두 인자 없이 씁니다.

규칙 (강제) · 코딩 에이전트용

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 「미리보기 테마 → 다크」.

언제 뜨나 ​

로그인은 됐지만 비밀번호를 반드시 바꿔야 하는 상태일 때입니다. 두 경우입니다.

  1. 관리자가 비밀번호를 초기화해 임시 비밀번호를 발급한 뒤
  2. 비밀번호 찾기로 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)**입니다 — 두 화면을 구분하는 유일한 시각 신호이므로 아이콘을 바꾸지 마세요.

호출 패턴 ​

html
<!-- 넘길 것이 없으면 인자 없이 쓴다 — 제목·부제는 init() 이 로케일에서 채운다 -->
<div x-data="changePasswordForm">
  <!-- include "../../packages/components/html/change-password-form.html" -->
</div>

여덟 앱 모두 이 형태입니다. 제목·부제를 바꿔야 할 때만 옵션을 넘깁니다.

html
<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타입기본값설명
titlestring"" → auth.change_password_title비우면 init()이 「비밀번호 변경」을 넣습니다
subtitlestring"" → 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초 뒤 /

성공 뒤 순서 ​

  1. success = true — 폼이 사라지고 성공 notice가 뜹니다
  2. onSuccess?.() 호출
  3. 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 partialpackages/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-08ChangePasswordForm 페이지 신규 작성 — Playground(제목·부제 × 상태 5종) + 모아보기 5상태