기획 BO-MAL-001~004(지역상생몰 > 업체관리)를 「가맹몰 상세 관리」로 옮겼다. 백엔드
(15d409a)의 상세는 기존 가맹점(frcsNo)에 1:1로 붙는 구조라 업체를 새로 만들지 않는다 —
목록에 가맹점 전체를 보이고 행에서 [상세 등록]으로 들어가며, 업체명·연락처·주소는 가맹점
원본을 읽기 전용으로 보여 준다(사용자 확정).
- 목록: 업체명·카테고리·노출상태·등록일 검색, 체크박스 일괄 비활성·삭제, 행 관리
- 등록·수정(/[frcsNo]): 기본 정보 + 이미지(대표 1 + 추가 11) + 메뉴 표 + 등록 정보
- 메뉴 등록·수정(/[frcsNo]/products): 이미지·메뉴명·가격·설명·배지·노출·순서
- @fox FoxSortableList 신설 — 네이티브 DnD + 방향키, 상태 없이 onReorder만 (사용자 확정)
- 배지는 코드를 쉼표로 이어 frcsBadge에 싣는다(BEST,MAIN,NEW,SALE — 사용자 확정)
- 파일 저장소 모듈은 바우처용이 없어 잠정 MODULE_VOUCHER (사용자 확정)
백엔드가 아직 주지 않는 값(노출 상태·카테고리·수정 프리필·기간 검색·메뉴 건수·바우처 사용)은
화면을 시안대로 두고 그대로 보낸다(사용자 확정). 목록의 노출 토글만은 예외다 — PUT이 모든
컬럼을 덮어써 단건 조회가 비어 있는 동안 값 없이 보내면 상세가 지워지므로, 조회 결과에
대표이미지가 없으면 거부하고 수정 화면으로 안내한다.
추가 이미지 묶음은 fileSn이 1..n으로 채번되어 순서·삭제·추가 어느 것이든 통째로 다시 올린다.
저장된 장은 백엔드에서 다시 읽어 합친다. 묶음의 장 수를 묻는 API가 없어 HEAD로 더듬는다.
Co-Authored-By: Claude Opus 5
가맹점: 조회 SQL이 USE_YN을 select하지 않고 MngrVocVo에도 필드가 없어 값이 오지 않는데
`!== 'N'`으로 읽어 늘 「노출」이었다. 「미노출」로 걸러 낸 행까지 전부 「노출」로 보였다.
값이 없으면 판단하지 않고(null) 목록·모달 모두 고르지 않은 상태로 둔다 — 브랜드명·메모·
등록일과 같은 처리다(사용자 확정). 신규 등록만 「노출」로 시작한다.
회원: 학생·관리자 모두 상태를 바꿀 수는 있는데 목록에 그 열이 없어 확인할 자리가 없었다.
학생은 사용여부(useYn), 관리자는 잠김여부(acctLockYn의 반대)를 열로 넣는다. 값이 없는 행은
`-`다.
Co-Authored-By: Claude Opus 5
칸과 함께 도메인 필드·검증·저장 파라미터·응답 매핑까지 걷어낸다 — 그리는 곳이 이 칸뿐이라
남겨 두면 죽은 데이터가 된다.
MENU_EXPLAN 컬럼은 백엔드에 그대로 있다. update가 동적 SET()
이라 보내지 않으면 기존 값이 유지되고, 신규는 NULL로 들어간다 — 메뉴권한(roleId)과 같은
방식이다.
Co-Authored-By: Claude Opus 5
@fox 목록 표는 셀을 이미 text-align: center로 두는데, FoxButtonGroup이 블록 레벨 flex라
셀 폭을 채우고 기본값이 justify-content: flex-start다. 그래서 align="center"를 준 두 화면
(관리자 회원·관리자 메뉴)만 가운데였고 나머지는 왼쪽에 붙어 있었다.
- 행 관리 묶음 다섯 곳에 align="center"를 준다(콘텐츠·꾸미기아이템·게시판·포인트기준·공통코드).
- 시스템 메뉴는 자체 flex div를 쓰고 있어 다른 화면과 같은 FoxButtonGroup으로 바꾼다.
- 가맹점은 묶음 없이 span에 aria-label만 있었다. FoxButtonGroup으로 감싸고 가맹점명을
각 버튼 라벨에 넣는다 — 비의미 요소의 aria-label보다 확실하게 읽힌다(공통코드와 같은 방식).
버튼 하나뿐인 칸(금칙어·포인트 적립)은 손대지 않았다. FoxIconButton이 inline-flex라
셀의 text-align만으로 이미 가운데다(실측 0px).
Co-Authored-By: Claude Opus 5
백엔드가 등록을 자체 저장에서 BizPlay 외부 가맹점 등록 API 위임으로 바꿨다
(edupay-backend 5fa9d34). `POST /api/v1/mngr/voc`가 form에서 @RequestBody(JSON)가 되고
bizPlayVocApiService.insertVoc를 부르며, 예금주명·입금은행코드·입금계좌번호·업태코드가
새로 필수가 됐다. 지금 등록 폼은 그 값을 받지 않아 계약이 맞지 않는다.
- 목록의 [등록] 버튼을 비활성화한다(엑셀등록과 같은 방식). 시안의 자리는 그대로 둔다.
- 모달을 여는 상태·렌더를 걷어내 폼에 닿을 수 없게 한다.
- saveMerchantAction도 저장 직전에서 끊는다 — Server Action은 UI를 거치지 않고도 호출되므로
버튼만 비활성화하면 막은 것이 아니다. 세션 확인과 입력 검증은 그대로 두어 다시 열 때
createMerchant 호출만 되살리면 되게 했다.
다시 열 때: createMerchant를 새 계약(JSON, MngrVocInsertReqVo)으로 바꾸고 폼에 정산 계좌
항목을 추가한 뒤, _actions.ts의 마지막 return을 try/catch + createMerchant로 되돌린다.
Co-Authored-By: Claude Opus 5
폼이 든 모달이 바깥 클릭으로 닫히면 적어 둔 값이 통째로 사라진다. 조회 모달도
표를 훑다 여백을 눌러 닫히는 일이 잦다.
알림·확인(feedback-host)과 중복 로그인 안내는 그대로 뒀다 — 읽고 넘기는 자리라
바깥 클릭이 곧 취소다.
Co-Authored-By: Claude Opus 5
Tailwind가 프로젝트에서 빠진 뒤에도 클래스만 남아 아무 스타일도 받지 못하던 자리들이
있었고, 전역 스피너는 만들어만 두고 어디서도 호출하지 않았다. 두 손질이 같은 파일들을
건드려 한 커밋으로 둔다.
Tailwind 제거:
- components/ui의 옛 Tailwind 레이어 16개 삭제. 그중 field·icon-button만 실제로 쓰여
각각 FoxTextArea·FoxFormLabel과 @fox 아이콘 버튼 마크업으로 옮기고 지웠다.
- error.tsx 다섯 곳이 같은 모양이라 PageError로 모은다(PageLoading과 같은 이유).
- 홈·Empty 레이아웃·게시판 첨부 링크는 module.scss로, 행 관리 버튼 묶음은 FoxButtonGroup로.
- 테마 토글은 FoxIconButton을 쓰지 못한다 — label이 필수라 aria-label이 늘 붙고, 그러면
테마별로 바뀌는 안쪽 글자를 스크린리더가 읽지 못한다. @fox 아이콘 버튼의 마크업 계약을
그대로 따라 같은 스타일을 받게 했다.
스피너 호출:
- useBlockingAction — 스피너를 켠 채 도는 transition. 삭제처럼 확인 얼럿이 닫힌 뒤
진행돼 표시가 행 아이콘 하나뿐이던 자리 11곳에 붙였다. 버튼이 스스로 "저장 중"을
보여주는 자리(중복확인·주소검색·엑셀 업로드)는 그대로 뒀다.
- 스피너 막도 토스트와 같은 이유로 popover(top layer)에 얹는다 — 모달 안에서 시작한
동작도 덮어야 한다.
- loading.tsx를 (basic) 한 곳으로 모은다. 다섯 화면에만 있던 것을 모든 하위 화면이
받도록 하고, 새 화면을 추가해도 자동으로 따라오게 한다.
Co-Authored-By: Claude Opus 5
지금까지는 수정 버튼이 "백엔드 준비 중" 토스트만 띄워 등록된 내용을 볼 방법이
없었다. 모달을 열되 값은 바꿀 수 없게 한다.
`IS_EDIT_ENABLED` 한 줄에 전부 걸어 뒀다 — 수정 API가 생기면 그것만 true로 바꾸면
입력칸도 저장 버튼도 그대로 살아난다. 폼 로직은 손대지 않았다.
Co-Authored-By: Claude Opus 5
폼 맨 아래 FoxHelperText에 있던 문구를 토스트로 올린다 — 등록 폼이 길어서 스크롤 밖에
있으면 저장이 왜 안 됐는지 보이지 않았다. 칸별 오류는 해당 입력칸에 그대로 둔다.
메시지도 백엔드 사유를 그대로 쓴다(형제 화면의 failureMessage와 같은 방식) — 고정 문구는
"저장하지 못했습니다"밖에 알려 주지 못했다.
Co-Authored-By: Claude Opus 5
관리자 회원을 회원관리 아래로, 금칙어관리·가맹점 관리를 관리자관리 아래로 옮기고
「회원정보관리 → 회원관리」·「관리자정보관리 → 관리자관리」로 이름을 바꿨다.
각 화면의 breadcrumb이 메뉴 경로를 그대로 되풀이하고 있어 여섯 곳을 같이 고쳤다 —
안 고치면 메뉴는 관리자관리인데 화면은 시스템관리라고 말하게 된다.
콘텐츠관리 아이콘이 기본값으로 떨어지던 것도 함께 고쳤다. 이름 `image`가 사이드바
허용 목록에 빠져 있었다.
Co-Authored-By: Claude Opus 5
백엔드가 중복 로그인 감지를 넣으면서, 내 토큰이 밀리면 이후 모든 요청이
403 + code 905(만료된 토큰입니다)로 떨어진다(JwtAuthenticationFilter).
어느 화면·어느 API에서 걸리든 같은 결말이어야 해서 backendFetch 한 곳에서
처리한다.
- 905/904를 받으면 /logout/revoked로 보낸다. 렌더 중에는 쿠키를 지울 수 없어
라우트 핸들러에서 세션을 파기하고 /login으로 안내와 함께 되돌린다.
- redirect()는 예외로 던져지므로 Server Action의 catch가 삼키면 안 된다.
저장소를 호출하는 catch에 unstable_rethrow를 넣어 통과시킨다.
- 그 경로는 쿠키가 이미 없어도 지나가야 안내가 유지돼 proxy 공개 경로에 넣는다.
Co-Authored-By: Claude Opus 5
세 가지가 겹쳤다. 파일업로드 칸이 `flex-shrink: 0`이라 좁은 그릇에서 버티고,
모달 안 form이 `align-items: flex-start`에 걸려 폭을 안 채우고, 모달은
`overflow-x: hidden`이라 넘친 만큼 그대로 잘렸다. 400px 모달에서 191px가 잘렸다.
칸은 늘어나지는 않고 줄어들기만 하게 두어 넓은 자리에서는 시안의 320px를 지킨다.
팝업은 md로 올려 그 320px가 그대로 들어간다.
Co-Authored-By: Claude Opus 5
기획 MCH_002_p ③. 버튼을 누른 순간 스크립트를 한 번만 받아 창을 연다 — 자주 열리는
화면이 아니라 모든 페이지가 이 30KB를 지고 다닐 이유가 없다. 도로명/지번은 사용자가
고른 표기를 그대로 쓰고, 우편번호(frcsPostZip)까지 함께 채운다.
좌표는 이 서비스가 주지 않는다. 카카오 지도 지오코더가 필요하고 앱 키가 아직 없어,
NEXT_PUBLIC_KAKAO_MAP_KEY가 설정되면 자동으로 동작하고 없으면 비운 채 진행한다.
Co-Authored-By: Claude Opus 5
기획 시안과 틀어져 지우면 안 되는 항목이었다(사용자 확정). 브랜드명·메모·등록일·
최종수정일을 목록·팝업·엑셀에 되살리고, 값이 없을 때 '-'가 나오게 한다.
등록 시 브랜드명·메모도 함께 보낸다 — 요청 VO에 없어 지금은 버려지지만 컬럼이
생기면 프론트 수정 없이 저장된다.
Co-Authored-By: Claude Opus 5