**FoxPhoneNumber** — `unit` 모드에서 `prefixOptions`가 비면 앞자리를 셀렉트가 아니라 입력 칸으로
그린다. 항목 없는 셀렉트는 placeholder "010"이 이미 고른 값처럼 보이는데 실제 값은 비어 있어,
뒷자리만 채운 번호가 조용히 만들어졌다(관리자 등록이 막힌 원인). 세 칸을 한 map으로 돌게
정리하면서 combine 분기와 모양을 맞췄다.
**FoxEmail** — 시안(form-email 5543:9940)은 줄이 나뉜다: [아이디] @ [도메인] 한 줄, 그 아래
도메인 드롭다운, 그 아래 확인 버튼. 종전에는 넷을 한 줄에 두고 넘치면 접히게 했는데, 좁은 곳에서
드롭다운이 도메인 칸보다 먼저 접혀 순서가 어긋났다(320px 팝업에서 세 줄로 흩어졌다).
`__body`(4px 간격) 안에 form-group·select·btn을 쌓고, 칸의 `min-inline-size: 24rem`을 0으로 바꿔
좁은 폭에서도 두 칸이 나눠 갖게 했다. 라벨의 필수 표시(`requirement`)도 다른 폼 컴포넌트와 같은
규약으로 추가했다.
**관리자 등록 팝업** — 손으로 짜 두었던 이메일 칸(아이디·도메인·직접입력 상태 세 개)을 `FoxEmail`
하나로 바꿨다. 시안 5227:2036 그대로이고, 아이디 placeholder는 "ID"(종전 "1234-5678"은 전화번호
칸에서 복사된 값이었다), 도메인은 첫 항목이 골라진 채로 시작한다.
검증 — 시안 대조 실측: 아이디 149×40 · 도메인 149×40(readOnly, "naver.com") · 셀렉트 320×40 ·
그룹↔셀렉트 간격 4 · 라벨 "이메일*"(시안 149/149/320/4와 일치).
동작: 직접입력 고르면 도메인 칸의 readOnly가 풀린다. prefixOptions 없이 쓴 FoxPhoneNumber는
앞자리가 입력 칸이 되고 "02"를 적으면 값에 실린다(종전에는 아무리 눌러도 빈 값이었다).
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
기획 SYS_COD_001(1842:17507)과 팝업 4종(1842:16056·16122·16179·16262),
시안 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) 5402:13399.
한 화면에 목록이 둘이다. 좌측 공통코드를 행 전체로 눌러 고르면 우측 상세코드가 그 그룹으로
갱신된다(기획 ④). 선택·검색·두 목록의 페이지가 모두 URL에 실려, 갱신은 서버가 다시 그린다 —
공통코드를 바꾸면 상세 페이지가 1로 돌아가는 규칙은 buildCommonCodeHref가 강제한다(호출부가
잊어도 어긋나지 않게).
**검색·페이징을 서버(RSC)에서 한다.** 백엔드가 두 목록 모두 전체를 반환하고 그룹 검색도
LIKE가 아닌 완전일치라, 부분일치 필터와 페이지 자르기를 page.tsx가 맡는다(사용자 확정 사항).
전체를 받으므로 총건수는 정확해 다른 목록의 하한값 보정이 여기엔 필요 없다.
URL의 comCd가 목록에 없으면 첫 행으로 대체한다 — 검색으로 걸러졌거나 삭제된 코드가 주소에
남아 있으면 우측이 영영 비어 보인다.
분류코드(시안의 CMS 셀렉트)는 저장되지 않는다. TB_SYS_COM_CD에 해당 컬럼이 없어(관리자용·
공용 두 매퍼와 후보 컬럼명까지 확인), 코드ID 접두사를 채워 주는 입력 보조로만 쓴다 — 시안
목록이 전부 CMS+3자리라 그 작명 규칙을 화면이 거드는 것이다(사용자 확정 사항).
설명 열은 한 줄 말줄임이다. 시안 셀에는 말줄임 스타일이 없지만(전문 + word-break) 프레임이
44로 고정이라 Figma가 넘침을 잘라 보여준다 — CSS는 감싸므로 그대로 두면 자유 입력인 설명
때문에 행 높이가 제각각이 된다. 잘린 글자는 title로 남긴다.
@fox: FoxHeadingGroup 신설(시안 3002:7177). 화면에 여럿 설 수 있는 구역 제목이라
FoxPageHeader(h1·breadcrumb)와 역할이 다르고, 제목 태그를 고를 수 있다. 아이템 목록 시안에도
숨김으로 들어 있어 재사용된다.
백엔드 요청 2건(시안대로 두고 보고, 사용자 확정 사항)
- 상세 목록 조회 SQL이 DTL_CD_EXPLN을 select하지 않아 상세코드설명이 늘 비어 온다.
- 상세 등록 INSERT에도 없어 등록 시 입력한 설명이 저장되지 않는다(수정은 정상).
검증 — 시안 대조 실측: 패널 2단 gap 40, 열 폭 공통 80/120/160/156/120/120 · 상세
80/120/120/196/120/120, 행 높이 44(+테두리 1), 검색 320, 패널 제목 26px #1e2124 h2.
동작: 분류코드 SYS 선택 시 코드ID 004→SYS004, 다시 CMS 선택 시 CMS004(접두사 교체),
상세코드 등록 팝업의 코드ID는 읽기 전용·name 없음(제출 제외)에 hidden comCd로 전달,
정렬번호 기본값 11(기존 10개 다음).
Co-Authored-By: Claude Opus 5
두 변경이 등록 팝업의 같은 파일에 겹쳐 한 커밋으로 둔다.
## 중복 확인 실패 사유
확인 호출이 백엔드에서 깨지면 `isAdminLoginIdTaken`이 던진 예외가 클라이언트 transition의
처리되지 않은 rejection으로 사라졌다 — 버튼만 원복되고 아무 말도 남지 않아 "눌러도 아무 일이
없는" 상태로 보였다. 확인 결과에 `failed`를 더해 **값의 판정(unavailable)과 확인 실패(failed)를
구분**하고, 백엔드 메시지를 ID 칸 아래 같은 자리에 그대로 띄운다.
## @fox/core/validation
Angular `Validators`의 계약을 옮겼다 — 검증기는 값을 받아 통과면 `null`, 아니면 **오류 객체**를
돌려주는 순수 함수이고, 키가 오류 이름, 값이 문구를 만들 맥락이다
(`{ minlength: { requiredLength, actualLength } }`). 인자가 `AbstractControl`이 아니라 문자열인
것만 다르다 — @fox에는 폼 모델이 없고 검증 대상이 칸의 값 하나뿐이다.
`required`를 뺀 검증기는 빈 값을 통과시키고(Angular와 같다), `compose`는 오류를 **병합**해
"10자 이상"과 "2종류 이상"을 함께 판정할 수 있게 한다.
**문구는 검증기가 갖지 않는다**(Angular와 같은 이유 — 같은 규칙도 자리마다 다른 말이 필요하다).
`foxValidationMessage(errors, overrides)`가 옮기고, 한 번에 한 문구만 낸다.
비밀번호처럼 규칙이 여러 개인 칸을 위해 `foxPasswordValidator(policy)`를 둔다 — 길이·조합에
더해 같은 문자 연속·잇따르는 문자·특정 문자열 포함 금지를 옵션으로 받는다.
## FoxInput 연결
`validators` · `validationMessages` · `validateOn` · `onValidationChange`.
- `message`는 평소 헬퍼이고 검증이 걸리면 그 자리에 사유가 들어간다. 호출부가 `invalid`를 직접
켜면 `message`가 이긴다 — 서버가 준 사유(중복·권한)는 화면 규칙이 알 수 없어 덮이면 안 된다.
- 오류는 **한 번 포커스를 벗어난 뒤부터** 보이고 그 뒤로는 입력할 때마다 갱신된다
(Angular의 `touched`. `updateOn` 기본값만 다르다 — 한 글자에 "10자 이상"이 뜨면 방해가 된다).
## 사용
비밀번호 규칙을 `ADMIN_PASSWORD_POLICY` 한 곳으로 모아 **화면과 Server Action이 같은 값을 본다**
— 한쪽만 고치면 화면은 통과시키고 저장은 거부하는 상태가 된다. 등록은 `required: true`,
수정은 비우면 "바꾸지 않음"이라 걸지 않는다.
Co-Authored-By: Claude Opus 5
목록 셀(80×80)과 등록·수정 팝업의 업로드 미리보기(120×120) 모두. 아이템은 생김새 자체가
정보라 잘라내면 어떤 아이템인지 알아볼 수 없고, 미리보기는 고른 그림을 확인하는 자리라
일부만 보이면 확인이 되지 않는다.
@fox: `.fox-file-upload__thumb-image`의 object-fit. FoxFileUpload의 image 모드를 쓰는 화면이
이 팝업뿐이라 영향 범위가 이 화면 안이다.
Co-Authored-By: Claude Opus 5
둘 다 시안 ADM_ADM_102_p(5227:2035)가 요구하는데 없던 것이다.
- `FoxInput`에 `requirement`를 열었다. 라벨을 FoxFormLabel로 그리면서 그 값을 넘기지 않아
`이름*`처럼 필수 별표가 붙은 시안을 그릴 수 없었다. 이미 같은 prop을 가진 `FoxSelect`와
같은 모양으로 맞췄다.
- `FoxChipArea`에 `wrap`을 열었다. 스타일 주석이 "칩이 여러 줄로 흘러야 하는 자리가 나오면
시안에 먼저 추가한다"고 적어 둔 그 자리다 — 시안의 "메뉴 선택"이 칩을 두 줄로 감는다.
기본값은 종전대로 한 줄이라 기존 호출부는 그대로다. `FoxButtonGroup`의 같은 prop과 맞췄다.
Co-Authored-By: Claude Opus 5
시안(관리자페이지 5199:9035의 search > btn, 노드 5199:9029)은 배경이 있는 secondary 계열
버튼인데 투명 배경에 radius 4로 그리고 있었다.
시안 bg button/secondary-surface(#ecf2fe) · radius form/radius/md(8) · 32×32 · 아이콘 20
종전 투명 · radius form/radius/xsm(4)
배경·radius·아이콘 색을 시안 토큰으로 맞추고, 배경이 생긴 만큼 hover·pressed·disabled도
button-secondary 계열로 함께 정의했다(종전에는 색만 흐려졌다). 크기 32와 아이콘 20은 이미
맞아서 그대로다.
검증 — PC 폭에서 배경 rgb(236,242,254)=#ecf2fe, radius 8px, 32×32, 아이콘 20×20,
아이콘색 rgb(11,80,208)=#0b50d0으로 시안과 일치한다.
Co-Authored-By: Claude Opus 5
충돌 2건은 양쪽이 같은 자리에 서로 다른 것을 **더하기만** 한 경우라 합집합으로 풀었다.
- styles/components.scss: hub의 파티셜 목록에 fox-status-indicator·fox-toast를 더했다.
- dev-test/component-registry.tsx: 데모 함수(hub의 ModalDemo·DemoThumb + origin/develop의
ToastDemo)와 카탈로그 항목(45 공통 + hub 5 + origin/develop 6 = 56)을 모두 남겼다.
git이 두 삽입의 공통 꼬리(`…}`)를 맞물려 놓아 마커 그대로는 함수가 끊긴다 — 그래서
마커를 고치지 않고 양쪽 원본에서 함수·항목 단위로 다시 세웠다.
검증: tsc·eslint 무오류, next build 성공(/dev-test/design이 빌드 시 프리렌더되므로 병합된
레지스트리가 실제로 렌더되는 것까지 확인).
Co-Authored-By: Claude Opus 5
FoxHelperText 커밋(83acf5b)에서 `_fox-helper-text.scss`는
`@include field.message($display: inline-flex)`로 부르는데, 믹스인 쪽 변경이
함께 올라가지 않아 인자를 받지 못하는 상태였다. 받은 트리에서 sass가
`No argument named $display`로 실패한다.
기본값이 `flex`라 이 믹스인을 부르는 폼 다섯 곳의 출력은 종전과 같다.
Co-Authored-By: Claude Opus 5 (1M context)
FoxListContainer가 FoxListSearch에 검색어·검색 대상을 제어 값으로만 넘기고 변경 핸들러를
넘기지 않아, 입력창은 값이 고정되고(FoxListSearch가 controlled로 판정) 검색 대상 셀렉트도
스스로 값을 바꾸지 않았다(FoxSelectText의 같은 규칙). 즉 검색 자체가 불가능했다.
넘어오는 keyword·searchField는 이미 **적용된** 조건이라 입력 중인 값과 역할이 다르다.
컨테이너가 입력 초안을 따로 들고, 적용된 조건이 바뀌면 초안을 그 값으로 되돌린다 —
뒤로가기처럼 주소가 검색어를 바꾸는 경로에서도 입력창이 따라온다.
학생 회원 목록이 같은 증상이었고 이 수정으로 함께 낫는다.
Co-Authored-By: Claude Opus 5