비밀번호에 이어 ID·이메일도 `@fox/core/validation` 위에 올렸다. 핵심은 검증기를 화면이 아니라
**도메인이 소유**한다는 점이다 — `ADMIN_LOGIN_ID_VALIDATORS`·`adminEmailValidators()`를
`admin-member-form.ts`가 내보내고, 팝업의 입력 칸과 Server Action의 검증이 **같은 배열**을
돌린다. 규칙이 한 군데뿐이라 화면은 통과시키는데 저장은 거부하는 상태가 생기지 않고, 서버가
돌려주는 문구와 입력 중에 보이는 문구가 같다.
ID의 "영어 소문자와 숫자를 조합"은 두 조각으로 나뉜다 — 허용 문자는 `pattern`이 소문자·숫자로
묶고, 그 안에서 둘 다 있어야 한다는 것은 `characterKinds(2)`가 본다. 종전의 손으로 쓴 세 갈래
분기(`validateAdminLoginId`)와 `isValidEmail`은 지웠다.
이메일은 필수 여부만 두 시안이 달라(등록에 `*`, 수정에 없음) 그것만 인자로 받는다.
등록 팝업의 이메일은 칸이 둘로 나뉘어 있어 검증 대상이 합친 값이다. FoxInput 한 칸이 스스로
판정할 수 없으므로 팝업이 돌리고 `touched`도 직접 든다(FoxInput의 blur 규칙과 같은 시점).
아울러 **로컬부가 비면 빈 이메일로 본다** — 도메인만 고른 `@naver.com`을 값으로 보내면 "형식이
올바르지 않습니다"가 뜨지만, 사용자가 한 일은 아무것도 입력하지 않은 것이다.
브라우저 확인: ID는 대문자(`Admin`)·22자에서 각각 다른 사유가 뜨고 `admin01`에서 사라진다.
이메일은 빈 로컬부에서 "이메일을 입력해 주세요.", 입력하면 사라지며 hidden 값이 채워진다.
Co-Authored-By: Claude Opus 5
두 변경이 등록 팝업의 같은 파일에 겹쳐 한 커밋으로 둔다.
## 중복 확인 실패 사유
확인 호출이 백엔드에서 깨지면 `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
`GET /api/v1/mngr/code/list/ITEM_CATE_CD`(사용자 안내)로 카테고리를 가져온다. 종전에는
코드테이블이 정비되기 전이라 프론트에 임시 코드(CATE01 계절 / CATE02 축하 / CATE03 시즌)를
두고 있었다 — 그 상수를 지웠다.
- lib/domain/common-code.ts — 코드 그룹 도메인. 코드 그룹 ID를 한곳에 모아 화면이 문자열을
직접 적지 않게 한다.
- lib/data/repositories/common-code-repository.ts — 조회. `cache()`로 감싸 한 요청 안에서
화면(선택지 그리기)과 Server Action(입력값 검증)이 같은 목록을 한 번만 받아 쓰게 한다.
그리는 데 쓴 목록과 검증에 쓴 목록이 반드시 같아진다는 점이 중요하다.
- 정렬은 백엔드 순서를 그대로 쓴다 — SQL이 아이템 목록과 같은 이중 역순 패턴이라 화면에서
다시 정렬하면 다른 화면과 어긋난다.
- 코드 한 줄이 깨져도(코드값 없음) 그 줄만 건너뛴다. 목록 조회의 fail-fast와 다른 판단인데,
선택지 하나 때문에 화면 전체를 못 쓰게 만들 이유가 없기 때문이다.
검증도 상수 대신 조회 결과를 쓴다. `validateDecorationItemCreate/Update`가 허용 코드 목록을
**인자로** 받고(domain 계층은 통신을 하지 않는다), Server Action이 저장 직전에 다시 조회해
넘긴다 — 화면이 보낸 코드를 그대로 믿으면 코드테이블에 없는 값이 저장된다.
카테고리 표기도 단순해졌다. 목록 SQL이 코드테이블을 조인해 `itemCateNm`을 내려 주므로
프론트 목록에서 이름을 찾던 두 번째 경로가 사라지고, 코드가 지워졌을 때만 코드값으로 떨어진다.
Co-Authored-By: Claude Opus 5
수정 팝업(ADM_ITM_103_p)은 시안이 없어 등록 팝업(5227:4629)의 구성을 그대로 쓰되 아이템ID만
읽기 전용이다. 붙이면서 백엔드(develop, "FIX API 수정")를 다시 읽어 세 가지가 달라진 것을
확인했다.
1. **수정은 이제 JSON이다.** 컨트롤러의 update가 `@ParameterObject` → `@RequestBody`로 바뀌었다.
등록은 그대로 `@ParameterObject`(form)라 **한 도메인 안에서 형식이 갈린다** — 종전처럼 둘 다
form으로 보내면 수정이 조용히 깨진다. 보내는 필드는 그대로 두고 전송 형식만 나눴다.
2. **수정일시 필드명이 다르다.** VO의 `lastMdfcnDt`는 `@JsonIgnore`라 응답에 없고, SQL이
`DATE_FORMAT(...) AS last_mdfcn_dt_str`로 따로 내려 준다 — `lastMdfcnDtStr`를 읽는다.
종전 코드는 응답에 없는 이름을 읽어 수정일시가 항상 `-`였다.
3. **종전에 보고한 "수정하면 유형이 날아간다"는 해소됐다.** 컨트롤러가 itemType을 빌더에 담고
UPDATE 문도 필드마다 ``로 감싸여 보내지 않은 값은 건드리지 않는다. 그래서 썸네일 파일
ID를 보내지 않아도 기존 값이 남는다.
썸네일을 지우면 유지 중이던 파일 ID도 함께 비운다 — 비우지 않으면 지운 것처럼 보이는데 저장은
예전 이미지를 그대로 남긴다. 이제 검증이 "썸네일 이미지를 등록해 주세요."로 잡는다.
남은 백엔드 제약: UPDATE의 `` 때문에 **설명을 빈 값으로 지울 수
없다**(조용히 무시된다).
검증 — 수정 팝업 프리뷰 실측: 탭이 item.itemType(셋트) 선택, 아이템ID 128 읽기전용·name 없음
(제출 제외), hidden itemSn/itemType/categoryCode/imageFileId 정상, 이름·포인트·설명·정렬순서·
사용여부 프리필, 기존 썸네일 표시, 썸네일 삭제 시 imageFileId가 ""로 비워짐.
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
`formatGradeClassNumber`가 학년·반·번호 셋 중 하나라도 없으면 통째로 `-`를 돌려주고 있었다.
백엔드 `TB_COM_USER`에는 GRADE·CLS_NO만 있고 학생 번호 컬럼이 아예 없어(코드베이스 전체 VO가
그 둘만 쓴다) 번호가 영영 오지 않으므로, 실제로 오는 학년·반까지 함께 묻혔다.
있는 값만 이어 붙이도록 바꿨다. 하나도 없을 때만 `-`다.
같은 규칙이 두 곳에 따로 있던 것도 정리했다 — 목록은 자체 `formatClass`로 부분 표기를 이미
하고 있어서 팝업과 결과가 달랐다. 목록이 도메인 함수를 쓰도록 바꿔 한 곳으로 모았다.
팝업은 `-`가 오면 "학교 / -"가 아니라 학교만 보이도록 그대로 걸러 낸다.
검증 — 2·5·7 → "2학년 5반 7번", 2·5·null → "2학년 5반", 2·null·null → "2학년",
null·5·null → "5반", 셋 다 null → "-". 화면에서도 "폭스고등학교 / 2학년 5반"으로 나온다.
Co-Authored-By: Claude Opus 5
백엔드는 `USER_TELNO`를 하이픈 없이 숫자열로 준다(`01012345678`). 시안은 `010-1234-5678`로
끊어 보여주므로 화면에 나가기 직전에 끊는다 — 저장된 값 자체는 건드리지 않는다.
lib/domain/phone-number.ts를 새로 뒀다. 학생·관리자·보호자가 같은 규칙을 써야 해서 도메인마다
두지 않고 한 곳에 모았다(지금은 학생 화면만 쓴다).
규칙 — 휴대전화 11자리는 3-4-4, 10자리는 3-3-4, 서울(02)은 9자리 2-3-4·10자리 2-4-4,
지역번호 없는 8자리는 4-4다. **판단이 서지 않으면 손대지 않는다**: 이미 하이픈이 있거나
자릿수가 규칙에 맞지 않으면 원본을 그대로 돌려준다 — 임의로 끊으면 잘못된 번호를 그럴듯하게
보여주게 된다.
적용 — 목록의 휴대전화번호·보호자연락처, 조회 팝업의 휴대전화 번호·보호자 연락처.
검증 — 함수를 직접 돌려 확인했다. 01011233400→010-1123-3400, 0111234567→011-123-4567,
0212345678→02-1234-5678, 021234567→02-123-4567, 12345678→1234-5678, 이미 하이픈 있는 값과
123·14자리는 원본 유지, 빈 문자열·공백은 null. 화면에서도 01012345678이 010-1234-5678로,
보호자가 010-2222-1234로 나온다.
Co-Authored-By: Claude Opus 5
백엔드를 최신화(develop 924db37 → 4d98756)하고 확인하니 막고 있던 두 가지가 모두 풀렸다.
- `GET /api/v1/mngr/user/pagination`이 4개 필드에서 18개로 늘었다 — userTelno·userEmlAddr·
userType·schNm·grade·clsNo·birth·useYn이 실린다.
- `PUT /api/v1/mngr/user/{userId}/{useYn}` 사용여부 변경 API가 생겼다.
- (덤) `GET /api/v1/mngr/user/{userId}` 단건 조회도 생겼다. 지금은 쓰지 않는다 — 목록 행이
이미 갖고 있는 값으로 팝업을 채우므로 재조회가 필요 없다.
이에 맞춰 이식했다.
- Repository: toStudentMember가 전화·이메일·학교·학년·반·생년월일·사용여부를 실제 값으로
채운다. `useYn`은 'Y'/'N' 문자열이라 boolean으로 옮기고, 값이 없으면 판단하지 않고 null이다.
보호자·학생번호·가입일은 응답에 여전히 없어 `-`로 남는다(SQL·VO 어디에도 컬럼이 없다).
- Repository: updateStudentMemberUseYn 추가. 값과 대상이 모두 경로에 실리고 본문은 쓰지 않는다.
- Server Action: 미지원 반환을 걷어내고 실제 호출로 바꿨다. 성공하면 목록을 revalidate한다.
- 팝업: 저장 성공 시 닫는 동작을 되살렸다.
backend-fetch에 `canHaveEmptyBody`를 더했다 — 이 PUT은 인터페이스에 `ApiResponseVO` 반환으로
문서화돼 있지만 구현이 `void`이고 `ApiResponseVO.success(null)`을 만들어 놓고 버려서, 본문 없는
200이 나간다. 봉투를 파싱하기 전에 빈 본문을 갈라 성공으로 받는다. 백엔드가 봉투를 돌려주도록
고쳐도 그대로 동작한다.
감사 컬럼(LAST_MDFR_*)은 보내지 않는다 — 매퍼가 쓰지만 백엔드 CrudLogInterceptor가 UPDATE마다
자동으로 채운다(확인함).
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
공지사항 등록 시 "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인 채 저장된다.
디자인 시안이 나오면 다시 추가한다. 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
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