mock 저장소를 제거하고 /api/v1/mngr/item CRUD와 파일 업로드 API에 연결한다. 기존 구현은
"이 도메인에는 백엔드 API가 전혀 없다"는 전제로 만들어져 있었으나, 백엔드에 등록·수정·삭제까지
갖춘 API가 존재해 스키마에 맞춰 도메인 타입부터 다시 맞췄다.
- 아이템ID는 사용자 입력이 아니라 백엔드 자동 채번(itemSn)이다. 시안도 "등록 후 자동발급
(Read Only)"이라 등록 폼의 입력 필드를 없애고 목록·수정 팝업에는 itemSn을 그대로 노출한다.
- 유형은 개별=N, 셋트=S로 매핑한다. 목록 조회는 searchItemType이 필수라 값이 없으면 아무것도
조회되지 않는다.
- 카테고리는 공통코드가 정비되기 전까지 프론트 임시 코드로 개발한다(사용자 지시). 표기는
백엔드가 조인해 준 이름을 우선 쓰고 없으면 임시 목록에서 찾는다.
- 이미지는 파일 하나만 받아 atchFileId에만 저장한다(썸네일은 추후 백엔드 자동생성 예정).
브라우저가 백엔드에 직접 올리지 않도록 Server Action이 File을 받아 중계한다. 목록 썸네일은
인증이 필요 없는 이미지 GET 주소를 서버에서 만들어 내려준다.
- 총건수 보정은 학생 목록과 동일하다(백엔드 PaginationUtil에 count 쿼리가 없다).
lib/http/backend-fetch.ts에 PUT/DELETE와 form·multipart 본문을 추가했다. 아이템 쓰기 API는
@RequestBody가 아니라 @ParameterObject로 받으므로 JSON을 보내면 전 필드가 null인 채 저장된다.
FoxInput·TextArea·PhoneNumber·Certified·Email·Address·FileUpload가 라벨·
헬퍼·입력 상자·상태 규칙을 같은 값으로 되풀이하고 있었다. 시안에서 이들이 모두
같은 공용 컴포넌트라 값이 갈라질 이유가 없다.
- @fox/styles/_form-field.scss 추가 — root·label·message·message-icon·
box·box-size·field·auto-states·fixed-states
- 믹스인만 있어 @use 해도 CSS가 늘지 않는다(파티셜 단독·묶음 모두 확인)
- 컴포넌트마다 다른 것만 인자로: 상자 gap·교차정렬, 알맹이 글자 크기·줄높이,
알맹이 element 이름(field/segment), 오류 판정 범위
- PhoneNumber unit은 조각이 다른 컴포넌트라 헬퍼 오류를 넓게 본다 —
$invalid-within으로 그대로 유지
값은 하나도 바꾸지 않았다. 컴파일 결과를 규칙 단위로 대조해 304개 규칙의
선택자·선언·순서가 모두 동일함을 확인했다(선언 순서만 5개 규칙에서 다르며
서로 다른 속성이라 렌더링 동일).
Co-Authored-By: Claude Opus 5
세로 배치의 교차정렬이 기본값 stretch라 버튼이 오른쪽 칸 폭(567px)만큼
늘어났다. 시안(txt-area)은 items-start이고 버튼은 shrink-0이다.
- align-items: flex-start 추가 — 버튼이 글자만큼만 차지한다
- 설명에 inline-size: 100% — 시안 min-w-full대로 폭을 채운다
Co-Authored-By: Claude Opus 5
`로 그리면서 브라우저 기본값을 지우지 않아 왼쪽에 40px
(padding-inline-start)과 위아래 10px(margin-block)이 붙었다. 우리 SCSS에는
`__list` 패딩 규칙이 없었고 전부 UA 스타일이었다.
같은 자리를 쓰는 FoxSelectOption은 초기화가 되어 있었다 — 이 파일만 빠져 있었다.
Co-Authored-By: Claude Opus 5
Figma 시안 다섯을 한 컴포넌트로 옮긴다 — file-upload(391:3677) ·
file-upload-area(392:5968) · file-upload-img(391:3732) · file-list(392:5938) ·
file-list-item(391:3509). 고르는 방식만 다르고 목록은 같아서다.
시안에서 버튼·입력이 모두 기존 컴포넌트의 인스턴스라 그대로 조합한다.
썸네일 삭제 버튼은 FoxIconButton overlay xsm이 시안과 정확히 같다(24px ·
모서리 4 · 검정 50% · 배경흐림 10 · 흰 아이콘).
업로드는 하지 않는다. 파일을 고르면 원본 File을 넘길 뿐이고 진행률·서버
오류를 목록에 어떻게 비출지는 files로 정한다. files를 안 주면 안에서 들고
있어 간단한 폼에 그대로 쓴다.
default 모드는 시안의 두 상태를 모두 갖는다 — 비었으면 흰 칸에
파일선택(secondary), 파일 하나를 목록 없이 들고 있으면 칸이 회색으로 잠기고
버튼이 삭제(default 계열 · 휴지통)로 바뀐다. 흰 칸 규칙은 FoxInput의 읽기
전용 규칙(0,6,0)보다 특이도를 높여 로드 순서와 무관하게 이기도록 적었다.
끌어다 놓기는 prop으로 켜되 area는 안내문이 그렇게 말하므로 언제나 받는다.
입력칸 폭과 썸네일 크기는 토큰에 없어 prop 기본값으로 뒀다(사용자 승인).
Co-Authored-By: Claude Opus 5
Figma form-email(136:3092)과 form-address(5346:5823)를 옮긴다. 두 시안 모두
상자가 input·select 인스턴스이고 버튼도 btn 인스턴스라 FoxInput·FoxSelect·
FoxButton을 그대로 조합했다 — 두 SCSS는 배치와 `@` 구분자만 갖는다.
FoxEmail
값은 아이디@도메인이 합쳐진 문자열 하나다. 도메인을 목록에서 고르면 옆 칸이
그 값으로 덮이고 읽기 전용이 되며, "직접입력"을 고르면 잠금만 풀린다.
확인 버튼은 onVerify를 넘겼을 때만 렌더된다.
칸은 시안의 240px을 최소폭으로 두고 남는 폭을 나눠 갖는다 — 좁아지면 줄이
넘어간다(사용자 승인). 토큰 스케일에 240이 없어 이 한 줄만 리터럴이다.
FoxAddress
우편번호와 주소는 검색 결과로만 채워지는 읽기 전용이고 상세주소만 입력한다.
어떤 우편번호 서비스를 쓰는지 @fox는 모른다 — 호출부가 onSearch에서
FoxAddressSearchResult(zipCode·address·선택 detail)로 바꿔 돌려주고, 취소는
null이다. detail을 안 넘기면 사용자가 적어 둔 상세주소를 건드리지 않는다.
Co-Authored-By: Claude Opus 5
Figma form-certified(190:2017)의 세 줄을 옮긴다. 전화번호는 FoxPhoneNumber
(combine), 두 버튼은 FoxButton(secondary·primary 전체폭)을 그대로 쓴다 —
시안도 그 인스턴스다. 인증번호 상자만 자체로 그리는데, 오른쪽 남은 시간이
FoxInput의 아이콘 슬롯(정사각 20px)보다 넓기 때문이다.
보내기·확인은 호출부가 Promise로 넘긴다. resolve면 성공, throw·reject면
실패로 보고 이벤트를 올린다 — onSendSuccess/onSendFail/onVerifySuccess/
onVerifyFail/onExpire.
화면은 최소한만 반응한다. 보내기가 성공해야 인증번호 칸과 확인 버튼이 열리고,
인증에 성공하면 전화번호·인증번호·두 버튼이 전부 잠긴다. 실패 문구는 호출부가
보여준다.
남은 시간은 remainingSeconds를 주면 그 값을 그리고, 없으면 expiresIn부터
컴포넌트가 센다. 0에 닿는 순간 onExpire를 한 번만 올린다.
Co-Authored-By: Claude Opus 5
칸에 들어오는 값에서 숫자가 아닌 글자를 걸러낸 뒤 자릿수를 자른다. 붙여넣기로
하이픈·공백이 섞여 와도 걸리고, 한 칸에 `010-1234`를 붙여도 넘치지 않는다.
밖에서 받은 value도 같은 방식으로 읽는다 — `010-1234-5678`처럼 꾸며진 값을
넣어도 칸이 맞게 갈리고, 내보낼 때는 숫자만 남는다.
Co-Authored-By: Claude Opus 5
Figma form-phonenumber(391:3156)의 두 모양을 옮긴다. unit은 상자 셋을
하이픈으로 잇고, combine은 상자 하나에 세 칸을 둔다.
값은 숫자만 담긴 문자열 하나다 — 하이픈은 화면에만 있고 값에는 없다. 화면의
세 칸이 진실 공급원이라 입력 도중 "0101"처럼 어디까지가 앞자리인지 알 수 없는
값이 생겨도 칸이 흔들리지 않고, 밖에서 value를 바꿨을 때만 다시 나눈다.
나눌 때 앞자리는 prefixOptions 중 가장 긴 일치를, 뒷자리는 항상 마지막 네
자리를 쓴다 — 010-123-4567과 010-1234-5678이 둘 다 맞게 갈린다. 목록이 없으면
maxLengths[0]만큼을 앞자리로 보므로 지역번호 02는 목록에 넣어야 한다.
unit의 조각은 시안에서도 select·input 인스턴스라 FoxSelect·FoxInput을 그대로
조합한다. 상태·포커스가 저절로 따라오고 이 파일은 줄 배치와 하이픈만 갖는다.
combine 상자만 자체 규칙을 둔다.
칸을 다 채우면 다음 칸으로 옮기고 빈 칸에서 지우면 앞칸으로 돌아간다
(autoAdvance로 끌 수 있다).
Co-Authored-By: Claude Opus 5
FoxTextArea와 별도로 들어온 FoxSelect 계열이 등록 목록 세 곳에서 부딪쳤다.
셋 다 추가만 하는 목록이라 양쪽을 합쳤다.
components.scss의 select 세 줄은 나머지와 같은 상대 경로 표기로 맞췄다 —
같은 폴더의 파티셜이라 결과는 같다.
component-registry.tsx의 FoxInput 임포트는 위쪽 배럴 임포트와 겹쳐 뺐다.
Figma textarea(135:1655)의 6상태를 옮긴다. 시안에 크기 축이 없어 상자는
하나뿐이고 높이는 rows가 정한다(기본 5줄, 시안과 같음).
상태 판정은 FoxInput과 같은 두 갈래다 — 기본은 브라우저가 판단하고(포커스·
값·네이티브 속성·aria-invalid), data-state를 주면 그 모양으로 고정된다.
상태 매트릭스도 FoxInput과 동일해 규칙을 그대로 옮겼다.
하단은 왼쪽 헬퍼와 오른쪽 글자수 카운터로 나뉜다. 카운터는 현재 길이만 굵고
색이 다르며, 오류일 때 빨개진다(시안의 error 변형이 100/100을 빨갛게 둔다).
maxLength가 없으면 개수만 적는다. 비제어 입력의 길이는 직접 따라간다.
리사이즈는 시안에 신호가 없어 폭이 흔들리지 않게 세로만 허용한다.
Co-Authored-By: Claude Opus 5
Figma input(135:621)의 2타입 × 3크기 × 6상태를 옮긴다. 네이티브 을
감싸므로 시안에 없는 password·email·number 등 모든 타입이 그대로 통한다.
상태는 두 갈래다. 기본은 브라우저가 판단한다 — 포커스는 :focus-within,
비활성·읽기전용은 네이티브 속성, 오류는 aria-invalid. 시안의 completed는
default와 글자색만 다른데 그건 플레이스홀더와 입력값의 네이티브 차이라 규칙이
필요 없다. 모양을 못박아야 하면 루트에 data-state를 준다.
date는 브라우저가 그리는 달력 아이콘과 피커를 그대로 쓴다(사용자 승인).
지우기 버튼은 포커스 중이고 값이 있을 때만 뜨고 누르면 값을 비운다 —
비제어 입력은 DOM을 직접 비우고 제어 입력은 onChange()로 알린다.
헬퍼 메시지 색은 상태를 따라간다(기본 회색 · 포커스 파랑 · 완료 초록 ·
오류 빨강). 아이콘은 @fox에 아이콘 세트가 없어 슬롯으로 두었다.
Co-Authored-By: Claude Opus 5
Figma btn-page(404:5121)의 네 변형을 옮긴다. 자식은 버튼 그룹이고 여럿이면
양끝, 하나면 가운데다.
자식 수를 React에서 세지 않고 CSS `:has(> :only-child)`로 판단한다. 프래그먼트나
조건부 렌더로 자식이 갈려도 실제 DOM이 기준이라 어긋나지 않고, React를 안 쓰는
소비자도 표시할 클래스가 없다.
패널이 자식에게 강제하는 두 가지(사용자 승인):
- 패널 안 그룹 간격은 gap-3(8px). 단독으로 쓰는 그룹의 gap-2(4px)는 그대로다.
- 모바일에서는 패널도 그룹도 세로로 쌓이고 버튼이 폭을 채운다.
시안이 패널 안 버튼에만 주는 min-width 160px은 변수 바인딩이 없고 토큰 스케일에도
160이 없어 리터럴로 둔다(사용자 승인). 상단 80px 여백은 spacing-top-2xlg이며
offset={false}로 끌 수 있다.
Co-Authored-By: Claude Opus 5
새 Figma 파일(KE9UILWhB8qTBe14uXUn4F)의 다섯 시안을 이전 파일과 대조했다.
노드 ID·변형 목록·토큰 값·구조가 전부 같아 스타일 변경분은 없다. 컴파일된
CSS는 바이트 단위로 이전과 동일하다.
btn 컴포넌트 세트 48변형 전체 대조 — 일치
btn-txt 생성 코드가 노드 ID까지 이전 파일과 동일
btn-lnk 생성 코드 바이트 비교 — 동일(에셋 URL만 재발급)
btn-ico 변수 세트·메타데이터 + ghost/overlay/disabled 대표 변형 — 일치
toggle default·ghost·overlay의 active 3종 + 아이콘 fill — 일치
btn-txt의 아이콘 크기는 컴포넌트 세트가 여전히 리터럴이다. 다만 전시 섹션의
인스턴스가 icon/4·3·2를 쓰고 있어 우리 매핑과 의도가 같음을 확인했고 그
근거를 주석에 남긴다.
Co-Authored-By: Claude Opus 5
$FOX_TOKENS_SRC를 새 export로 바꿔 변환기를 재실행했다. 토큰의 개수와
이름은 그대로이고 값 세 자리만 바뀐다.
spacing-bottom-sm 1.2rem → 1rem (PC·모바일 공통)
grid-wrap-default 158.4rem → 155.2rem (PC만, 모바일 34.8rem 유지)
변환기가 함께 생성하는 _root/_functions/_mixins/abstracts/index는 손으로
줄여 둔 주석이 되살아나 되돌렸다 — 이 파일들은 토큰 맵을 순회할 뿐이라
값 변화의 영향을 받지 않는다.
원본 JSON 전량 대조: responsive 150 · size 55 · number 23 · primitive 색 69 ·
theme 33 · light/dark 271쌍 · chart-alt 29 모두 일치.
Co-Authored-By: Claude Opus 5
Figma btn-group(398:4854)에서 가져온 건 간격뿐이다 — 크기·방향과 무관하게
gap-2(4px) 하나여서 그룹에 크기별 스타일이 없다. 배치 규칙(방향·정렬·줄바꿈)은
지시받은 사양이다.
크기는 그룹이 단일 진실 공급원이다. 자식을 cloneElement로 덮어써서 자식이
저마다 다른 size를 줘도 그룹 값으로 통일된다. 세로일 때는 fullWidth까지 함께
내려주고 CSS도 stretch라, React를 안 쓰는 소비자도 같은 결과를 얻는다.
Children.map이 프래그먼트 안으로 들어가지 않아 직접 내려간다. 그러지 않으면
<>…로 감싼 버튼이 덮어쓰기를 피해 가고 Fragment에 size가 붙어 경고가 난다.
호스트 요소(div·span)는 DOM에 없는 속성이 넘어가지 않도록 건드리지 않는다.
Co-Authored-By: Claude Opus 5
시안이 지정한 text-underline-position/text-decoration-thickness의
`from-font`를 옮기지 않아 브라우저 기본값 `auto`로 그려졌다. auto는 밑줄을
베이스라인 바로 아래에 붙여 g·y·다 같은 내림획을 관통하고, skip-ink가 그
자리를 파내 밑줄이 끊겨 보인다. lg에서 글자가 커질수록 두드러진다.
단축 `text-decoration`은 thickness를 auto로 되돌리므로 계열·상태 규칙은
`text-decoration-line`만 쓴다.
Co-Authored-By: Claude Opus 5