셸은 `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
@fox 토큰이 패밀리명을 리터럴 `Pretendard`로 지정하는데 프로젝트 어디에도 그 이름의 폰트
공급이 없었다. 그래서 모든 @fox 컴포넌트가 OS 기본 서체(맥 Apple SD Gothic Neo / 윈도
맑은 고딕)로 갈라져 그려졌고, 글자 폭이 시안과 어긋났다.
- public/fonts: Pretendard v1.3.9 woff2 400/600/800 반입(OFL 1.1). 토큰이 쓰는 weight가
정확히 이 셋이다 — 특히 800은 wantedSans에 없어 가짜 굵기로 합성되고 있었다.
- globals.scss: @font-face로 직접 선언. next/font/local은 패밀리명을 해시로 바꿔 토큰의
리터럴과 만나지 못해 이 용도로는 쓸 수 없다.
- globals.scss: body에 `--app-font-sans` 연결. 이 계약 변수를 읽는 곳이 없어 wantedSans가
로드만 되고 어디에도 적용되지 않았다. 이제 @fox 밖 텍스트가 wantedSans를 쓴다.
- next.config.ts: `/fonts/*`를 no-store에서 제외. public/ 자산이라 그대로 두면 페이지를
열 때마다 2.2MB를 다시 받는다.
- proxy.ts: 폰트 확장자를 matcher에서 제외. 세션 없는 방문자의 폰트 요청이 `/login`으로
리다이렉트돼 로그인 화면만 대체 서체로 그려졌다.
- @fox/README.md: "호스트 앱과의 계약"이 @fox 컴포넌트도 `--app-font-sans`를 읽는 것처럼
적혀 있었으나 사실이 아니다. 실제 요구사항(Pretendard @font-face·400/600/800)으로 교체.
FoxCarouselIndicator number형 56.92px → 59.69px. 시안 62px과의 잔여 2.31px은 Figma가 텍스트
상자 폭을 정수로 올리고(+1.57) 마지막 글자의 음수 자간을 빼지 않는(+0.75) 표기 차이다.
Co-Authored-By: Claude Opus 5
공지사항 등록 시 "A 'use server' file can only export async functions,
found object"로 500이 발생했다. boards/_actions.ts가 INITIAL_* 상태 상수를
함께 export한 것이 원인이다.
관리자 회원에서 같은 원인을 고친 방식(abb6259)에 맞춰 *FormState 타입과
INITIAL_* 상수를 lib/domain/board-post-form.ts로 옮겼다. 빌드·타입체크는
통과하고 폼을 실제로 제출해야만 드러나는 런타임 전용 제약이라, 근거를
domain 파일 주석에 남겼다.
Co-Authored-By: Claude Opus 5
충돌 2건 해결:
- lib/http/backend-fetch.ts: hub 버전 채택. hub가 이미 PUT/DELETE·form·
multipart를 갖췄고 data:null 성공을 canHaveNullData로 다루므로, 같은 목적으로
추가했던 backendCommand를 걷어내고 게시판 Repository를 그 규약에 맞췄다.
파일 업로드용 timeoutMs만 BackendRequestInit에 추가로 남겼다.
- admin-sidebar.tsx: 꾸미기아이템관리(hub)와 게시판관리(고객센터)를 함께 둔다.
Co-Authored-By: Claude Opus 5
사이드바 「게시판관리(고객센터)」 그룹과 3개 화면 신설
(/boards/notices, /boards/inquiries, /boards/faqs).
백엔드 최신화(develop 924db37)로 새로 생긴 관리자 게시판 API에 연동한다.
세 화면이 TB_COM_BBS 한 테이블을 stngId로만 구분하므로 도메인·Repository·
Server Action·공용 컴포넌트를 한 벌로 두고 BoardType으로 분기한다.
- 공지사항: 목록·등록·수정·삭제 (구분/사용여부 필터, 상단고정·노출기간·앱푸쉬)
- 1:1문의: 목록·상세답변·삭제 (등록 없음, 전화번호 마스킹, 진행상태)
- FAQ: 목록·등록·수정·삭제 (구분/유형 필터)
- 첨부파일 업로드(Server Action 경유)와 다운로드 중계 라우트 추가
백엔드 계약 확인에 따른 결정(사용자 확인 완료):
- totalCount·검색조건 한계로 전체를 받아 서버에서 검색·필터·페이징
- stngId·moduleId·구분/유형/진행상태 코드는 백엔드 미확정이라 상수 한 곳에
임시값을 두고 추후 교체(BOARD_SETTING_IDS 등)
- 유형은 저장 필드가 없어 임시 파라미터명으로 전송(백엔드 추가 예정)
- 노출기간·앱푸쉬는 조회 SELECT에 없어 전송만 하고 조회는 빈 값
- 본문은 리치 에디터 대신 글자수 카운트 textarea
backend-fetch: PUT/DELETE·폼 인코딩·멀티파트 지원 추가. 쓰기 API가
success(null)을 반환해 기존 backendFetch로는 성공이 실패로 읽히므로
데이터 없는 쓰기 전용 backendCommand를 분리했다.
Co-Authored-By: Claude Opus 5
두 레인이 같은 시기에 backendFetch에 form-urlencoded 전송을 추가해 lib/http/backend-fetch.ts가
충돌했다. 원인이 같다 — 백엔드의 쓰기 API가 @RequestBody가 아니라 @ParameterObject로 받아
JSON을 보내면 전 필드가 null로 저장된다(관리자 등록·수정, 아이템 등록·수정 모두).
아이템 쪽 구현이 관리자 쪽을 포함하므로 그것을 채택했다:
- method에 DELETE가 더 있다(아이템 삭제).
- form 값이 string 외에 number·undefined를 받는다(undefined는 전송에서 제외).
- multipart 본문을 지원한다(아이템 이미지 업로드).
- 본문·Content-Type 결정을 buildRequestBody 한 곳으로 모아 세 형식을 한눈에 볼 수 있다.
관리자 레인의 호출부는 Record을 넘기므로 넓어진 타입에 그대로 들어맞는다.
주석에는 두 도메인의 예시를 함께 남겼다.
백엔드(develop 924db37)에 관리자 쓰기 API가 추가되어 mock을 걷어낸다.
등록 POST /mngr/admin, 수정 PUT /mngr/admin, 중복확인과 단건 조회는 전용 GET을
쓴다. mock 저장소에는 삭제만 남는다 — 백엔드에 삭제 엔드포인트가 없고, 수정
API로도 USE_YN을 끌 수 없어 비활성화로 대신할 수 없다.
수정 시 비밀번호를 필수로 막는다. 백엔드 UPDATE가 LOGIN_PW를 조건 없이
덮어써서 빈 값을 보내면 그 계정이 로그인 불가가 되기 때문이다.
메뉴 선택은 시안대로 유지하되 전송하지 않는다 — 관리자별 메뉴 권한 개념이
백엔드에 없다.
Co-Authored-By: Claude Opus 5
mock 저장소를 제거하고 /api/v1/mngr/item CRUD와 파일 업로드 API에 연결한다. 기존 구현은
"이 도메인에는 백엔드 API가 전혀 없다"는 전제로 만들어져 있었으나, 백엔드에 등록·수정·삭제까지
갖춘 API가 존재해 스키마에 맞춰 도메인 타입부터 다시 맞췄다.
- 아이템ID는 사용자 입력이 아니라 백엔드 자동 채번(itemSn)이다. 시안도 "등록 후 자동발급
(Read Only)"이라 등록 폼의 입력 필드를 없애고 목록·수정 팝업에는 itemSn을 그대로 노출한다.
- 유형은 개별=N, 셋트=S로 매핑한다. 목록 조회는 searchItemType이 필수라 값이 없으면 아무것도
조회되지 않는다.
- 카테고리는 공통코드가 정비되기 전까지 프론트 임시 코드로 개발한다(사용자 지시). 표기는
백엔드가 조인해 준 이름을 우선 쓰고 없으면 임시 목록에서 찾는다.
- 이미지는 파일 하나만 받아 atchFileId에만 저장한다(썸네일은 추후 백엔드 자동생성 예정).
브라우저가 백엔드에 직접 올리지 않도록 Server Action이 File을 받아 중계한다. 목록 썸네일은
인증이 필요 없는 이미지 GET 주소를 서버에서 만들어 내려준다.
- 총건수 보정은 학생 목록과 동일하다(백엔드 PaginationUtil에 count 쿼리가 없다).
lib/http/backend-fetch.ts에 PUT/DELETE와 form·multipart 본문을 추가했다. 아이템 쓰기 API는
@RequestBody가 아니라 @ParameterObject로 받으므로 JSON을 보내면 전 필드가 null인 채 저장된다.
디자인 레이어가 React + CSS Modules 전용이던 문제를 푼다. 기존 클래스명은
`.button`·`.label`·`.icon`처럼 일반명사여서, CSS Modules 스코프 밖으로 나가면 소비
프로젝트의 동명 클래스와 전부 충돌했다. SCSS만 쓰는 프로젝트(Vue·Svelte·서버 템플릿)는
버튼을 다시 만들 수밖에 없었다.
`fox-` 접두사 + BEM(fox-button__label · fox-button--primary)으로 바꿔 이름을 전역
유일하게 만들고 CSS Modules를 걷어냈다. 충돌 방지는 도구가 아니라 접두사 규율이 담당한다.
React 컴포넌트는 이 클래스를 조립하는 얇은 래퍼가 되고, 래퍼 없이도 같은 디자인을 얻는다.
진입점을 둘 다 제공한다 — `@fox/styles/components`(전부)와 개별 파티셜
`@fox/styles/fox-button`(안 쓰는 CSS를 번들에서 뺀다). 둘을 같이 써도 Sass가 모듈을
한 번만 로드해 중복되지 않는다.
마크업 계약을 스타일 파일 상단 주석과 @fox/README.md에 적었다 — React 밖 소비자는
그것만 보고 DOM을 짤 수 있어야 한다.
앱 화면 스타일은 `.module.scss` 그대로다(앱 전용이라 스코프가 이득). 규약 문서도 그
구분이 드러나게 고쳤다.
Co-Authored-By: Claude Opus 5
디자인 시안이 나오면 다시 추가한다. Tailwind 제거 이후 퇴장 애니메이션(animate-fade-out)
클래스가 죽어 onAnimationEnd가 영영 발화하지 않았고, 그 결과 markSplashShown()도
setIsVisible(false)도 호출되지 않아 모든 화면 상단에 스플래시가 영구히 남아 있었다.
삭제: app/_components/splash-gate.tsx · components/ui/splash-screen.tsx ·
lib/constants/splash.ts. app/layout.tsx의 inline style/script와 도
함께 걷어냈다.
lib/constants/theme.ts의 주석이 삭제될 splash 상수 파일을 가리키고 있어 그 상호참조만
제거했다. public/images/logo-foxpay-white.svg는 재추가 시 다시 쓰므로 남겨둔다.
(public)/layout.tsx는 AuthCard를 더 이상 감싸지 않으므로 그 설명 주석을 제거한다.
Co-Authored-By: Claude Opus 5
크롬 없는 공개 레이아웃 (public)/(empty)를 신설하고 그 아래에 마운트한다 —
(public)/layout.tsx의 AuthCard는 로그인 화면용 껍데기라 이 페이지엔 맞지 않는다.
화면 구현은 @fox/dev-test가 전부 소유하므로 이 라우트는 마운트와 프로덕션 차단만
담당한다.
proxy의 개발 전용 공개 경로도 새 경로로 옮긴다 — 프로덕션에서는 페이지가
notFound()를 내고 이 목록도 평가되지 않아 fail-closed가 유지된다.
Co-Authored-By: Claude Opus 5
토큰 배치 확인과 컴포넌트 예제를 한 페이지에서 본다. 사이드바는 토큰/컴포넌트
두 대분류이며, 토큰 그룹은 CSSOM에서 읽으므로 목록을 코드에 두지 않는다.
컴포넌트는 component-registry의 배열에 항목 하나만 추가하면 사이드바와 본문이
함께 갱신된다 — 만들 때마다 다른 파일을 고칠 필요가 없다.
하니스를 앱이 아니라 @fox에 두는 이유는 폴더를 다른 프로젝트로 복사할 때 함께
따라가게 하기 위함이다. 그래서 앱 상수(테마 저장 키 등)를 import 하지 않고,
테마 전환은 @fox가 소유한 data-theme 계약만 건드린다(저장하지 않는다).
선택 상태는 URL 해시가 SSOT라 새로고침해도 보던 섹션이 유지된다.
같은 목적의 카탈로그를 두 벌 두면 갈라지므로 구 app/design은 함께 제거한다.
Co-Authored-By: Claude Opus 5
디자인 시스템 기반을 Tailwind에서 @fox(SCSS + CSS Modules)로 교체한다.
⚠️ 기존 화면(꾸미기 아이템 포함)의 Tailwind 클래스는 이 시점부터 무효다 —
라우팅·로직은 정상이나 시각 스타일이 빠진 상태이며, 화면을 @fox 컴포넌트로
재작성하며 복구한다. components/ui/의 기존 22개는 소비처가 남아 있어 함께
교체될 때까지 유지한다.
Co-Authored-By: Claude Opus 5
토큰 목록을 코드에 두지 않고 CSSOM에서 읽는다 — tokens/에 값을 추가하면
카탈로그에 자동으로 나타난다. CSS Modules의 :export를 먼저 시도했으나
Turbopack의 Lightning CSS가 이를 인식하지 못해(빌드는 통과하되 값이 조용히
undefined가 된다) CSSOM 열거로 대체했다.
개발 전용 라우트이며 프로덕션에서는 notFound()로 차단된다. proxy는 개발
환경에서만 인증을 면제하므로 프로덕션 빌드에서는 fail-closed가 유지된다.
Co-Authored-By: Claude Opus 5
globals는 @fox 로드와 앱 셸 구조만 담당하고 디자인 값은 갖지 않는다.
폰트는 앱이 소유하고 @fox는 --app-font-sans/--app-font-mono를 참조만 하므로,
@fox 폴더를 다른 프로젝트로 복사해도 그쪽 서체를 그대로 따른다.
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