두 변경이 등록 팝업의 같은 파일에 겹쳐 한 커밋으로 둔다.
## 중복 확인 실패 사유
확인 호출이 백엔드에서 깨지면 `isAdminLoginIdTaken`이 던진 예외가 클라이언트 transition의
처리되지 않은 rejection으로 사라졌다 — 버튼만 원복되고 아무 말도 남지 않아 "눌러도 아무 일이
없는" 상태로 보였다. 확인 결과에 `failed`를 더해 **값의 판정(unavailable)과 확인 실패(failed)를
구분**하고, 백엔드 메시지를 ID 칸 아래 같은 자리에 그대로 띄운다.
## @fox/core/validation
Angular `Validators`의 계약을 옮겼다 — 검증기는 값을 받아 통과면 `null`, 아니면 **오류 객체**를
돌려주는 순수 함수이고, 키가 오류 이름, 값이 문구를 만들 맥락이다
(`{ minlength: { requiredLength, actualLength } }`). 인자가 `AbstractControl`이 아니라 문자열인
것만 다르다 — @fox에는 폼 모델이 없고 검증 대상이 칸의 값 하나뿐이다.
`required`를 뺀 검증기는 빈 값을 통과시키고(Angular와 같다), `compose`는 오류를 **병합**해
"10자 이상"과 "2종류 이상"을 함께 판정할 수 있게 한다.
**문구는 검증기가 갖지 않는다**(Angular와 같은 이유 — 같은 규칙도 자리마다 다른 말이 필요하다).
`foxValidationMessage(errors, overrides)`가 옮기고, 한 번에 한 문구만 낸다.
비밀번호처럼 규칙이 여러 개인 칸을 위해 `foxPasswordValidator(policy)`를 둔다 — 길이·조합에
더해 같은 문자 연속·잇따르는 문자·특정 문자열 포함 금지를 옵션으로 받는다.
## FoxInput 연결
`validators` · `validationMessages` · `validateOn` · `onValidationChange`.
- `message`는 평소 헬퍼이고 검증이 걸리면 그 자리에 사유가 들어간다. 호출부가 `invalid`를 직접
켜면 `message`가 이긴다 — 서버가 준 사유(중복·권한)는 화면 규칙이 알 수 없어 덮이면 안 된다.
- 오류는 **한 번 포커스를 벗어난 뒤부터** 보이고 그 뒤로는 입력할 때마다 갱신된다
(Angular의 `touched`. `updateOn` 기본값만 다르다 — 한 글자에 "10자 이상"이 뜨면 방해가 된다).
## 사용
비밀번호 규칙을 `ADMIN_PASSWORD_POLICY` 한 곳으로 모아 **화면과 Server Action이 같은 값을 본다**
— 한쪽만 고치면 화면은 통과시키고 저장은 거부하는 상태가 된다. 등록은 `required: true`,
수정은 비우면 "바꾸지 않음"이라 걸지 않는다.
Co-Authored-By: Claude Opus 5
## 백엔드 최신화 반영 (edupay-backend develop 4d98756)
- **수정이 동작하지 않던 것을 고쳤다.** `PUT /mngr/admin`에 form으로 보내고 있었으나 백엔드는
`PUT /mngr/admin/{admUserId}` + `@RequestBody`(JSON)다. 등록만 `@ParameterObject`(form)라
같은 도메인인데 본문 형식이 갈린다 — 두 함수 주석에 그 이유를 적어 두었다.
- 삭제를 mock 오버레이에서 실 API(`DELETE /{admUserId}`, soft delete)로 옮기고
`lib/data/mock/admin-member-store.ts`를 지웠다.
- 엑셀 다운로드 API가 생겨 `/admins/excel` 라우트 핸들러로 중계하고 버튼을 열었다.
- 목록 응답에 `admTelNo`·`admEmlAddr`·`frstRegDtStr`이 추가돼 생성일 매핑을 넣었다 —
휴대전화번호·이메일·생성일 세 열의 `-`가 실값으로 바뀐다.
- 검색 분기가 `USER_TELNO`→`ADM_TEL_NO`로 고쳐져 휴대전화번호 검색을 되살렸다(시안 ①의 셋).
- 수정 시 비밀번호 필수 제약을 없앴다 — UPDATE의 `LOGIN_PW`가 빈 값이면 SET 절에서 빠지도록
바뀌어 "바꾸지 않음"을 표현할 수 있게 됐다.
- `totalCount`는 여전히 `list.size()`라 전체를 받아 서버에서 세는 방식은 유지한다.
## 등록·수정 팝업 (시안 5227:2035 / 5227:3351)
@fox로 다시 그렸다 — modal·input·btn·form-label·form-phonenumber·select·chip-area·toggle이
Figma 인스턴스 이름과 그대로 대응한다. 확인한 값: 모달 400, 입력 40, 칩 28, 푸터 버튼 80×40,
필드 간격 32.
저장 버튼은 모달 actions 슬롯이라 폼 밖에 그려진다. FoxButton에 네이티브 `form` 속성이 없어
폼을 ref로 잡아 `requestSubmit()`을 부른다. @fox 제어 위젯(전화번호·역할·메뉴)은 값이
FormData에 실리지 않아 hidden input으로 함께 낸다.
이메일은 두 시안이 다르다 — 등록은 `[아이디] @ [도메인]`, 수정은 단일 입력이라 그대로 따랐다.
등록 쪽은 `FoxEmail`을 쓰지 않는다: 잠긴 '직접입력' 상자를 하나 더 그리고 칸마다 240px 최소폭이
박혀 320px 안에서 세 줄로 쌓인다(사용자 확정).
메뉴 선택은 시안대로 그리되 **필수 검증을 걸지 않는다**(사용자 확정) — 백엔드에 관리자별 메뉴
권한이 테이블·VO·SQL 어디에도 없어 고른 값이 저장되지 않는다.
삭제 확인 얼럿의 버튼도 무스타일 `components/ui/button`에서 FoxButton으로 옮겼다.
Co-Authored-By: Claude Opus 5
시안 ADM_ADM_101(5227:1545)을 학생 회원 목록과 같은 구성으로 옮겼다. 화면 뼈대는
FoxListContainer가 전부 그리고, 이 화면이 갖는 것은 8개 열 정의·정렬 셀렉트·툴바 버튼
두 개뿐이다. 종전의 표/툴바/검색바 세 파일은 이 목록 하나로 합쳐 지웠다 — 셋 다 Tailwind
제거 이후 무스타일로 남아 있던 조각이라 시안과 무관하게 그려지고 있었다.
시안 값과 대조해 확인한 것: 역할 배지 색(#fbeff0·#ab2b36 / #ecf2fe·#0b50d0), 아이콘 버튼
28×28·radius 4·테두리 #b1b8be, 버튼 간격 4, 검색 상자 320×40, 열 폭 8개 전부.
이름·아이디·이메일은 시안에서 남는 폭을 균등하게 나눠 갖는다. 폭을 비워 두면 표가 글자
길이대로 나눠 이메일 열만 넓어지므로 실측값(277)을 넣었다.
[엑셀 다운로드]는 자리만 잡고 비활성이다(사용자 확정) — 백엔드에 관리자 엑셀 API가 없어
학생 목록처럼 중계할 대상이 없다.
Co-Authored-By: Claude Opus 5
전환 중 뜨는 표시가 Tailwind 클래스(animate-spin·border-t-transparent·size-8)로만
그려져 있었는데, Tailwind가 제거된 뒤로 그 클래스들이 정의되지 않아 아무것도 보이지
않는 상태였다. 감싸던 배치 클래스(flex·flex-1·items-center)도 마찬가지였다.
- components/ui/spinner.tsx가 FoxSpinner를 그리도록 바꿨다. size·tone 축은 그대로 둬서
호출부 다섯 곳을 건드리지 않는다(loading.tsx 넷 + 전역 오버레이 하나).
- 계열 색은 CSS 모듈로 옮겼다. FoxSpinner 기본이 테마색이라 inverse·muted만 덮는다.
- 화면 넷의 loading.tsx가 같은 내용이라 PageLoading 공용 컴포넌트로 모으고, 죽어 있던
배치를 CSS 모듈로 되살렸다.
확인한 것: 두 CSS 모듈이 토큰 var()로 컴파일되고, FoxSpinner가 32px에서 시안대로
그려지며, tsc·eslint가 통과한다.
확인하지 못한 것: 전환 화면 자체는 로그인이 필요한 라우트라 눈으로 보지 못했다.
Co-Authored-By: Claude Opus 5
아이콘이 없어 텍스트로 대체돼 있던 자리를 시안대로 되돌린다. `@fox`에 Phosphor 전량이
들어왔으므로 더 이상 막힐 이유가 없다.
- students: 돋보기(FoxMagnifyingGlassIcon) — "조회" 텍스트 버튼을 대체
- admins · boards · decoration-items: 연필(FoxPencilSimpleIcon)·휴지통(FoxTrashIcon)
- 테마 토글: 손으로 그린 monitor/moon/sun 3종을 @fox 아이콘으로 교체하고
components/ui/icons/ 디렉터리를 제거했다(다른 사용처 없음)
버튼을 `components/ui/button`이 아니라 `FoxIconButton`으로 쓴 이유 — @fox 아이콘은 스스로
크기를 갖지 않고 감싼 슬롯이 정하는 규약인데, 그 슬롯 크기를 실제로 정의하는 CSS가 있는 건
@fox 쪽뿐이다. Tailwind 제거(e3d3133)가 빌드 설정만 바꾸고 components/ui 19개 파일은 그대로
둔 탓에 `size-9`·`inline-flex` 같은 클래스가 전부 무효라, 그쪽에 아이콘을 넣으면 SVG가 기본
300×150으로 그려진다.
테마 토글은 같은 이유로 실제 버그였다 — 표시 전환을 Tailwind의 `hidden theme-*:contents`에
기대고 있어서 세 아이콘이 한꺼번에 보이고 있었다. 같은 동작을 CSS 모듈로 다시 세웠고,
`aria-label` 대신 `sr-only` 텍스트로 현재 선택을 읽히는 기존 접근성 설계는 그대로 유지했다
(IconButton에 label을 넘기면 aria-label이 내용을 덮어쓴다).
검증 — 컴파일된 CSS를 주입해 `` 3상태를 실제로 전환했다. 속성 없음→system,
light→light, dark→dark로 **매번 하나만** 표시되고 20×20px(--fox-icon-4)로 그려진다.
FoxIconButton은 sm/ghost에서 28px 버튼에 24×24 아이콘이다.
남은 것 — sidebar-group.tsx의 셰브론은 손으로 그린 채로 뒀다. 회전(`rotate-90`)과
크기(`size-4`)를 Tailwind 클래스에 기대고 있어 그 둘을 실재하는 CSS로 옮기기 전에는 글리프만
바꿔도 나아지지 않는다. components/ui 전체 리스타일과 함께 처리한다.
Co-Authored-By: Claude Opus 5
백엔드(develop 924db37)에 관리자 쓰기 API가 추가되어 mock을 걷어낸다.
등록 POST /mngr/admin, 수정 PUT /mngr/admin, 중복확인과 단건 조회는 전용 GET을
쓴다. mock 저장소에는 삭제만 남는다 — 백엔드에 삭제 엔드포인트가 없고, 수정
API로도 USE_YN을 끌 수 없어 비활성화로 대신할 수 없다.
수정 시 비밀번호를 필수로 막는다. 백엔드 UPDATE가 LOGIN_PW를 조건 없이
덮어써서 빈 값을 보내면 그 계정이 로그인 불가가 되기 때문이다.
메뉴 선택은 시안대로 유지하되 전송하지 않는다 — 관리자별 메뉴 권한 개념이
백엔드에 없다.
Co-Authored-By: Claude Opus 5
admins/_actions.ts('use server')가 AdminMemberFormState 타입의 초깃값
INITIAL_ADMIN_MEMBER_FORM_STATE를 값으로 export하고 있어, 관리자 등록/수정/
삭제/ID 중복확인 Server Action을 호출할 때마다 "A 'use server' file can
only export async functions, found object" 런타임 에러로 500이 발생했다.
빌드·타입체크는 통과하고 실제로 폼을 제출해야만 드러나는 문제였다.
decoration-items가 이미 적용해 둔 동일 패턴을 따라 AdminMemberFormState
타입과 INITIAL_ADMIN_MEMBER_FORM_STATE 상수를 'use server'가 없는
lib/domain/admin-member-form.ts로 옮기고, _actions.ts는 타입만 다시
import하도록 정리했다. 두 소비처(admin-member-create-modal.tsx,
admin-member-edit-modal.tsx)의 import 경로도 함께 갱신했다.
Co-Authored-By: Claude Opus 5
사이드바 「관리자정보관리 > 관리자 회원」(/admins) 신설.
목록은 백엔드 GET /api/v1/mngr/admin/pagination으로 조회하고,
등록·수정·삭제는 백엔드 API가 없어 메모리 mock 오버레이로 처리한다
(lib/data/mock/admin-member-store.ts — API 추가 시 Repository 세 함수만 교체).
백엔드 계약 확인에 따른 결정:
- 응답에 휴대전화번호·이메일·생성일이 없어 해당 열은 `-`로 표기(열은 유지)
- totalCount가 현재 페이지 행 수라 신뢰할 수 없어, 전체를 받아 서버에서
검색·정렬·페이징한다(총건수 확정 → 번호 역순·현재페이지 표기 성립)
- searchCondition=3(휴대전화번호)이 TB_ADM_USER에 없는 USER_TELNO를
참조해 검색 대상에서 제외
- admRoleCd 코드 정의가 백엔드에 없어 잠정 매핑 + 미지의 코드는 원문 노출
엑셀다운로드는 버튼만 배치(추후 진행), 아이콘 부재로 관리 열은 텍스트 버튼.
Co-Authored-By: Claude Opus 5