색상·타이포·간격·radius·그림자·모션·브레이크포인트 토큰은 실제 디자인 자료 없이
임의로 만들어진 값이었다. 디자인 시스템은 제공된 자료로만 구축한다는 원칙에 따라
값과 그에 딸린 함수·믹스인·리셋을 모두 제거한다.
자료를 받는 시점에 구조부터 다시 세운다.
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
토큰 SSOT는 tokens/의 SCSS map 하나다. 여기서 --fox-* 커스텀 프로퍼티(_root)와
토큰 이름 검증 목록(_functions)이 함께 파생되므로 값과 이름이 어긋날 수 없다.
없는 토큰 이름은 @error로 빌드를 실패시켜 하드코딩 금지를 컴파일러가 강제한다.
모든 치수는 rem이며 1rem = 10px(html font-size 62.5%). 단 미디어 쿼리의 rem은
브라우저 기본 크기 기준이라 브레이크포인트만 예외이며, 각 값에 px 환산을 병기했다.
토큰 값은 파이프라인 검증용 임시 플레이스홀더이며 Figma 확정 시 교체한다.
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
Next.js 16의 'use server' 파일은 async 함수 외의 값을 export할 수 없어(런타임 전용 제약),
폼 상태 초깃값(INITIAL_DECORATION_ITEM_FORM_STATE)을 domain 계층으로 옮겼다.
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
개발 모드 Strict Mode(App Router 기본값)의 effect mount→cleanup→remount 2회 호출로
쿼리 파라미터 감지 토스트가 두 번 뜨는 것을 실제 브라우저 검증 중 확인했다. cleanup 없는
effect라 가드가 없으면 매 remount마다 showToast가 재실행된다. hasNotifiedRef로 최초
1회만 실행되도록 고정한다 — 사용자에게 보이는 알림은 Strict Mode 유무와 무관하게 항상
1회여야 한다.
Co-Authored-By: Claude Opus 5
로그아웃 Server Action이 백엔드 호출 실패를 /login?logoutError=1로 알리면, 새 클라이언트
컴포넌트 LogoutNotice가 마운트 시 기존 FeedbackProvider/Toast로 경고 토스트를 띄우고
쿼리 파라미터를 제거한다. redirect()는 Server Action에서 즉시 던져져 클라이언트가 결과를
돌려받을 수 없으므로, 토스트 표시를 목적지 페이지 마운트 이후로 미뤄 redirect와 경쟁하지
않게 한다. 내부 백엔드 메시지는 노출하지 않고 고정 문구만 보여준다.
Co-Authored-By: Claude Opus 5
로그아웃 Server Action이 GET /api/v1/common/auth/logout을 best-effort로 호출하도록
Repository(requestAdminLogout)를 추가하고 연동한다. 백엔드 호출이 실패해도 로컬 세션
파기·리다이렉트는 항상 진행된다.
이 엔드포인트의 성공 응답은 data가 항상 null(ApiResponseVO.success(null))인데
backendFetch의 기존 규약은 data===null을 실패로 간주해 정상 성공을 오판하므로,
backend-fetch.ts에 opt-in 플래그 canHaveNullData를 추가해 이 호출에만 적용한다(기존
호출부는 동작 변화 없음).
백엔드 호출 실패 시에는 /login?logoutError=1로 리다이렉트해 실패 신호를 남긴다(다음
커밋의 알림 UI가 소비).
Co-Authored-By: Claude Opus 5
세션에 실린 백엔드 accessToken의 형식·만료·유저 일치(adminId/loginId)를 검사하는
isAccessTokenFresh를 추가하고, getSessionAdmin·getSessionAccessToken이 공통 헬퍼
getValidSessionPayload를 통해 이 검사를 공유하도록 dal.ts를 리팩터링한다. 검사 실패 시
두 함수 모두 기존과 동일하게 null을 반환한다(fail-closed, 외부 계약 불변).
Co-Authored-By: Claude Opus 5
비활성이던 정렬 select를 되살린다. 백엔드 목록 API에는 정렬 파라미터가 없고 목록 SQL의
ORDER BY가 하드코딩돼 있어(ORDER BY rnum DESC), 두 정렬을 서로 다른 방식으로 만족시킨다.
- 가입일순: 백엔드의 고정 순서가 이미 가입일 최신순이다. rnum이
ROW_NUMBER() OVER (ORDER BY frst_reg_dt, user_nm DESC)로 매겨진 최초등록일시 순번이고
그것을 역순으로 뒤집기 때문이다. 따라서 페이지 하나만 받아오면 되고 기존 경로 그대로다.
- 이름순: 전체를 받아 서버에서 정렬하고 페이지를 잘라낸다. 받아온 한 페이지만 정렬하면
"그 페이지 안에서만 이름순"이 되어 2페이지의 '김'이 1페이지의 '이'보다 뒤에 오는, 전체 기준
정렬처럼 보이는 잘못된 결과가 나온다. 검색 조건은 백엔드에 그대로 위임하고 페이지 크기만
상한(10,000)까지 키워 한 번에 받는다. 전체를 쥐게 되므로 이 경로에서는 총건수가 확정된다.
회원 수가 상한을 넘으면 그 위로는 정렬 대상에서 빠지고 총건수도 하한값으로 표기된다 —
그 규모에서는 백엔드에 정렬 파라미터가 필요하고, 상한 인상은 임시방편일 뿐이다.
비활성 상태였던 엑셀다운로드 버튼을 GET /api/v1/mngr/user/list/excel에 연결한다.
브라우저는 백엔드를 직접 부르지 않으므로(토큰이 httpOnly 세션 안에만 있다) 라우트 핸들러
/students/excel이 인증을 확인하고 백엔드 xlsx를 스트림으로 중계한다. 파일명·MIME은 백엔드가
RFC 5987 형식으로 이미 올바르게 내려주므로 그대로 전달한다.
백엔드 엑셀 API는 목록 API와 같은 selectPagination을 그대로 타서 페이징이 걸린다 —
파라미터를 생략하면 기본값 recordCountPerPage=10이 적용돼 10건짜리 파일이 나온다. "항상 전체"
사양을 만족시키려고 recordCountPerPage에 충분히 큰 상한(100,000)을 명시해 1페이지로 전부
받아온다. 검색 조건은 의도적으로 싣지 않는다(화면에서 무엇을 검색 중이든 파일은 전체).
- lib/http/backend-fetch.ts: JSON 봉투가 아닌 응답을 위한 backendFetchStream 추가. 성공 판정만
HTTP status 기준이고(성공 응답에 봉투가 없다) 실패 봉투의 code/message 보존은 동일하다.
파일 생성 시간을 감안해 호출부가 타임아웃을 늘려 잡을 수 있게 했다.
- 버튼은 네이티브 GET 폼 제출로 둔다 — 첨부파일 응답이라 화면을 유지한 채 파일만 받는다.
학생 회원 목록 조회를 로그인 구현이 세운 인증·통신 기반 위로 옮긴다.
- 백엔드 HTTP 클라이언트를 lib/http/backend-fetch.ts 하나로 통일한다. 목록 연동에서 임시로
두었던 lib/data/api-client.ts는 삭제하고, 거기에만 있던 세 가지를 backendFetch로 옮겼다:
쿼리스트링 조립, Authorization 헤더(accessToken), 그리고 비2xx 응답의 봉투 code 보존.
마지막 항목이 없으면 백엔드가 HTTP 401로 주는 인증 실패가 통신 오류(code -1)로 뭉개져
"세션이 끊겼다"와 "서버가 죽었다"를 호출부가 구분할 수 없다.
- 세션에 보관된 accessToken을 꺼내는 getSessionAccessToken()을 DAL에 추가한다. 화면 DTO
(AdminUser)는 토큰을 담지 않으므로 서버 전용 경로를 따로 둔다.
- 개발용 토큰 우회로(getDevApiAccessToken / EDUPAY_API_ACCESS_TOKEN)를 제거한다. 실제
로그인이 토큰을 공급하므로 존재 이유가 사라졌다.
- lib/env.ts 충돌은 hub 쪽 getApiBaseUrl()을 채택해 해소했다(같은 환경변수 키, 같은 동작).
mock 학생 회원 생성기를 제거하고 GET /api/v1/mngr/user/pagination에 연결한다.
백엔드 저장소(develop b742bb4)의 실제 구현을 읽고 계약을 확정했다.
- searchCondition은 "1"(이름)·"2"(아이디)·"3"(휴대전화)만 유효하다. 목록에 없는 값을
보내면 조건 없이 전체가 반환되므로 지원되지 않는 학교명·회원코드 검색은 제거했다.
- 유효한 페이징 파라미터는 pageIndex·recordCountPerPage 둘뿐이다(서버가 offset을 직접
계산해 나머지를 덮어쓴다).
- 응답의 totalCount는 전체 건수가 아니라 현재 페이지 행 수다(백엔드에 count 쿼리 없음).
그대로 믿으면 가득 찬 페이지 뒤 데이터에 접근할 수 없어 하한값으로 보정하고, 확정되지
않은 건수는 화면에 "N명 이상"으로 표기한다.
- 응답이 주지 않는 항목(휴대전화·이메일·학교·학년/반·보호자·가입일·사용여부)은 열을
유지한 채 '-'로 표시한다. 정렬 파라미터와 사용여부 변경 API가 없어 해당 컨트롤은
비활성으로 둔다.