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
디자인팀 요구다. 클래스명 하나로 쓰고 이름은 Figma의 `ico/House`를 대소문자까지
그대로 담는다. 굵기별 @font-face가 따로라 화면이 쓰는 굵기만 내려받는다.
기존 SVG 컴포넌트에서 뽑은 path로 구워 그림이 같다는 것이 데이터 수준에서 보장된다
(글리프 대조 결과 불일치 0.97%, 전부 외곽선 안티에일리어싱).
duotone은 글리프 하나가 두 색을 못 가져 배경겹을 별도 글리프로 넣고 CSS가 겹친다.
Co-Authored-By: Claude Opus 5
INSERT에 DEFAULT_YN이 들어가는데 화면에 없는 항목이라 아무도 보내지 않아 NULL이
박히고 있었다. 등록에는 'N'을 싣는다.
아이콘을 고르지 않았을 때 빈 문자열을 보내고 있었다. 파일 테이블을 가리키는 컬럼이라
등록에서는 값이 있을 때만 싣고, 수정에서는 지우기를 위해 빈 문자열을 유지한다.
실패하면 코드·메시지·보낸 파라미터를 서버 콘솔에 남긴다 — 백엔드 로그를 볼 수 없는
환경이라 이게 유일한 단서다.
Co-Authored-By: Claude Opus 5
백엔드가 success:false 봉투로 사유를 주는데("최상위 메뉴가 없습니다" 등) 액션이
전부 "저장하지 못했습니다"로 뭉개고 있었다. 통신 오류만 일반 문구로 남긴다.
menuIdPath는 조상 경로가 되도록 부모 경로 + 부모 id로 만든다. 백엔드가 값을 만들어
주지 않고 저장만 한다.
Co-Authored-By: Claude Opus 5
유형을 프론트 상수 표(SIGNUP/PAYMENT/…)로 치환하고 있었다. 그 표를 지우고 공통코드
`BBS_FAQ_CD`를 조회해 상세코드명으로 옮긴다 — 1:1문의 진행상태(BBS_ANS_CD), 학교·학년
(COM_SCHUL_CD·GRD_CD)과 같은 방식이다.
목록의 「유형」 열과 등록·수정 팝업의 유형 선택지, Server Action의 허용 코드 검증이 모두 같은
응답을 본다. 페이지가 한 번 조회해 목록으로 내리고, Server Action은 저장 직전에 다시 조회한다
(화면이 보낸 코드를 그대로 믿지 않기 위해서다).
목록 URL의 `type` 필터만 예외로 뒀다 — `parseBoardPostQuery`는 도메인 계층이라 코드 조회를 할 수
없어 FAQ에서는 값을 그대로 받는다. 모르는 코드면 결과가 비는 것으로 끝나고, 저장 검증은 위에서
코드표로 한다.
코드가 한 건도 없으면 유형 칸을 그리지 않고 검증도 요구하지 않는다 — 빈 셀렉트를 보여 주고
"유형을 선택해 주세요"로 막으면 저장할 방법이 없어진다.
확인: 임시 페이지로 목록의 FAQ01→회원가입&로그인 치환, 표에 없는 코드(ZZZ99)는 코드 그대로
노출, 등록 팝업 선택지가 코드명으로 뜨고 고르면 hidden 필드에 코드값(FAQ01)이 실리는 것을
확인한 뒤 지웠다.
Co-Authored-By: Claude Opus 5