백엔드가 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
기획 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
10MB 벽의 원인이 Server Action이 아니었다. **미들웨어(`proxy.ts`)를 지나는 요청은 본문이
10MiB에서 잘린다** — 30MB를 보내면 라우트에 10,485,472바이트만 도달하고, 같은 경로를 matcher에서
빼면 31,457,478바이트가 전부 도달한다(curl 측정). 잘려도 오류가 아니라 조용히 끝나서 업로드가
성공한 것처럼 실패한다. 그동안 `bodySizeLimit`을 올려도 낫지 않던 `Unexpected end of form`이
이것이었다.
그래서 영상은 Server Action(페이지 URL로 POST → 반드시 미들웨어 통과)을 쓰지 않고, matcher에서
제외한 `/contents/upload`로 브라우저가 직접 올린다. 라우트는 본문을 파싱하지 않고 백엔드로 그대로
흘려보낸다 — 여기서 버퍼링하면 같은 문제를 다시 만드는 셈이다. 토큰이 httpOnly 세션 안에만 있어
브라우저가 백엔드를 직접 부를 수 없으므로 이 중계가 필요하다.
인증은 느슨해지지 않는다. 미들웨어의 쿠키 확인은 원래 낙관적 체크이고 최종 판단은 DAL의
`verifySession()`이며, 이 라우트도 그것으로 시작한다(확인: 세션 없이 POST → 303 /login,
GET → 405로 라우트까지는 도달, 다른 경로는 그대로 307 /login).
상한은 백엔드 `multipartResolver.maxUploadSize`와 같은 100MB로 잡았다. 썸네일·게시판 첨부는
Server Action 경로 그대로라 10MB 가드를 유지한다.
확장자는 저장소 설정만으로 부족했다 — `Globals.fileUpload.Extensions`가 먼저 거른다.
`MODULE_VOD_CONTENT`가 mov·avi·mkv·webm·wmv를 허용해도 실제로 통과하는 것은 mp4뿐이라
두 목록의 교집합을 화면 accept와 사전 검증이 함께 본다(확인: 영상 칸 accept=".mp4").
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
콘텐츠관리는 게시판(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
상태를 프론트 상수 표(`WAIT`/`ING`/`DONE`)로 치환하고 있었다. 그 표를 지우고 공통코드
`BBS_ANS_CD`를 조회해 코드명으로 옮긴다 — 꾸미기 아이템 카테고리(`ITEM_CATE_CD`)와 같은 방식이다.
목록의 "상태" 열과 답변 팝업의 진행상태 선택지, Server Action의 허용 코드 검증이 모두 같은
응답을 본다. 페이지가 한 번 조회해 목록으로 내리고, Server Action은 저장 직전에 다시 조회한다
(화면이 보낸 코드를 그대로 믿지 않기 위해서다 — 아이템 카테고리와 같은 근거).
`ANSWER_STATUS_DONE`만 상수로 남는다. "답변완료일 때만 FO 노출·답변일 표시"라는 판단이 특정
코드값을 알아야 하는데 BBS_ANS_CD의 실제 값을 아직 확인하지 못해 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
두 가지가 어긋나 있었다.
**분류 필터가 탭이었다.** 시안은 `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
시안 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) A_BOA 목록 5227:5058 · 등록 팝업 5227:5848.
공지사항·1:1문의·FAQ 세 화면이 같은 뼈대를 쓴다.
stngId를 실제 값으로 바꿨다. 종전 TEMP_STNG_ID_* 임시값이라 목록이 늘 비어 있었다
(/api/v1/common/bbs/stng/list 응답, typeSe가 셋 다 'NOT'이라 구분에 쓸 수 없어 하드코딩).
목록 — FoxListContainer + 시안의 필터 줄(새로고침 아이콘 + 전체/노출/미노출 알약 탭).
구분은 FoxBadge(neutral), 사용여부는 FoxStatusIndicator, 첨부는 클립 아이콘, 관리는 테두리
아이콘 버튼. 세 화면이 열 정의만 다르고 나머지는 공유한다.
등록·수정 팝업 — FoxModal(md=560, 시안 폭)에 구분·제목·내용(FoxTextArea)·첨부파일
(FoxFileUpload default)·상단고정·게시기간·사용여부·앱푸쉬. 공지사항 전용 항목은 그 게시판에서만
그린다. 죽은 components/ui 대신 전부 @fox다.
옛 표·툴바·필터바 컴포넌트 5개는 지웠다(참조 0).
@fox: FoxTextArea에 requirement 추가 — 다른 폼 컴포넌트와 같은 규약.
TODO(백엔드): 구분(bbsCd)은 조회로만 오고 등록·수정 요청 VO에 필드가 없어 저장되지 않는다.
TODO: 게시기간이 네이티브 date 입력이다 — 시안은 YYYY.MM.DD 형식의 전용 필드이고 @fox에
날짜 선택 조각이 없다.
TODO: 목록 정렬이 백엔드 고정(등록일 오름차순)이라 "최근등록순" 셀렉트가 표시만 한다.
검증 — 시안 대조 실측: 모달 560px, 첨부 [파일선택] 버튼 100px 정상 노출, 목록 8열·필터 알약·
페이지네이션 정상.
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
## 원인
`FoxFileUpload`가 파일을 고른 **직후 네이티브 입력을 비웠다**.
const handlePicked = (event) => {
takeFiles(Array.from(event.target.files ?? []));
event.target.value = ""; // ← 같은 파일 재선택 때문에
};
`input.value = ""`는 `input.files`까지 비운다. 폼 제출에 실리는 것은 React 상태가 아니라 그
`files`이므로, `name`을 줘도 FormData에는 **빈 File(size 0)**만 남았다 — `name` prop 설명이
"주면 고른 파일이 폼 제출에 그대로 실린다"고 약속한 것과 정반대다. 끌어다 놓은 파일은 애초에
입력을 거치지 않아 마찬가지였다.
그래서 꾸미기 아이템 등록은 이미지를 아무리 골라도 Server Action이 파일을 못 받아
"썸네일 이미지를 등록해 주세요."만 반복했다.
## 고침
비우는 대신 **지금 고른 목록으로 입력을 덮는다**(`DataTransfer`). 두 가지가 함께 해결된다 —
제출에 실리고, 목록을 비웠을 때는 입력도 비어 같은 파일을 다시 고를 수 있다. 삭제도 같은 경로를
지나므로 썸네일을 지우면 입력에서도 빠진다.
확인: `name="imageFile"`인 FoxFileUpload를 폼에 넣고 파일을 고른 뒤 제출하니
`name=thumb.png size=1234`가 나온다(종전에는 `name= size=0`). 삭제 후 제출하면 다시 `size=0`이다.
## 곁들여
시안이 "JPG, PNG 파일만 가능 … 최대 5MB"라고 못박아 두었는데 화면이 그걸 재지 않았다. 백엔드
저장소 설정에도 확장자·크기 제한이 있지만 걸리면 "이미지를 업로드하지 못했습니다."라는 뭉뚱그린
문구만 남는다. 규칙과 안내문을 `decoration-item-form.ts` 한곳에 두고 업로드 직전에 재도록 했다.
Co-Authored-By: Claude Opus 5
앞자리 셀렉트를 비워 두면 placeholder "010"이 이미 고른 것처럼 보이는데 값은 빈 문자열이라,
사용자가 앞자리를 건드리지 않고 뒷자리만 채우면 8자리가 제출돼 "휴대전화 번호를 정확히 입력해
주세요."로 막혔다. 목록을 넘겨 고를 수 있게 한 것(a0316e6)만으로는 여전히 한 번 골라야 했다.
값을 화면이 소유하므로 초기값을 DEFAULT_MOBILE_PHONE_PREFIX('010')로 둔다 — 이제 placeholder가
아니라 실제 값이라 그대로 제출된다. 수정 팝업도 저장된 번호가 없으면 같은 기본값으로 시작한다.
검증 — 화면 실측: 팝업을 열자마자 제출값이 "010"(placeholder 아님)이고, 앞자리 셀렉트를 한 번도
건드리지 않고 1234·5678만 입력했을 때 제출값 "01012345678" → 서버 포맷 "010-1234-5678" 통과.
Co-Authored-By: Claude Opus 5
증상: 번호를 다 입력해도 "휴대전화 번호를 정확히 입력해 주세요."로 막힘.
`FoxPhoneNumber`의 `prefixOptions` 기본값이 빈 배열인데 호출부가 넘기지 않았다. 그래서 앞자리
셀렉트에 고를 항목이 하나도 없고, 그 자리에는 placeholder "010"이 떠서 이미 고른 것처럼 보인다.
실제 값은 빈 문자열이라 가운데·끝자리를 다 채워도 8자리만 제출되고, `formatPhoneNumber`의
`/^(\d{3})(\d{3,4})(\d{4})$/`에 걸리지 않아 null → 빈 값 → 위 오류로 떨어졌다.
앞자리 목록을 `lib/domain/phone-number.ts`의 MOBILE_PHONE_PREFIXES로 두고 호출부가 넘긴다 —
등록·수정이 같은 `AdminMemberFormFields`를 쓰므로 두 팝업이 함께 고쳐진다.
검증 — 화면 실측(실제 클릭·키 입력): 앞자리 셀렉트에 010/011/016/017/018/019가 뜨고, 010 선택
후 1234·5678 입력 시 제출값 "01012345678"(11자리) → 서버 포맷 "010-1234-5678" 통과.
수정 전에는 같은 조작에서 "12345678"(8자리) → null이었다.
@fox 관찰(보고만): `unit` 모드에서 `prefixOptions`를 주지 않으면 항목이 없는 셀렉트가 그려지고,
placeholder 때문에 고른 것처럼 보인 채 잘못된 값이 조용히 만들어진다. 기본값을 두거나 빈 목록일
때 앞자리를 입력 칸으로 바꾸는 편이 안전해 보인다.
Co-Authored-By: Claude Opus 5