두 레인이 같은 시기에 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을 넘기므로 넓어진 타입에 그대로 들어맞는다.
주석에는 두 도메인의 예시를 함께 남겼다.
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인 채 저장된다.
로그아웃 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
비활성 상태였던 엑셀다운로드 버튼을 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()을 채택해 해소했다(같은 환경변수 키, 같은 동작).