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
시안에 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
기획 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
`Cannot read properties of undefined (reading 'stack')`의 출처가 확인됐다. 우리 코드가 아니라
콘솔을 계측하는 개발 도구다 — VS Code 확장 Console Ninja가 소스의 `console.error`를
`oo_tx(...)`로 감싸는데, 그 안에서 인자 배열을 훑다가 `undefined`를 만나면 `.stack`을 읽고
터진다(사용자 콘솔 스택으로 확인: `communicationError (backend-fetch.ts:118)` → `console.eval`
→ `Array.find` → console-ninja).
그래서 `communicationError`가 값을 돌려주지 못하고 예외를 던졌고, `backendFetch`는
`{ok:false}` 대신 예외로 끝났다. 없는 엔드포인트(`POST /api/v1/mngr/quiz`)를 부르면 반드시
지나는 `예상치 못한 HTTP 상태` 분기가 `detail`로 `undefined`를 넘기고 있었다.
`detail`을 선택 인자로 바꾸고 없을 때는 아예 넘기지 않는다. 세 군데 호출부에서 `undefined`
인자를 걷어냈다. 계측 도구가 없어도 `console.error(msg, undefined)`는 로그에 의미 없는
`undefined`만 남기므로 어느 쪽이든 맞는 형태다.
Co-Authored-By: Claude Opus 5
`failure()`가 잡힌 예외의 `message`를 그대로 화면에 실었다. 그래서 코드 오류(TypeError 등)가
나면 "Cannot read properties of undefined (reading 'stack')" 같은 개발자용 문구가 폼 아래에
그대로 뜨고, 정작 스택은 아무 데도 남지 않아 원인을 좁힐 수 없었다.
이제 `runWrite`가 잡은 예외를 서버 콘솔에 그대로 찍고(`[contents] 저장 실패`), 화면에는 사람이
고칠 수 있는 사유만 낸다 — 백엔드가 돌려준 메시지(`BackendRequestError`)와 용량·확장자 같은
입력 오류(`ContentInputError`)뿐이고 나머지는 일반 문구로 바꾼다.
확인차 재현을 시도한 것: Server Action 안에서 `console.error(msg, undefined)`는 던지지 않고,
`POST /api/v1/mngr/quiz`(없는 엔드포인트)와 코드표 조회 실패는 각각 깨끗한 `BackendRequestError`로
돌아온다. 즉 저 문구를 만든 예외는 이 경로들이 아니며, 로그가 붙었으니 다음 실패에서 드러난다.
확장자 안내도 저장소 목록이 아니라 전역 화이트리스트와의 교집합을 말하도록 맞췄다 — mov를
올릴 수 없는데 "mov 파일만 올릴 수 있습니다"라고 안내하고 있었다.
Co-Authored-By: Claude Opus 5