백엔드가 toon/video 컨트롤러를 /cntntns 하나로 합치고 CRUD를 열었다. 목록은
searchCntntsType(V·T)으로 갈리고, 단건·등록·수정·삭제가 모두 실제 API를 탄다.
등록은 @RequestBody가 없어 form이라 중첩 목록을 keywordList[0].cntntsKeyword 같은
인덱스 표기로 편다. JSON으로 보내면 한 칸도 바인딩되지 않는다.
학교·학년은 새로 생긴 schulGrdList로 오간다 — 학년마다 한 줄이다.
컷은 upload/list/{moduleId}로 한 첨부에 묶어 올린다. 백엔드가 fileSn을 1,2,3으로
채번해 주므로 컷을 가리키는 건 아이디가 아니라 순번이 됐다. 응답의 fileList로
실제 순번을 받아 미리보기 주소를 만든다.
Co-Authored-By: Claude Opus 5
INSERT에 DEFAULT_YN이 들어가는데 화면에 없는 항목이라 아무도 보내지 않아 NULL이
박히고 있었다. 등록에는 'N'을 싣는다.
아이콘을 고르지 않았을 때 빈 문자열을 보내고 있었다. 파일 테이블을 가리키는 컬럼이라
등록에서는 값이 있을 때만 싣고, 수정에서는 지우기를 위해 빈 문자열을 유지한다.
실패하면 코드·메시지·보낸 파라미터를 서버 콘솔에 남긴다 — 백엔드 로그를 볼 수 없는
환경이라 이게 유일한 단서다.
Co-Authored-By: Claude Opus 5
기획 SYS_MNU_001/002_p/003_p, SYS_WRD_001/002. 디자인 시안은 메뉴관리 목록
5432:16942, 메뉴 수정 5431:12392.
메뉴관리는 트리다. 기획대로 페이징 없이 전부 받아 upMenuId로 세우고, 최상위 신규
등록은 두지 않았다(추가는 각 행의 + 로 하위에만). 아이콘은 MODULE_MENU_ICON에
올려 받은 파일 아이디를 저장하고, 지울 때는 빈 문자열을 보낸다 — UPDATE가 null만
건너뛰어 이래야 컬럼이 비워진다.
금칙어는 목록·등록(별도 화면)·삭제·엑셀 일괄등록까지 연결했다. 중복은 백엔드가
판정하고 화면은 기획 문구로 바꿔 보여준다.
백엔드 결함 세 가지를 확인해 우회했다(별도 보고).
- menu/benWord DELETE에 @PathVariable이 없어 경로와 쿼리에 같은 값을 함께 보낸다.
- benWord 목록 정렬이 이중 역순이라 오래된 것이 먼저 온다 — 한 번에 받아 최신순으로
다시 세운다.
- TB_SYS_MENU에 새창여부 컬럼이 없다. 사용자 확정에 따라 칸은 두고 값은 보내지 않는다.
Co-Authored-By: Claude Opus 5
기획 PTS10100001 목록 / PTS10100002_p 수정 / PTS10200001 목록 /
PTS10300001 상세내역. 디자인 시안이 없어 기존 화면들과 같은 뼈대로 짰다 —
FoxListContainer + searchParams SSOT + Server Action.
백엔드 API가 없어 Repository 두 개가 mock이다. 기준관리는 모듈 메모리를 고쳐
수정·삭제가 실제로 반영되고(서버 재시작 시 초기화), 누적포인트는 인덱스로만
3,290건을 만든다 — 난수를 쓰면 새로고침마다 목록이 흔들린다.
기획을 그대로 따른 것: 기준관리에 신규등록이 없다(코드관리와 연동되는 항목),
이벤트는 읽기 전용, 제한포인트 0 = 제한 없음, 상세내역 취득은 파랑 +·사용은 빨강 -.
FoxSelect에는 FoxSelectText와 달리 ariaLabel이 없어 필터 셀렉트의 접근성 이름을
옵션 라벨("학년 전체", "3반")이 대신한다.
Co-Authored-By: Claude Opus 5
학교·학년 코드표 조회(`fetchSchoolGradeCodes`)가 검증 인자로 들어가면서 try/catch **밖**에
놓였다. 저장 호출만 감싸고 있었으니 코드 조회가 실패하면 예외가 Server Action을 그대로 빠져나가
`Cannot read properties of undefined (reading 'stack')`로 끝난다.
실패는 실제로 일어난다 — 백엔드는 인가 실패도 200이 아닌 401에 봉투를 실어 돌려주고
(확인: `GET /api/v1/mngr/code/list/COM_SCHUL_CD` → `{"code":401,"message":"인가된 사용자가
아닙니다."}`), 세션 쿠키는 살아 있어도 액세스 토큰이 먼저 만료될 수 있다. 화면을 열 때는
멀쩡하다가 저장할 때 터지는 이유가 이것이다.
여섯 개 쓰기 액션의 본문을 `runWrite`로 감싸 코드표 조회·업로드·저장이 모두 한 울타리 안에서
일어나게 한다(게시판 액션과 같은 방식). `redirect()`는 예외로 흐름을 끊으므로 울타리 밖에 둔다.
이제 같은 실패가 폼 아래 메시지로 보인다.
함께 고친 것:
- 코드가 한 건도 없는 그룹이 `data: null`로 오면 예외 대신 빈 목록으로 다룬다. 학년(`GRD_CD`)은
학교에 따라 결과가 빌 수 있어 이것을 오류로 보면 화면 전체가 멎는다.
- `/contents`에 error·loading 경계를 둔다(다른 화면과 같은 구성). 없으면 조회 실패가 세그먼트
경계 없이 위로 튀어 같은 종류의 불투명한 오류로 보인다.
Co-Authored-By: Claude Opus 5
학교급을 프론트 상수 표(ELEM/MIDD/HIGH/ALL + 학년 수)로 들고 있었다. 그 표를 지우고 공통코드를
조회해 쓴다 — 1:1문의 진행상태(BBS_ANS_CD), 꾸미기 아이템 카테고리(ITEM_CATE_CD)와 같은 방식이다.
학년은 학교마다 다르므로 `GET /api/v1/mngr/code/list/GRD_CD?atrbNm={학교코드}`로 학교별로
조회한다(백엔드 매퍼가 `ATRB_NM1 = #{atrbNm}`로 거른다). 상세 목록 SQL이 `ATRB_NM1`을 select하지
않아 한 번에 받아 화면에서 가를 수 없어 학교 수만큼 왕복하고, `fetchSchoolGradeCodes()`가 그
왕복을 한 곳에 모아 `cache()`로 중복을 막는다.
저장에는 `GRD_CD` 코드값을 보낸다 — 화면의 학년 칩이 코드값을 그대로 hidden 필드에 싣고,
Server Action은 저장 직전에 코드표를 다시 조회해 그 학교에 없는 학년 코드를 거른다(화면이 보낸
값을 믿지 않는 기존 규칙과 같다).
"전체연령은 학년 선택 불가"가 하드코딩에서 데이터로 바뀌었다 — 학년 코드가 하나도 없는 학교는
칩을 그리지 않고 검증도 학년을 요구하지 않는다.
확인: 학교 전환 시 학년 칩이 그 학교 것으로 갈리고 이전 선택이 비워지는 것, 다중 선택이
`GRD001,GRD003`처럼 코드값으로 실리는 것, 학년 코드가 없는 학교는 칩이 0개인 것을 임시 페이지로
확인한 뒤 지웠다.
Co-Authored-By: Claude Opus 5
지어낸 값(`MODULE_CNTNTS`)을 쓰고 있었다. 저장소 설정에 없는 id를 보내도 업로드가 실패하지
않고 기본 저장소로 조용히 폴백하기 때문에(`EgovFileMngUtil.parseFileInf`) 지금까지 드러나지
않았을 뿐이다. 받은 실제 목록 8종으로 상수를 바꾼다.
id만 두지 않고 설명·허용 확장자를 함께 담았다. 화면의 `accept`를 `fileModuleAccept()`로 그
목록에서 만들고, Server Action이 업로드 직전에 `isAllowedFileExtension()`으로 한 번 더 본다 —
확장자가 틀리면 백엔드는 사유 없이 실패하므로 어느 파일이 왜 안 되는지 여기서 알려 준다.
업로드 지점별로 저장소가 갈린다:
- 콘텐츠 썸네일(쇼츠·만화) → MODULE_CONTENT_THUMB
- 만화 컷 이미지 → MODULE_TOON_CONTENT
- 금융쇼츠 영상 → MODULE_VOD_CONTENT
- 꾸미기 아이템 썸네일 → MODULE_USER_ITEM (기존과 같음)
꾸미기 아이템의 화면 제한은 JPG·PNG로 그대로 둔다 — 저장소는 gif·svg도 받지만 그 화면 시안이
안내문에 JPG/PNG를 못박고 있어 저장소 허용 범위를 상한으로만 쓴다.
게시판 첨부용 저장소는 목록에 없다. `MODULE_BBS`를 그대로 두되 상수를 분리하고 TODO를 달았다 —
지금은 기본 저장소로 저장되고 있다.
Co-Authored-By: Claude Opus 5
`video/pagination` 컨트롤러가 `ApiResponseVO`가 아니라 `null`을 돌려준다. 그러면 Spring이
봉투를 쓰지 않고 **본문이 통째로 빈** 200을 내려보내는데, `backendFetch`가 곧바로
`response.json()`을 불러 "Unexpected end of JSON input"으로 끊겼다.
`data: null`은 이미 허용해 두었지만 그것은 봉투 안의 data가 null인 경우고, 이번 건은 봉투
자체가 없는 경우라 걸리지 않았다. 목록 조회에 `canHaveEmptyBody`를 켜서 본문을 먼저 글자로
읽고 비어 있으면 빈 목록으로 다룬다.
백엔드가 서비스를 붙이면 이 분기는 자연히 지나가지 않는다.
Co-Authored-By: Claude Opus 5
콘텐츠관리는 게시판(bbs)이 아니라 별도 API다. 쇼츠·만화는 `TB_COM_CNTNTS`를 공유하고
(`/api/v1/mngr/cntntns/{video|toon}`), 퀴즈는 테이블도 경로도 따로다(`/api/v1/mngr/quiz`).
그래서 Repository도 그 경계대로 둘로 나눴다.
등록·수정은 팝업이 아니라 별도 화면이다 — 기획의 화면ID·화면경로(등록 < 금융쇼츠 < 콘텐츠관리)와
디자인 시안이 모두 전체 화면이고, 항목 수가 팝업에 담기지 않는다.
목록 화면은 디자인 시안이 없어 디자인 시스템과 기존 목록 화면의 흐름을 따랐다 — 정렬은 기획의
두 버튼 대신 다른 목록과 같은 정렬 셀렉트로, 학교급·학년·난이도는 필터 칩으로 놓았다.
정답여부는 O·X 두 줄을 모두 그리되 정답으로 고른 줄에서만 노출 문구를 정한다(기획 ③). 기획은
두 줄 다 입력칸을 그리고 아닌 쪽을 흐리게, 디자인 시안은 고른 쪽에만 컨트롤을 그렸는데 비활성으로
두면 두 시안이 같은 화면이 된다.
백엔드 미비 사항은 코드에 이슈로 남겼다. 요약:
- 등록·수정·삭제·단건조회 API가 3종 모두 없다. Repository는 다른 관리 화면과 같은 규약으로
미리 맞춰 두었고, 단건은 그 API가 생길 때까지 목록에서 찾는다.
- `cntntns` 경로 오타, `video/pagination`의 `return null`, toon 매퍼의 `BEN_WORD_NM` 복붙,
키워드 조인으로 인한 count 불일치.
- 학교급·학년·주제·전달메시지·정답 노출문구를 담을 컬럼이 없다. 코드값도 정의되지 않아
잠정 상수로 두고 TODO를 달았다.
확인: 임시 페이지로 세 폼과 두 목록을 렌더링해 항목 구성, 학교급 전환 시 학년 칩 잠금(전체연령),
학년 다중선택, 정답여부 「기타」 선택 시 직접입력 노출을 확인한 뒤 페이지를 지웠다.
Co-Authored-By: Claude Opus 5
1:1문의 컬럼이 공지·FAQ와 같은 `actionsColumn('inquiry')`를 쓰고 있어 연필을 누르면 **글 수정
폼**이 열렸다. 관리자는 질문 글을 수정하지 않는다 — 전용 `InquiryRowActions`가 이미 있었는데
컬럼에 연결되지 않아 쓰이지 않고 있었다. 연결하고, 그 버튼도 무스타일 `components/ui/button`
에서 다른 목록과 같은 `FoxIconButton`(연필·휴지통)으로 바꿨다.
답변 팝업은 기획(1676:18693)대로 @fox로 다시 그렸다 — FoxModal·FoxInput·FoxTextArea·FoxSelect.
기획의 "게시물의 항목 (조회 전용)" 구분 제목, 질문자명/작성일·이메일/전화번호 2열, 진행상태
안내 문구를 그대로 옮겼다. 답변내용에는 글자수 카운터가 붙는다.
## 곁들여 고친 것
첨부 상한 상수를 `file-repository`(server-only)에서 도메인으로 옮겼다. 지난 커밋에서 클라이언트
컴포넌트가 그 상수를 가져오게 만들어 `next/headers`가 클라이언트 번들로 끌려들어 갔고, dev에서
"You're importing a module that depends on next/headers"로 빌드가 깨졌다.
첨부 입력은 Tailwind 제거 이후 무스타일이라 토큰으로 파일선택 버튼 모양을 맞췄다.
Co-Authored-By: Claude Opus 5
두 가지가 겹쳐 세 토글이 모두 먹히지 않았다.
1. 폼과 Server Action의 값 규약이 어긋났다. 팝업을 @fox로 옮기며 토글을 value="true"로 두었는데
게시판 액션의 readBoolean은 'Y'만 참으로 본다 — 켜도 항상 false가 되어 'N'으로 저장됐다.
꾸미기 아이템 쪽 규약을 그대로 가져온 것이 원인이라 게시판 규약('Y')에 맞췄다.
2. 수정 PUT을 쿼리스트링으로 보내고 있었다. 백엔드 update는 @RequestBody(JSON)라 본문이 비어
바인딩이 되지 않고, 통과하더라도 전 필드가 null이라 UPDATE의 가드가
모두 건너뛴다 — 수정으로는 어떤 값도 바뀌지 않았다. 1:1문의 답변 저장도 같은 경로였다.
등록 POST는 컨트롤러가 form 바인딩이라 그대로 둔다.
검증 — 팝업 FormData 실측: 기본 상태 isVisible='Y'(켜짐)·isPinned/isPushEnabled 없음(꺼짐),
두 토글을 켜면 셋 다 'Y'로 실린다(액션의 'Y' 규약과 일치).
TODO(백엔드): 앱푸쉬는 이 수정으로도 저장되지 않는다 — PUSH_YN이 MngrBbsMapper.xml의
INSERT·UPDATE·SELECT 어디에도 없다.
Co-Authored-By: Claude Opus 5
두 가지가 어긋나 있었다.
**분류 필터가 탭이었다.** 시안은 `chip-area`에 `chip` 세 개(전체·노출·미노출)다.
`FoxTab`을 `FoxChipArea` + `FoxChip type="action"`으로 바꿨다 — `check` 계열은 켜졌을 때
체크 아이콘이 붙는데, 시안의 "전체"와 "노출"이 둘 다 42px로 같아 아이콘이 없는 계열이다.
초기화 버튼도 시안이 28×28이라 `size="md"`(40)에서 `sm`으로 내렸다.
**검색 대상에 구분이 없었다.** 시안은 닫힌 상태가 "구분"인데 공지사항의 검색 대상이 제목
하나뿐이었다. 구분을 첫 항목으로 넣어 기본값이 되게 하고, 검색값은 코드가 아니라 표기 라벨로
견준다(사용자가 "공통"을 치면 걸리게).
확인: 초기화 버튼 28×28, chip-area x=36, 칩 43/43/56×28, 체크 아이콘 없음, 검색 대상 "구분" —
시안 치수와 1px 안에서 일치한다.
Co-Authored-By: Claude Opus 5
기획 3화면을 앞선 목록 화면들과 같은 구성으로 만들었다 — 시안이 없어 @fox의 FoxListContainer·
FoxModal·FoxInput·FoxSelect 조합을 그대로 쓴다.
목록: 메뉴번호·활성여부·메뉴명·메뉴순번·권한·관리. 메뉴명은 `upMenuId`로 트리를 세워 들여쓰고
자식이 있으면 캐럿으로 접고 편다(기획 ①의 아코디언). 검색·페이징은 기획대로 두지 않는다.
관리 열은 수정·하위추가·삭제 세 개이고, 최상위 신규 등록은 기획대로 제공하지 않는다(툴바 없음).
팝업은 등록·수정이 항목·순서가 같아 한 파일이 두 모드를 맡는다. 상위메뉴명은 `[번호] 이름`으로
읽기 전용이고, 최상위 메뉴 수정처럼 상위가 없으면 칸 자체가 빠진다.
브라우저 확인: 트리 6행이 부모-자식 순으로 서고, FOX PAY를 접으면 하위가 통째로 사라져 2행이
된다(재귀). 수정 팝업은 상위 `[4] 내 강의실`·메뉴명·순번·역할·활성 값을 모두 채운다.
## 백엔드 이슈 (그대로 연동, 사용자 확정)
- `DELETE /{menuId}`에 `@PathVariable`이 없어 경로값이 안 들어온다 → 삭제가 항상 실패한다.
- 등록 menuId 채번이 `MAX(menu_id) WHERE UP_MENU_ID = ?`라 상위별 최대값이다 → 자식이 없는
메뉴에 처음 추가하면 이미 있는 번호가 나와 PK 충돌로 실패한다.
- `GET /{menuId}`는 경로변수 이름이 `userId`로 어긋나고 빈 VO로 조회한다 → 쓰지 않고 수정 팝업의
초기값은 목록 응답에서 넘긴다.
- 새창여부에 해당하는 컬럼이 없다 — 기획대로 그리되 저장되지 않는다.
- 역할 목록 API가 없어 메뉴에 쓰인 역할을 모아 선택지로 쓴다.
API는 `/api/v1/mngr/menu/**`다. `mngr/bbs`는 게시판 설정이라 메뉴와 무관하다.
Co-Authored-By: Claude Opus 5
사용자 지시 — "주석이 너무 많다, 주석은 TODO나 이슈사항만 적어줘".
이번 작업에서 내가 쓴 설명 주석을 지웠다(23개 파일, 633줄 삭제 / 14줄 추가). 파일·함수 설명,
동작 요약, 결정 근거, 시안 번호, 검증 방법은 전부 뺐다 — 코드·커밋 메시지·README가 할 일이다.
남긴 것은 없으면 다음 사람이 사고를 낼 자리 다섯 줄뿐이다.
- 등록은 form(@ParameterObject), 수정은 JSON(@RequestBody)로 갈려 있음
- 백엔드 totalCount가 전체가 아니라 현재 페이지 행 수
- 관리자별 메뉴 권한이 백엔드에 없어 저장되지 않음
- 파일 업로드 moduleId는 틀려도 실패하지 않고 기본 저장소로 폴백 (+ 모듈 조회 API TODO)
- `
백엔드가 **모듈 목록 조회 API를 만들어 주기로 해서**(2026-08-19 합의) 그때 바꿀 곳이 하나가
되도록 정리했다. 지금은 `MODULE_ITEM`(아이템 썸네일)이 Repository 안 사설 상수로, `MODULE_BBS`
(게시판 첨부)가 게시판 도메인 파일로 흩어져 있었다.
이 값은 백엔드에서 파일 저장 설정(`FileStrgStngVo.strgStngId`)을 가리켜 저장 폴더·허용 확장자·
최대 크기를 정하는데, **틀려도 업로드가 실패하지 않는다** — 설정을 못 찾으면 기본 저장소
(`FILE_STORAGE`)로 폴백해 조용히 다른 곳에 저장된다. 티가 나지 않는 것이 이 값의 위험한 점이라
근거를 `lib/domain/file-module.ts` 주석에 모아 두었다.
곁들여 `file-repository.ts`의 설명을 사실에 맞게 고쳤다 — "설정에 없으면 `UNKNOWN` 경로로
저장된다"고 적혀 있었으나, 실제 폴백은 `FILE_STORAGE` 저장소다(`UNKNOWN`은 설정의 strgStngId가
비었을 때 쓰는 별개 값이다).
Co-Authored-By: Claude Opus 5