두 변경이 등록 팝업의 같은 파일에 겹쳐 한 커밋으로 둔다.
## 중복 확인 실패 사유
확인 호출이 백엔드에서 깨지면 `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
토글은 백엔드에 사용여부 값도 변경 API도 없어 `disabled`로 막아 두었었다. 화면에서는 움직이게
해 달라는 결정에 따라 잠금을 풀었다(사용자 확정).
- 토글 상태를 이 컴포넌트가 갖는다. 종전에는 라벨을 prop에서 계산해(`member.isActive ? …`)
잠금을 풀어도 스위치만 움직이고 글자는 그대로였을 것이다 — 함께 고쳤다.
- 저장 버튼의 잠금도 풀었다. 다만 Server Action은 여전히 미지원을 알리므로, 그 사유를
모달 foot 왼쪽(utility 슬롯)에 `role="alert"`로 띄운다. 종전에는 title 속성에만 있어
눈에 띄지 않았다.
- 성공 시 닫는 useEffect를 지웠다. 지금 이 액션은 성공을 돌려주지 않는다 — 변경 API가 생겨
성공 경로가 살아나면 그때 함께 되살린다.
한계 — 토글 값은 화면에만 머문다. 저장되지 않고 목록에도 반영되지 않으며 팝업을 닫으면
사라진다. 실제 저장이 필요하면 백엔드 API가 생기거나 mock 오버레이(lib/data/mock)를 두어야
한다.
검증 — 비활성↔활성으로 체크와 라벨이 함께 바뀌고(트랙 #256ef4), 저장 버튼이 활성이다.
오류 문구는 세션이 필요한 Server Action 응답이라 프리뷰에서는 경로만 확인했다.
Co-Authored-By: Claude Opus 5
FoxModal의 `contents`가 `align-items: flex-start`인 flex 열이라, 그 안의 블록 요소가 내용
너비로 줄어들었다. 조회 목록은 줄마다 아래에 구분선을 긋기 때문에 폭이 줄면 선이 중간에서
끊긴 것처럼 보인다. 실측으로 contents 318에 폼과 목록이 169였다.
- FoxDescriptionList에 `align-self: stretch`를 줬다. flex 부모가 `flex-start`여도 스스로
가로를 채운다 — 컴포넌트가 어디에 놓이든 폭을 보장한다.
- 학생 조회 팝업의 폼 래퍼에도 폭을 줬다. 목록의 부모가 이 폼이라, 폼이 줄면 그 안에서
100%를 잡아도 소용이 없다.
검증 — 폼·목록·8줄 전부 318로 contents와 같아졌다(종전 169).
Co-Authored-By: Claude Opus 5
시안(관리자페이지 5199:9014)을 다시 대조해 고쳤다. 1·2·5는 내가 잘못 읽은 것이고, 3·4는
@fox에 기능이 없어 추가했다(사용자 확정).
1. 이름 열의 bold를 뺐다. 시안 셀 서체가 `Pretendard:Regular`인데 `emphasis`를 넣었다.
2. 역할을 배지로 바꿨다. 시안 셀에 `type=pastel, color=primary, size=md` 배지가 들어 있는데
평문으로 그렸다.
3. 관리 열을 없애고 행을 눌러 상세를 열게 했다. FoxTableRow에 `onSelect`,
FoxListContainer에 `onRowSelect`를 추가했다. ``에 `role="button"`은 씌우지 않는다 —
보조기술에서 행이 표의 일부가 아니게 되어 열 이름 읽기가 끊긴다. 초점만 받게 하고
Enter·Space를 직접 듣는다. 모달 상태는 student-list가 갖고, 쓰임이 사라진
student-row-actions는 지웠다.
4. 정렬 셀렉트 아이콘을 ArrowsDownUp으로 바꿨다. 시안은 정렬과 페이지 크기가 같은 select-txt를
쓰면서 아이콘만 다른데(노드 293:138376 vs 293:139748), FoxSelectText가 캐럿을 하드코딩해
교체할 수 없었다. `icon` prop을 넣고 기본값은 캐럿 그대로 뒀다.
5. 엑셀 버튼을 secondary + DownloadSimple 아이콘으로 바꿨다. 시안 글자색이
`button/secondary-font`(#0b50d0)이고 20px 아이콘이 붙어 있다.
검증 — 이름 굵기 400, 역할 `fox-badge--primary --pastel --md`, 열 12개(마지막 가입일)에
행이 clickable·tabIndex 0이고 클릭 시 모달이 열린다, 정렬 아이콘이 페이지 크기 아이콘과 다르다,
엑셀 버튼이 `fox-button--secondary`에 아이콘을 갖는다.
Co-Authored-By: Claude Opus 5
시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) container 5199:9014.
@fox에 없는 조각은 없었다 — 지난 작업의 FoxListContainer가 머리말·도구 줄·표·페이지네이션을
한 번에 덮고, 정렬 셀렉트는 FoxSelectText, 검색은 FoxListSearch, 버튼은 FoxButton이다.
데이터도 이미 맞물려 있었다: StudentMember에 시안의 13열이 전부 있고 StudentMemberQuery에
검색필드·정렬·페이지크기·buildStudentMemberHref가 있다.
- students/_components/student-list.tsx 신설. 이 화면 고유의 것만 갖는다 — 13열 정의,
정렬·페이지 크기 셀렉트, 엑셀 다운로드, URL 갱신 방법.
- page.tsx는 조회만 하고 넘긴다(Server Component 유지).
- student-search-bar · student-list-toolbar · student-table 3종을 지웠다. 셋이 하던 일을
FoxListContainer가 대신한다.
이동은 두 갈래다. 페이지네이션은 `buildHref`로 링크가 되고(주소가 곧 상태), 검색·정렬·페이지
크기는 `router.replace`로 같은 주소 규칙을 태운다. 조건이 바뀌면 늘 1페이지로 되돌린다.
사용자 확정 2건
- 열 폭 합계가 1880px로 컨테이너(1552)보다 넓어 FoxListContainer에 가로 스크롤 래퍼를 넣었다.
좁은 화면에서는 목록표가 카드로 바뀌므로(FoxTable의 mobile 규칙) PC에서만 건다.
- 관리 열을 "학생 회원 조회" 텍스트 버튼으로 바꿨다. 앞서 기획 시안을 근거로 돋보기 아이콘을
넣었으나 이 Figma 시안이 더 최신이다. 누르면 상세 팝업이 열리는 동작은 그대로다.
지킨 것 — 순번은 저장된 값이 아니라 현재 페이지 기준 표시 순번((page-1)*pageSize+index+1)으로
종전 표와 같은 규칙이다. 엑셀은 네이티브 GET 폼이라야 첨부파일 응답을 브라우저가 받는다.
검증 — 실측이 시안과 일치한다. 제목·총 123건·정렬 셀렉트("가입일순"·"10개씩 보기")·검색 대상
"회원명"·엑셀 버튼, 13열 이름 전부, 관리 버튼 "학생 회원 조회", 표 1880/래퍼 1552로 가로
스크롤, 페이지 링크 `/students?page=2`, 모바일용 셀 라벨은 PC에서 display:none.
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 컴포넌트는 각자 `box-sizing: border-box`를
선언하지만 앱 쪽에는 기준이 없어 모듈마다 따로 챙겨야 했고, 빠뜨리면 `inline-size: 100%`에
패딩이 더해져 자식이 부모보다 넓어졌다.
globals.scss에 리셋을 넣고, 직전 커밋에서 사이드바 모듈에 넣었던 범위 한정 리셋은 지웠다 —
같은 일을 두 곳에서 하지 않는다.
@fox는 이미 스스로 선언하고 있어 영향이 없다. 검증도 그렇게 나왔다 — 헤더 64/pill 44/아이콘
버튼 40, 사이드바 240·216·216·216·200(3개), 본문 x240·여백 24, 로그인 카드 440·입력 56×360·
버튼 360×56·토글 세로중앙차 0으로 전부 종전과 같다.
Co-Authored-By: Claude Opus 5
원인이 둘이었다.
1. `box-sizing`이 기본값 `content-box`였다. 앱에 전역 리셋이 없고(@fox 컴포넌트는 각자
선언한다) 이 모듈은 `.sidebar`에만 줘서, `inline-size: 100%`에 패딩·테두리가 더해졌다.
2depth 판이 233, 그 안의 행이 231이 됐다(시안 216/200). 이 파일이 그리는 조각 전체에
`border-box`를 적용했다.
2. 테두리가 폭을 먹었다. Figma stroke는 안쪽에 그려지지만 CSS `border`는 박스를 줄인다.
사이드바 오른쪽 구분선 때문에 안쪽 폭이 216에서 215로(사용자가 본 값), 2depth 판의
테두리 때문에 행이 200에서 198로 줄었다. 둘 다 레이아웃에 관여하지 않는 inset 그림자로
같은 선을 그렸다 — 헤더 프로필 pill에서 쓴 것과 같은 처리다.
검증 — 실측이 시안과 일치한다. 사이드바 240, 내부 리스트·1depth 행·2depth 판 216,
2depth 행 200(3개 모두), 즐겨찾기 행 216. 흰 판과 구분선은 그대로 보인다.
Co-Authored-By: Claude Opus 5
버튼·입력칸이 Pretendard가 아니라 Arial 13.33px으로 그려지고 있었다. 폼 조작기는 CSS
명세상 `font`를 상속받지 않는데 그걸 되돌리는 규칙이 없어, 아무도 서체를 지정하지 않은
자리는 브라우저 기본값이 이겼다. 그래서 제목·본문은 Pretendard인데 그 옆 버튼만 다른
서체로 나와 한 화면에 두 서체가 섞였다.
app/globals.scss에 조작기 리셋을 한 번 둔다. 타입 선택자(0,0,1)라 클래스로 서체·크기를
정한 곳은 그대로 이긴다.
확인한 범위:
- @fox 컴포넌트: 글자 크기(17/15/13px)·상자 크기(360×40) 모두 그대로. 서체를 지정하지
않았던 FoxInput의 지우기 버튼만 Arial → Pretendard로 바뀐다(아이콘만 있어 보이는 변화 없음).
- 카탈로그: Arial이던 버튼 49개가 전부 Pretendard로. 크기·굵기는 유지되고 높이만 1px
줄어드는데 서체가 바뀌면서 `line-height: normal` 계산이 달라진 결과다.
- 로그인 화면: 조작기 4개 모두 Pretendard. Arial 0건.
폰트 파일 자체와 시안 대조는 이상이 없었다 — 파일은 정품 Pretendard 400/600/700/800이고,
탭 label(20px/800)의 잉크 폭이 자간 반영 시 42px로 Figma 렌더와 같다.
Co-Authored-By: Claude Opus 5
로그인 카드 안에만 두었던 `:autofill` 우회를 `_form-field.scss`의 `field` 믹스인으로 옮긴다.
자동완성이 칠하는 배경은 특정 화면 문제가 아니라 텍스트 입력 전부에 해당한다.
- `_autofilled` 믹스인 신설. 배경은 `background`로 덮이지 않아 안쪽을 가득 채우는 inset
그림자로 가리고, 글자색은 `color`가 먹지 않아 `-webkit-text-fill-color`로 맞춘다.
- 처음엔 표준(`:autofill`)과 접두사(`:-webkit-autofill`)를 따로 적었으나, Lightning CSS가
browserslist 타깃을 보고 접두사를 표준형으로 합쳐 같은 규칙이 두 번 나오기만 했다
(컴파일 결과 8건 → 정리 후 4건). 표준 하나만 남겼다.
적용 범위 — 자체 ``을 가진 fox-input · fox-text-area · fox-certified ·
fox-phone-number 넷에 직접 붙고, fox-email · fox-address · fox-file-upload는 내부에서
fox-input을 조립하므로 `.fox-input__field` 규칙으로 함께 덮인다. 즉 @fox의 모든 텍스트
입력이 커버된다.
Co-Authored-By: Claude Opus 5
FoxInput에 `state`를 넘긴 것이 원인이었다. `state`(=`data-state`)는 카탈로그처럼 상태를 눈으로
비교할 때 쓰는 **고정** 스위치라, 주는 순간 자동 규칙이 통째로 꺼진다 — form-field.scss의
자동 규칙이 `.fox-input:not([data-state])` 안에 들어 있다. 그래서 클릭해도 테두리가 굵어지지
않았고 오류 표시도 살아 있지 않았다. 오류는 `invalid`로 넘기는 것이 정식 경로이며
(`aria-invalid`가 되어 자동 규칙이 잡는다) 그렇게 고쳤다.
자동완성으로 채워진 칸은 Chrome이 자기 배경(연한 파랑)을 칠하는데 @fox에도 앱에도
`:-webkit-autofill` 처리가 없어 그대로 노출된다. 일반 CSS로는 못 덮으므로 표준 우회인 inset
그림자로 가리고 글자색은 `-webkit-text-fill-color`로 맞췄다. 우선 로그인 카드 안으로 범위를
좁혔다 — 모든 FoxInput이 같은 상태라 디자인시스템에 올릴지는 별도 판단이 필요하다.
검증 — data-state가 사라졌고, 포커스 시 테두리가 1px에서 2px #256ef4로, aria-invalid 시
2px #de3412로 바뀐다. autofill 우회 규칙이 스타일시트에 실린 것도 확인했다.
Co-Authored-By: Claude Opus 5
시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) login 5435:14598.
시안의 입력·라벨·버튼이 디자인시스템 컴포넌트 인스턴스라 @fox로 거의 그대로 덮인다 — 치수가
우연이 아니라 정확히 같다. FoxInput lg가 h56·px20·radius12·17px·아이콘 24, FoxButton lg가
h56·px20·radius12·17px다. `label`을 주면 FoxInput이 FoxFormLabel까지 함께 그린다.
- login.module.scss 신설. 남는 건 화면 셸(배경·중앙 정렬)과 카드(440·padding 40·radius 24·
2겹 그림자), 비밀번호 토글 자리뿐이라 @fox에 넣지 않고 화면 전용으로 뒀다(사용자 확정).
- 비밀번호 보기 토글은 로그인 전용이다(사용자 확정). FoxInput에 그 기능이 없고, `icon` 슬롯은
`aria-hidden="true"`라 버튼을 넣으면 보조기술에서 사라진다. 그래서 슬롯 대신 같은 자리에
버튼을 겹쳐 두고 aria-pressed·aria-controls로 상태와 대상을 알린다.
- 오류 문구는 FoxInput의 `message`가 아니라 시안의 안내문 자리에 함께 둔다. 어느 칸이 틀렸는지
알리지 않는 편이 맞고(계정 존재 여부가 새지 않는다), 입력칸 아래 메시지가 끼면 토글의 세로
위치도 어긋난다 — 토글을 아래에서 띄우므로 입력칸이 래퍼의 마지막 요소여야 한다.
- components/ui/auth-card를 더 이상 쓰지 않는다. 시안에 로고가 없고 그쪽은 Tailwind 제거 이후
무스타일이다(헤더·풋터와 같은 처리).
검증 — 실측이 시안과 일치한다. 배경 #f4f5f6, 카드 440/40/24/gap16 + 그림자 2겹
(1·1·4 / 4·4·8, alpha-black10), 제목 32px 800 #1e2124, 라벨 15px 600, 입력 56/12/20/#58616a,
버튼 56/12/#256ef4/17px, 토글 24×24가 입력칸 세로 정중앙(차이 0px)에 오른쪽 여백 20px.
토글은 password↔text 전환과 aria-pressed·aria-label 갱신까지 확인했다.
Co-Authored-By: Claude Opus 5
셸은 `flex min-h-screen flex-col` 같은 Tailwind 클래스로만 잡혀 있었는데 Tailwind 제거
(e3d3133) 이후 그 클래스를 정의한 CSS가 없어 **배치가 하나도 적용되지 않고 있었다**. 헤더·
사이드바·본문·풋터가 그냥 블록으로 쌓여 사이드바가 본문 옆에 서지도 않았다.
@fox에는 넣지 않았다(사용자 확정) — 로고·프로필처럼 이 앱에만 있는 구성을 전제로 한 배치라
포터블 디자인시스템에 들어갈 성격이 아니다. 대신 여백·색·서체는 전부 @fox 토큰을 거친다.
- admin-shell.module.scss 신설. 뷰포트를 채우고 그 안에서만 스크롤한다 — 헤더는 항상 보이고
사이드바와 본문이 각자 스크롤한다. 사이드바가 이미 `overflow-y: auto` 전제로 만들어져 있어
(시안 높이 1251px이 뷰포트보다 크다) 행 높이가 뷰포트에 묶여야 그 스크롤이 동작한다.
- 본문 열에 `min-inline-size: 0`을 줬다. flex 자식 기본값이 auto라 넓은 표가 들어오면 줄지 않고
사이드바를 밀어낸다.
- main 여백은 헤더의 좌우 여백과 같은 padding(8)=24px이라 세로 기준선이 맞는다.
- 풋터가 components/ui/footer를 떠나 셸이 직접 그린다 — 그쪽도 무스타일이라 그대로 쓰면
테두리도 여백도 없다. 헤더가 components/ui/header를 떠난 것과 같은 이유다.
검증 — 1440×900에서 헤더 y0/64/전폭, 사이드바 y64(시안과 같은 위치)/240/잔여 높이,
본문열 x240/1200/자체 스크롤/흰 배경, main 여백 24, 풋터 border-top 1px·여백 16·24·
13px font-neutral-subtler·가운데 정렬. 페이지 자체는 스크롤되지 않고, 본문을 끝까지 내려도
사이드바 scrollTop은 0으로 유지된다(독립 스크롤).
Co-Authored-By: Claude Opus 5
시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) logo 5199:9639.
헤더 구현 때는 28px 정사각 심볼 자산이 없어 170×62 워드마크로 대신했는데, 이번 시안이 그
심볼을 제공한다. export SVG를 public/images/logo-foxpay-symbol.svg로 반입했다 — Figma 자산
URL은 7일 뒤 만료되므로 원본 바이트를 저장소에 둔다.
- 글자를 "에듀페이 관리자" 한 덩어리에서 시안의 2색 구성으로 나눴다. 제품명 "FOX PAY"는
ExtraBold + font-neutral-strong, 역할 "ADMIN"은 SemiBold + font-neutral-subtler다. 시안이
리터럴로 적은 #1E2124·#8A949E가 각각 그 토큰의 라이트 값과 같다.
- 심볼은 가로세로를 둘 다 적었다. ``의 width/height 속성이 만드는 1:1 기본 비율 때문에
한쪽만 적으면 28.12가 28로 눌린다.
- 심볼 alt는 비웠다 — 바로 옆 글자가 같은 내용을 이미 말하므로 채우면 두 번 읽힌다.
검증 — 실측이 시안과 일치한다. 심볼 28.12×28, gap 8, "FOX PAY" #1E2124/800/24px,
"ADMIN" #8A949E/600/24px, 헤더 높이 64 유지.
기존 logo-foxpay.svg는 그대로 둔다 — auth-card와 components/ui/header가 아직 쓴다.
Co-Authored-By: Claude Opus 5
시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F) snb-area 3041:11860.
메뉴는 화면이 소유하지 않는다. 계정 권한마다 항목이 달라 매 요청 백엔드가 구성해 주는 값이라,
라우트 목록을 컴포넌트 상수로 두던 구조를 걷어내고 서버에서 조회해 내려주는 흐름으로 바꿨다.
그래서 새 화면이 생겨도 사이드바 컴포넌트는 손대지 않는다.
- lib/domain/sidebar-menu.ts: section > 1depth > 2depth > 3depth + 즐겨찾기 타입. 아이콘은
컴포넌트가 아니라 이름 문자열로 받는다 — 백엔드가 넘길 수 있는 형태가 그것뿐이다.
- lib/data/repositories/sidebar-menu-repository.ts: 조회 지점. 아직 API가 없어 mock을 돌려주되
권한(roleCode)을 인자로 받아 `cache()`가 권한별로 따로 기억하게 했다 — API가 생기면 이 함수
본문만 교체하면 호출부는 그대로다.
- layout.tsx가 세션 권한으로 메뉴를 조회해 AdminShell을 거쳐 사이드바에 넘긴다. 클라이언트에서
불러오면 첫 페인트에 메뉴가 비었다가 채워지고 조회용 토큰도 브라우저로 내려가야 한다.
- sidebar-menu-icon.tsx: 이름 → @fox 아이콘 화이트리스트. 동적 import로 하면 번들러가 1,512종을
전부 싣게 되므로 적힌 것만 번들에 넣는다. 모르는 이름이 와도 기본 아이콘으로 그려진다.
- use-sidebar-nav.ts: 3단 트리용으로 다시 썼다. 펼침은 "현재 경로에서 파생된 기본값 + 사용자가
누른 값" 2층이라, 이동하면 그 가지가 저절로 열리고 직접 접으면 그 선택이 유지된다.
@fox 사용 범위(사용자 확정) — 1·2·3depth 항목은 @fox에 없어 사이드바 전용으로 짜되 토큰·아이콘은
전부 @fox를 쓰고, 있는 것은 그대로 재사용했다.
- 토글 버튼: FoxIconButton xsm/ghost가 24×24·radius 4·아이콘 20으로 시안과 정확히 일치한다.
- 검색창: FoxInput md가 크기·radius·여백까지 시안과 같고 테두리 색만 달라(input-border는
neutral-60, 시안은 border-neutral-subtler) 그 한 줄만 덮었다.
mock 메뉴는 실제로 존재하는 화면만 담았다 — 시안의 통합플랫폼 메뉴(기관 관리·역할 관리 등)를
그대로 넣으면 전부 갈 곳 없는 죽은 링크가 된다.
검증 — 브라우저 실측이 시안과 일치한다. 셸 240px·패딩 12·우측 1px, 배경 그라데이션
#f4f5f6 70% → #ecf2fe, 검색창 40/radius 8/테두리 #e6e8ea, 토글 24×24/radius 4/아이콘 20,
1depth 40/패딩 10·8/radius 8/14px ExtraBold/mix-blend multiply/아이콘 20, 활성 배경 #063a74 +
흰 글자, 2depth 판 흰 배경+1px+radius 8+패딩 8, 2depth 행 28, 별·캐럿 12(자리는 유지하고
opacity 0으로 감춤 — 시안과 같다).
AdminShell이 더 이상 useSidebar를 쓰지 않는다. 헤더 구현 때 시안에 없어 걷어낸 토글의 후속이다.
Co-Authored-By: Claude Opus 5
시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F) header 7028:7614.
시안에서 디자인시스템 컴포넌트로 정의된 조각은 아이콘 4종과 badge-dot뿐이다(나머지는 header
안의 로컬 프레임). 그 구분을 그대로 따라 badge-dot은 @fox의 FoxBadgeDot을 쓰고, 로고 영역·
아이콘 버튼·프로필 pill은 헤더 전용으로 조립했다. 색·간격·radius·타이포는 전부 @fox 토큰을
거치므로 없는 이름을 쓰면 빌드가 실패한다.
- admin-header.tsx/.module.scss 신설. 좌측 로고, 우측 아이콘 버튼 3종 + 프로필 pill.
- header-icon-button.tsx/.module.scss 신설. FoxIconButton 위에 시안 값 두 개(radius 4px,
아이콘 24px)만 덮는다 — md는 radius 8px, ghost는 아이콘 32px이라 어느 조합도 시안과 맞지
않는데, 이 조합은 btn-ico 시안에 없어 @fox에 새 계열을 만들 근거가 없다. 명시도가 같아
로드 순서에 좌우되지 않도록 `:global()`을 겹쳐 이기게 했다.
- 관리자 이름을 세션에서 읽어 프로필에 표시한다. layout이 `getSessionAdmin()`으로 읽어
AdminShell을 거쳐 넘긴다 — 인증 판단이 아니라 표시용이라 redirect하는 verifySession()은
쓰지 않았다.
- admin-header에 'use client'를 붙였다. 상태를 갖게 됐으므로 부모 경계에 기대지 않는다.
사용자 확정 사항 4건
- 로고: 시안은 28px 정사각 심볼이지만 에듀페이 로고가 170×62 워드마크라 높이만 28px로 맞추고
옆에 "에듀페이 관리자"를 둔다.
- 사이드바 토글 버튼 제거(시안에 없음). AdminShell이 더 이상 openMobile/toggleDesktopCollapse를
넘기지 않으므로 모바일에서 사이드바를 열 수단이 없다.
- 로그아웃은 시안에 자리가 없고 다른 경로도 없어서 프로필 pill의 메뉴로 옮겼다. CaretDown이
이미 "열린다"고 알리는 자리다. 메뉴 판때기는 시안에 없어 FoxSelectOption과 같은 토큰 조합으로
맞췄다 — 이 부분만 시안 근거가 없다.
- 아바타 안은 관리자 사진을 받는 경로가 없어 FoxUserIcon(fill)으로 채웠다. 크기는 시안 20px 유지.
검증 — 브라우저 실측이 시안과 일치한다. 헤더 64 · pill 44 · 아바타 28(내부 20) · 캐럿 16 ·
아이콘 버튼 40/radius 4/아이콘 24/테두리 0 · 배지 6×6 #256EF4 · 이름 15px 600.
높이 두 곳은 Figma stroke가 안쪽에 그려지는 걸 반영했다 — 헤더는 box-sizing: border-box로,
pill은 레이아웃에 관여하지 않는 inset 그림자로 선을 그려 각각 64·44를 맞췄다(`border`면 65·46).
메뉴는 열림/Esc/바깥 클릭 닫힘과 aria-expanded 전환을 확인했다.
Co-Authored-By: Claude Opus 5
시안(Figma 디자인시스템)이 Pretendard를 지정하는데 앱 셸만 WantedSans를 쓰고 있어 한 화면에
두 서체가 섞여 있었다. 앱도 Pretendard로 맞춘다.
- layout.tsx: `next/font/local`의 wantedSans 선언을 걷어냈다. 앱 기본 서체는 이제 globals.scss의
`@font-face`가 공급한다 — @fox 토큰이 패밀리명을 리터럴 `Pretendard`로 못 박고 있어
next/font(패밀리명을 해시로 바꾼다)를 쓸 수 없기 때문이고, 같은 서체를 두 경로로 넣을 이유도
없다.
- globals.scss: `--app-font-sans: "Pretendard"`를 @font-face 옆에 정의했다. @fox README가 정의한
호스트 앱 계약의 실제 주입 지점이라, 나중에 서체를 갈 때 이 한 줄만 고치면 된다.
- weight 700(Bold) 추가. 토큰이 쓰는 400·600·800만으로는 어떤 CSS도 굵기를 지정하지 않는
자리(제목·th·strong 등 UA 기본값 700)가 800으로 올라가 굵어진다.
- public/fonts: WantedSans TTF 4종(9.4MB) 삭제. 참조가 layout.tsx 한 곳뿐이었고 그게 사라졌다.
검증 — `--app-font-sans`가 `"Pretendard"`, body 계산값이 `Pretendard, system-ui, sans-serif`,
document.fonts에 wantedSans가 아예 없다. 굵기별 문자열 폭이 400/600/700/800에서 각각
328.2 / 335.2 / 338.7 / 342.2px로 전부 달라, 700이 800으로 승격되지 않고 실제 Bold 페이스로
잡히는 것까지 확인했다. FoxCarouselIndicator는 59.69px로 종전과 같다(회귀 없음).
저장소 폰트 용량 11.6MB → 3.0MB.
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