퀴즈가 콘텐츠관리 API 아래로 들어왔다. 경로를 `/api/v1/mngr/quiz`에서
`/api/v1/mngr/cntntns/quiz`로 옮기고 쇼츠·만화와 같은 규약으로 맞춘다 — 등록은 form(중첩
목록은 인덱스 표기), 수정은 JSON, 삭제는 soft delete.
학교·학년이 생겼다. `TB_COM_QUIZ_SCHUL_GRD`에 따로 저장되고 `schulGrdList`로 오간다 —
쇼츠·만화와 같은 모양이라 매핑·전송 규칙을 그대로 쓴다(학년 없는 학교는 `grdCd`를 빼서
GRD_CD에 빈 문자열이 박히지 않게 한다). 수정 화면이 저장된 학교·학년을 불러온다.
엑셀 일괄등록은 기획 중이라 넣지 않았다 — 백엔드에도 업로드 엔드포인트가 없고
`excel/download`만 있다. 버튼과 Server Action을 걷어내고 TODO만 남긴다. 다운로드 경로는 새
API로 옮겼다.
확인(임시 프로브로 실제 요청 캡처): 등록 `POST /cntntns/quiz` form +
`schulGrdList[0].schulCd`, 수정 `PUT /cntntns/quiz/42` JSON, 삭제 `DELETE /cntntns/quiz/42`,
학년 없는 학교는 `grdCd` 파라미터 자체가 빠짐. 응답 매핑도 `schulGrdList` → 학교 코드 +
학년 배열, `useYn:'N'` → 비공개로 확인.
⚠️ 백엔드 결함 5건을 Repository 주석에 적었다. 특히 **등록 INSERT의 컬럼 11개 : 값 10개**
불일치(`USE_YN` 자리 누락)로 지금은 등록이 SQL 오류로 끝난다. 프론트는 `useYn`을 정상적으로
보내므로 값 목록만 맞추면 그대로 동작한다.
Co-Authored-By: Claude Opus 5
컷당 구성이 시안과 달랐다. 시안은 테두리 컨테이너 안 회색 판(#E6E8EA) 위에 흰 컷 카드
(radius 12·neutral/lv1 그림자)가 4열로 깔리고, 카드마다 **점선 상자 자체가 파일선택 버튼**
(CameraPlus 중앙, 높이 172)이며 아래에 「N컷 · 초기화 · 삭제」가 붙는다. 「컷 추가」는 컨테이너
흰 푸터에 있다. 기존 구현은 테두리 박스 안에 작은 132×74 썸네일 + 파일선택 버튼이었고
초기화가 없었다.
FoxFileUpload에 `thumbnailPicker` 변형을 추가했다 — 썸네일 상자만 그리고 상자를 파일선택
버튼으로 쓴다(파일선택 버튼·설명·삭제 오버레이 없음). 시안의 이 패턴을 담을 형태가 image
모드에 없었다. 비우기·삭제는 호출부가 바깥에 만든다는 계약을 주석으로 못박았다.
초기화는 그 컷의 React key를 갈아 끼워 새로 마운트한다 — 비제어 FoxFileUpload가 들고 있는
파일과 네이티브 입력을 밖에서 비울 방법이 그것뿐이다. 초기화하면 기존 첨부 유지용
`cutFileId`도 함께 비워져 저장 시 그 컷이 빠진다.
확인(임시 페이지): 상자 클릭 대상 aria-label 「1컷 이미지 선택」, 파일 주입 → 미리보기 표시,
초기화 → 미리보기·네이티브 입력 모두 비워짐, 컷 추가 → 5컷에서 삭제 활성, 삭제 → 4컷에서
비활성. 컨테이너 테두리 #B1B8BE·radius 12·판 #E6E8EA·카드 그림자·점선 #CDD1D5·높이 172가
계산값으로 시안과 일치.
Co-Authored-By: Claude Fable 5
예시안 안에 손으로 그린 SVG 도형이 셋 남아 있었다(DemoIcon·DemoStar·DemoCalendar). 이 화면이
곧 사용법 안내라, 버튼·칩·셀렉트 슬롯에 SVG가 들어가 있으면 "아이콘은 이렇게 넣는다"로 잘못
읽힌다 — 실제로 혼동이 생겼다. 셋 다 `fox-ico` 클래스로 바꾼다(CaretRight·Star·CalendarBlank).
호출부 69곳은 그대로다.
폰트 아이콘은 `width`/`height`가 아니라 `font-size`가 크기를 정한다. FoxSelect 힌트 아이콘
15곳의 `style`도 함께 바꿨다.
아이콘 슬롯 prop 주석도 고친다 — "`currentColor`로 그린 SVG여야 한다"가 다섯 컴포넌트에 남아
있어 같은 혼동을 계속 만든다. 마크업 계약을 그대로 적는다.
`DemoThumb`(320×180)만 SVG로 둔다 — 아이콘이 아니라 카드·모달의 사진 자리를 채우는 그림이다.
확인: 예시안의 버튼 아이콘이 "Fox Icons Regular" 16×16으로 그려지고 글리프가 실린다.
칩의 달력도 CalendarBlank로 나온다. 예시안 어디에도 `
`MODULE_BBS`가 저장소 목록에 없어 임시 상수로 빼 두었는데(백엔드가 설정을 못 찾으면 기본
저장소로 조용히 폴백한다) 이제 「게시글 스토리지」로 추가돼 다른 모듈과 같은 자리로 옮긴다.
허용 확장자는 이 저장소만의 목록을 받지 못해 전역 화이트리스트(`Globals.fileUpload.Extensions`)를
그대로 둔다 — 화면이 먼저 막지 않게 하기 위해서다. 저장소 목록이 더 좁으면 백엔드가 사유를
돌려주고 그 문구가 화면에 그대로 나간다. 실제 목록을 받으면 그 한 줄만 바꾼다.
첨부 입력에 `accept`와 확장자 사전 검증이 붙는다 — 지금까지는 아무 파일이나 고를 수 있었고
확장자가 틀리면 백엔드가 사유 없이 실패했다.
Co-Authored-By: Claude Opus 5
findBoardPostById가 있는데 아무도 부르지 않고 목록 행으로 폼을 채우고 있었다.
행은 화면에 떠 있는 동안 낡을 수 있고, 그대로 저장하면 남의 수정을 덮는다.
없어진 게시물이면 얼럿으로 알리고 팝업을 열지 않는다.
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
Figma의 grid 값은 아트보드 좌표였다. 폭은 아트보드 폭이고, 여백은 그 안에서 가운데로 맞추기
위한 offset이다 — PC의 `grid-margin-sm`이 420px인 이유가 (1920-1080)/2였다. CSS에서는 둘 다
뜻이 달라 그대로 쓸 수 없었다.
폭은 흔한 브레이크포인트로 반올림한다(사용자 확정 사항).
PC 1552 / 1080 / 800 → 1440 / 1024 / 768
모바일 348 / 320 / 280 → 360 / 320 / 280
같은 수를 `$breakpoints`로도 낸다. 그래야 "이 폭에서 레이아웃이 갈린다"와 "컨테이너가 여기까지
넓어진다"가 어긋나지 않는다. `xsm`(768)은 기존 `$breakpoint-pc`와 같은 값이라 그 믹스인은
그대로 쓸 수 있다.
여백은 아트보드 offset이 아니라 페이지 좌우 여백으로 바꾼다 — 모바일 16px · PC 48px. 컨테이너가
좁아진다고 여백이 넓어지지는 않으므로 세 단계가 한 값을 공유하고, `full`만 0(가장자리까지)이다.
가운데 정렬은 `margin-inline: auto`가 맡는다.
미디어 쿼리는 `var()`를 해석하지 못하므로 `fox.from(xsm|sm|default)` 믹스인을 함께 연다 —
`fox.pc`/`fox.mobile`과 같은 이유다. 모르는 이름은 컴파일 때 잡는다.
치환은 변환기(`build-tokens.py`)가 들고 있어 재export해도 유지된다.
확인: `fox.from(sm)`·`fox.from(default)`가 각각 `@media (min-width: 1024px)`·`1440px`로
컴파일되고, 모르는 이름은 "알 수 없는 브레이크포인트"로 끊긴다. 산출 CSS 변수도 새 값으로 나온다.
Co-Authored-By: Claude Opus 5
시안에 ico/*가 지정된 자리를 Figma에서 확인해 그 글리프로 바꿨다. 시안에서 확인이
안 되는 두 개는 손그림을 캔버스로 렌더해 후보 글리프와 픽셀 대조했다 —
tag의 택 표시는 Tag/Fill(불일치 0.03%), chip의 체크는 Check/Bold(0.19%)로
손그림이 그 글리프를 그대로 옮긴 것이었다.
남긴 것 넷은 아이콘이 아니다. 툴팁 꼬리, 체크박스·라디오의 표시(시안이 별도
컴포넌트로 둔 것), 단계 표시의 체크(시안이 선으로 준 것), 그리고 Tailwind 잔재에
기대고 있는 components/ui 캐럿이다.
Co-Authored-By: Claude Opus 5
Fox*Icon 컴포넌트 141곳을 fox-ico 클래스로 바꾸고 옛 SVG 모듈을 지웠다.
아이콘 상자 규칙에는 상자 크기를 그대로 둔 채 font-size만 더했다 — 손으로 그린
인라인 svg가 든 자리도 같이 쓰이기 때문이다.
사이드바 메뉴 아이콘의 화이트리스트가 사라졌다. 폰트는 통째로 하나라 이름이 늘어도
번들이 커지지 않아, 백엔드가 주는 kebab 이름을 PascalCase로 바꿔 붙이기만 하면 된다.
@fox 13.3MB → 6.4MB.
Co-Authored-By: Claude Opus 5
`grid-wrap-*`는 Figma 아트보드 폭이 그대로 들어온 값이다. `full`은 "부모 폭을 채운다"는 뜻인데
모바일 38rem(380px)·PC 192rem(1920px)으로 잡혀 있어 그보다 좁은 창에서는 넘치고 넓은 창에서는
덜 찬다. 두 모드 모두 `100%`로 바꾼다.
나머지 세 단계(default·sm·xsm)는 그대로 둔다 — 최대 폭 단계로는 쓸 수 있는 값이고, 유일한
소비처인 `_fox-snackbar.scss`도 `max-inline-size`로 쓰고 있다.
생성 파일만 고치면 다음 재export에서 지워지므로 변환기(`build-tokens.py`)에 `DEV_OVERRIDES`를
두고 거기서 치환한다. 시안 값을 CSS에서 그대로 쓸 수 없는 토큰이 더 나오면 같은 표에 붙인다.
Co-Authored-By: Claude Opus 5