임동욱 임동욱 08-19
merge: 관리자 등록 비밀번호 규칙 불일치 수정
@5e91c45ba76a6c5a7d0af9ff54585140329099ba
app/(protected)/(basic)/admins/_components/admin-member-create-modal.tsx
--- app/(protected)/(basic)/admins/_components/admin-member-create-modal.tsx
+++ app/(protected)/(basic)/admins/_components/admin-member-create-modal.tsx
@@ -26,6 +26,7 @@
   ADMIN_LOGIN_ID_HELP_TEXT,
   ADMIN_LOGIN_ID_MESSAGES,
   ADMIN_LOGIN_ID_VALIDATORS,
+  ADMIN_PASSWORD_ERROR_TEXT,
   ADMIN_PASSWORD_HELP_TEXT,
   ADMIN_PASSWORD_POLICY,
   INITIAL_ADMIN_MEMBER_FORM_STATE,
@@ -124,17 +125,19 @@
         )
       : undefined);
 
-  // 중복 확인 결과는 서버 검증 오류보다 먼저 보여준다 — 방금 누른 버튼의 답이기 때문이다.
+  // 중복 확인 결과는 화면 규칙보다 먼저 보여준다 — 방금 누른 버튼의 답이기 때문이다.
   // 값이 나쁜 것(unavailable)과 확인을 못 한 것(failed)은 사유가 다르므로 둘 다 문구로 낸다.
   const loginIdFailure =
     checkResult.status === 'unavailable' || checkResult.status === 'failed'
       ? checkResult.message
       : undefined;
+  // **서버 오류는 성공 문구를 이긴다** — 확인을 통과한 뒤에도 저장이 ID 때문에 거부될 수 있는데
+  // (확인과 저장 사이의 선점, 형식 규칙 변경), '사용할 수 있는 ID입니다.'가 계속 떠 있으면
+  // 거부된 사실이 화면에서 사라진다.
   const loginIdMessage =
     loginIdFailure ??
-    (isLoginIdVerified
-      ? '사용할 수 있는 ID입니다.'
-      : (errors.loginId ?? ADMIN_LOGIN_ID_HELP_TEXT));
+    errors.loginId ??
+    (isLoginIdVerified ? '사용할 수 있는 ID입니다.' : ADMIN_LOGIN_ID_HELP_TEXT);
 
   return (
     <FoxModal
@@ -221,7 +224,7 @@
           invalid={Boolean(errors.password)}
           // 규칙은 도메인이 한 벌로 갖는다 — 저장 직전 서버 검증이 같은 값을 본다.
           validators={[foxPasswordValidator({ ...ADMIN_PASSWORD_POLICY, required: true })]}
-          validationMessages={{ minlength: ADMIN_PASSWORD_HELP_TEXT }}
+          validationMessages={{ minlength: ADMIN_PASSWORD_ERROR_TEXT }}
         />
 
         <AdminMemberFormFields
app/(protected)/(basic)/admins/_components/admin-member-edit-modal.tsx
--- app/(protected)/(basic)/admins/_components/admin-member-edit-modal.tsx
+++ app/(protected)/(basic)/admins/_components/admin-member-edit-modal.tsx
@@ -13,6 +13,7 @@
 import { DEFAULT_ADMIN_ROLE_CODE, type AdminMember } from '@/lib/domain/admin-member';
 import {
   ADMIN_EMAIL_MESSAGES,
+  ADMIN_PASSWORD_ERROR_TEXT,
   ADMIN_PASSWORD_HELP_TEXT,
   ADMIN_PASSWORD_POLICY,
   INITIAL_ADMIN_MEMBER_FORM_STATE,
@@ -139,7 +140,7 @@
             invalid={Boolean(errors.password)}
             // 비우면 "바꾸지 않음"이라 required를 걸지 않는다(등록 팝업과 다른 점).
             validators={[foxPasswordValidator(ADMIN_PASSWORD_POLICY)]}
-            validationMessages={{ minlength: ADMIN_PASSWORD_HELP_TEXT }}
+            validationMessages={{ minlength: ADMIN_PASSWORD_ERROR_TEXT }}
           />
           <FoxButton
             type="secondary"
lib/domain/admin-member-form.ts
--- lib/domain/admin-member-form.ts
+++ lib/domain/admin-member-form.ts
@@ -34,7 +34,14 @@
 export const ADMIN_LOGIN_ID_HELP_TEXT =
   '영어 소문자, 숫자를 조합하여 입력 후 중복여부를 확인하세요.';
 export const ADMIN_PASSWORD_HELP_TEXT =
-  '영어 소문자, 숫자, 특수문자 중 2종류 이상 조합, 최소 10자리 이상';
+  '영어, 숫자, 특수문자 중 2종류 이상 조합, 최소 10자리 이상';
+
+/**
+ * 비밀번호가 규칙에 걸렸을 때의 문구. **안내 문구와 글자가 달라야 한다** — 같으면 저장이
+ * 거부돼도 화면의 글자가 하나도 바뀌지 않아 사용자가 실패한 줄 모른다(실제로 그랬다).
+ */
+export const ADMIN_PASSWORD_ERROR_TEXT =
+  '비밀번호가 규칙에 맞지 않습니다. 영어·숫자·특수문자 중 2종류 이상으로 10자리 이상 입력해 주세요.';
 
 
 const LOGIN_ID_MIN_LENGTH = 4;
@@ -141,11 +148,15 @@
 };
 
 /**
- * 비밀번호 조합 종류 수 — 영문 소문자 / 숫자 / 특수문자 중 몇 종류가 섞였는지 센다.
- * 시안이 "2종류 이상"만 요구하므로 대문자는 별도 종류로 세지 않는다(있어도 무방하다).
+ * 비밀번호 조합 종류 수 — 영문 / 숫자 / 특수문자 중 몇 종류가 섞였는지 센다.
+ *
+ * **대문자도 영문 한 종류로 센다** — 화면이 쓰는 `@fox`의 `characterKinds`가 `/[a-zA-Z]/`로
+ * 판정하므로 여기가 `[a-z]`면 규칙이 갈린다. 실제로 `PASSWORD123`처럼 영문자가 전부 대문자인
+ * 값이 화면은 통과하고 저장만 거부되는 상태였다(오류 문구가 안내 문구와 같아 화면에 아무것도
+ * 드러나지 않았다).
  */
 function countCharacterKinds(password: string): number {
-  const kinds = [/[a-z]/, /\d/, /[^a-zA-Z0-9]/];
+  const kinds = [/[a-zA-Z]/, /\d/, /[^a-zA-Z0-9]/];
   return kinds.filter((pattern) => pattern.test(password)).length;
 }
 
@@ -197,13 +208,13 @@
 
   if (!password) {
     if (options.passwordRequired) {
-      errors.password = ADMIN_PASSWORD_HELP_TEXT;
+      errors.password = ADMIN_PASSWORD_ERROR_TEXT;
     }
   } else if (
     password.length < ADMIN_PASSWORD_POLICY.minLength ||
     countCharacterKinds(password) < ADMIN_PASSWORD_POLICY.kinds
   ) {
-    errors.password = ADMIN_PASSWORD_HELP_TEXT;
+    errors.password = ADMIN_PASSWORD_ERROR_TEXT;
   }
 
   if (!phoneNumber) {
Add a comment
List