로그가 콘텐츠 쓰기에만 있어 만화 등록 같은 요청은 형태를 확인할 수 없었다.
backend-fetch 단일 진입점(일반·스트림 다운로드·스트림 업로드)으로 내려 메서드,
쿼리 포함 전체 URL, 본문(JSON은 Swagger에 붙여 넣을 수 있는 모양, form은 인코딩
문자열, multipart는 파일명·크기)을 전부 찍는다. 운영에서는 찍지 않는다.
콘텐츠 쪽 성공 로그는 중복이라 실패 전용으로 되돌린다.
Co-Authored-By: Claude Opus 5
목록 SQL이 rnum(등록일 내림차순)을 ORDER BY RNUM DESC로 다시 뒤집어 옛 글부터
내보내고, LIMIT도 키워드·학교급·파일 조인으로 불어난 행 기준이라 방금 등록한
글이 첫 페이지에 없었다. 페이지 경계에서 잘린 콘텐츠는 키워드 목록도 반쪽으로
온다.
benWord와 같은 우회를 쓴다 — 한 번에 받아(상한 500) 일련번호 내림차순으로 다시
세우고 로컬에서 자른다. 검색어도 서버에 보내지 않는다. 검색 조건이 c.·k. 별칭을
쓰는데 count 쿼리에 별칭이 없어 키워드를 실으면 count가 SQL 오류로 죽는다.
학교·학년·키워드 필터를 모두 로컬로 걸어 총건수도 정확해졌다.
Co-Authored-By: Claude Opus 5
백엔드가 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
학교급을 프론트 상수 표(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
`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