Figma 표 시안 9종(tbl-basic·tbl-list·tbl-form과 각각의 cell-head·cell-body)을
FoxTable / FoxTableRow / FoxTableCell 셋으로 옮겼다. 세 타입이 뼈대가 같고 여백·글자·
기본 정렬만 달라 한 벌로 충분하다.
진짜
이라 스크린리더가 행·열을 읽어 주고 colSpan·rowSpan이 그대로 동작한다.
본문 행의 제목 칸은 그 줄의 이름이라 scope="row", 머리행 칸은 열 이름이라 scope="col"이
붙는다. 호출부는 행을 늘어놓기만 하면 되고 head 여부로 thead·tbody가 갈린다.
타입 차이를 클래스가 아니라 커스텀 프로퍼티로 내려보낸다. `.fox-table--basic
.fox-table-cell`처럼 후손 선택자를 쓰면 표 안에 표가 들어갔을 때 바깥 규칙이 안쪽까지
덮어쓴다 — 글머리 목록에서 겪은 것과 같은 문제다. 커스텀 프로퍼티는 안쪽 표가 자기
값으로 다시 덮으므로 그 일이 생기지 않는다.
좁은 화면 대응도 시안대로 넣었다. list는 머리행을 감추고 한 줄이 카드로 접히며, 이때
열 이름을 알 수 없으므로 칸이 label을 받아 글자로 보여 준다. form은 레이블이 입력 위로
올라가고 레이블 칸의 회색 바탕이 사라진다. basic은 시안에 모바일 변형이 없어 그대로 둔다.
승인된 비토큰 값: form 머리칸 최소 크기 12rem×4.8rem.
검증: 여백·바탕·글자·정렬 18항목이 시안과 일치(나머지 1건은 내 예상값 오류였고 코드가
맞다 — font/weight/medium은 SemiBold 600이다). 모바일에서 list 카드 접힘·열 이름 노출,
form 위아래 쌓임, basic 유지를 확인했다. sass 524규칙 · tsc · eslint 통과.
Co-Authored-By: Claude Opus 5
Figma badge(391:4804) · badge-number(392:4810) · badge-dot(392:4816) ·
badge-group(392:4848)을 FoxBadge / FoxBadgeDot / FoxBadgeGroup으로 옮겼다.
badge-number와 badge-dot을 합친 이유는 둘 다 알림 표시이고 색 축(primary·new)이
같으며 개수를 아느냐 모르느냐만 다르기 때문이다. count를 주면 숫자, 없으면 점이다.
색 9종 × 계열 3종 × 크기 3종은 색마다 쓰는 토큰이 규칙적이지 않아 SCSS 맵에 모아
한 번에 펼친다 — 손으로 27벌을 적으면 어긋난다. theme 계열은 solid에도 `-strong`을
쓰고, system 계열은 테두리가 `-strong`·solid가 기본값이며, warning만 solid 글자가
검정이고, disabled는 pastel도 solid와 같은 색이다(모두 시안 확인).
테두리를 border가 아니라 안쪽 그림자로 그린다. 시안에서 테두리가 크기를 늘리지 않아
outline과 solid의 높이가 같은데, CSS border는 상자를 키워 sm이 20px 대신 21px이
된다. 굵기는 그대로 border/1 토큰을 쓴다.
FoxBadgeGroup의 size는 자식 배지를 덮어쓴다(FoxButtonGroup과 같은 방식).
승인된 비토큰 값: 최소 높이 2.4rem(md)·2rem(sm), 점 0.6rem, 숫자 배지 2rem —
모두 시안 값이나 Figma가 변수로 export하지 않는다.
검증: 색·계열 17조합의 바탕·글자·테두리 색이 시안 토큰과 정확히 일치. 크기 3종의
높이 31/24/20과 글자 15/13/11이 일치하며 계열이 달라도 높이가 같다. 숫자 배지는
자릿수가 늘어도 높이가 20으로 고정된다. sass 498규칙 · tsc · eslint 통과.
Co-Authored-By: Claude Opus 5
2단계 줄에 `-`와 점이 함께 나왔다. 항목이 항목을 품는 구조인데 단계별 규칙을 후손
선택자로 적어, `.--lv1 .__marker`가 그 안에 중첩된 2·3단계 마커에도 걸렸다.
단계별 규칙을 자식 결합자로 바꿔 자기 줄만 고르게 했다.
`.--lv1 > .__row > .__marker`
카탈로그의 34개 항목을 전부 훑어 글자와 도형이 함께 있는 줄이 없음을 확인했고,
여섯 조합이 시안대로 하나씩만 나온다 — 점 6px / `-` / 빈 원 4px / `1.` / `a.` / `①`.
시안 대조 20항목도 그대로 통과한다.
Co-Authored-By: Claude Opus 5
Figma bullet(391:4049) · bullet-list-item(391:4081) · bullet-list(484:5722)를
FoxBulletList / FoxBulletListItem 둘로 옮겼다.
bullet을 따로 두지 않은 이유는 마커가 목록 밖에서 홀로 쓰이는 자리가 없고, 모양이
전부 단계·번호 여부로 갈리기 때문이다. 그 두 축이 항목의 수식어로 들어가 있다.
호출부는 단계를 평평하게 늘어놓고 level만 적는다. 어느 항목 안에 무엇을 넣을지는
목록이 정한다.
레벨1레벨2
시안은 2·3단계를 형제 프레임으로 두고 왼쪽 여백만 20·40px 줬지만, 나가는 마크업은
/ 안에
/이 든 진짜 중첩이다 — 목록의 단계를 스크린리더에 알리려면
그래야 한다. 한 단계마다 20px씩 들어가 3단계가 40px이 되므로 서는 자리는 시안과 같다.
앞에 걸어 둘 항목이 없는 경우(첫 줄부터 2단계)는 빈
에 매달아 목록을 유효하게 둔다.
번호는 같은 묶음 안에서 1부터 다시 센다. 시안에 없는 범위는 z 다음 aa, ⑳ 다음 (21)로
잇고, 다르게 매겨야 하면 marker 프롭으로 덮는다.
승인된 비토큰 값: 마커 칸 2.2×2.6rem · 1.6×2.3rem · 2.4×2.3rem, 채운 원 0.6rem,
빈 원 0.4rem — 모두 시안 값이나 Figma가 변수로 export하지 않는다.
검증: 시안 대조 20항목 전부 일치(칸 크기·마커 모양·글자 크기·단계별 들여쓰기
0/20/40px). 목록의 자식이 li뿐이고 중첩 목록은 모두 li 안에 있다.
sass 463규칙 · tsc --noEmit · eslint 통과.
Co-Authored-By: Claude Opus 5
develop에서 온 FoxFormLabel과 내가 두었던 form-field의 label 믹스인이 같은 일을
두 갈래로 하고 있었다. 라벨 모양을 아는 곳을 하나로 줄인다.
- FoxFormLabel에 `as` 프롭 추가 — 칸 하나를 가리킬 때는 label, 칸 여러 개를 묶은
필드에서는 span. 가리킬 대상이 하나가 아니라 htmlFor를 쓸 수 없고, 아무것도
가리키지 않는
FoxFormLabel · FoxRadio(+Icon·Group) · FoxConditionalOption · FoxCheckbox(+Icon·Group)
· FoxTag(+List) · FoxToggleSwitch가 들어왔다.
충돌 2건은 양쪽을 모두 살리는 union으로 해소했다 — 둘 다 "컴포넌트를 만들 때마다
항목 하나를 추가하는" 등록 파일이라 서로 지울 이유가 없다.
- @fox/styles/components.scss: 새 파티셜 10개 + Navigation 파티셜 5개
- @fox/dev-test/component-registry.tsx: 카탈로그 항목 32개
검증: sass 컴파일 450규칙 · tsc --noEmit · eslint 모두 통과.
Co-Authored-By: Claude Opus 5
시안의 `ico/*`는 Phosphor Icons를 그대로 올린 것이다. `ico/House` Regular와 Phosphor
`house.svg`는 문 너비 16.67%·문 span 25%·벽 두께 8.33%가 전부 같고, 프레임 구조도
`Format=Stroke|Outline × Weight=Thin|Light|Regular|Bold|Fill|Duotone`으로 Phosphor의
weight 체계 그대로다. 그래서 Figma를 1,512번 호출하는 대신 원본에서 생성했다.
- @fox/tools/build-icons.py 신설. build-tokens.py와 같은 자리·같은 철학(Figma가 SSOT이고
생성물은 재현 가능)이다. 실행 1.4초.
- core/icons/glyphs/*.tsx 1,512개 + fox-icon.tsx(공용 껍데기) + index.ts 배럴.
기존 icons.tsx(8종 수기 이식)는 이 안에 포함되므로 삭제했다.
- 굵기를 이름이 아니라 `weight` prop으로 바꿨다. 시안의 `Weight=` 변형과 1:1이다.
FoxCaretLeftBoldIcon → .
- viewBox를 Phosphor 원본의 256으로 통일했다. 기존 8종은 Figma export라 16/24 박스에
translate가 붙어 있었지만 같은 그림을 축소한 것이다(House bbox x=32 → 32/256×16=2로
기존 translate와 일치).
검증 — 기존 8종과 새 생성물을 256×256으로 래스터화해 픽셀 비교했다. 차이가 잉크의 0.33%
이하(안티에일리어싱 경계)였고 Pause는 0픽셀이었다. weight 매핑이 유일한지도 오답을 넣어
확인했다: Play를 regular로 두면 58.4%, bold면 60.7%, Plus를 regular로 두면 34.0% 어긋난다.
즉 시안의 Play·Pause는 fill, Plus는 bold다 — 이름에 표시가 없어 놓치기 쉬운 지점이라
fox-carousel-button 주석과 README에 명시했다.
번들 — 배럴이 크지만 프로덕션에는 쓰는 글리프만 들어간다(안 쓰는 아이콘의 path 문자열이
프로덕션 .js 어디에도 없음을 확인). build 6초, tsc 3초로 영향 없다.
라이선스 — Phosphor는 MIT다. 사본에 고지를 함께 두도록 요구하므로 생성기가 원본 LICENSE를
core/icons/PHOSPHOR-LICENSE.txt로 옮긴다.
Co-Authored-By: Claude Opus 5
@fox 토큰이 패밀리명을 리터럴 `Pretendard`로 지정하는데 프로젝트 어디에도 그 이름의 폰트
공급이 없었다. 그래서 모든 @fox 컴포넌트가 OS 기본 서체(맥 Apple SD Gothic Neo / 윈도
맑은 고딕)로 갈라져 그려졌고, 글자 폭이 시안과 어긋났다.
- public/fonts: Pretendard v1.3.9 woff2 400/600/800 반입(OFL 1.1). 토큰이 쓰는 weight가
정확히 이 셋이다 — 특히 800은 wantedSans에 없어 가짜 굵기로 합성되고 있었다.
- globals.scss: @font-face로 직접 선언. next/font/local은 패밀리명을 해시로 바꿔 토큰의
리터럴과 만나지 못해 이 용도로는 쓸 수 없다.
- globals.scss: body에 `--app-font-sans` 연결. 이 계약 변수를 읽는 곳이 없어 wantedSans가
로드만 되고 어디에도 적용되지 않았다. 이제 @fox 밖 텍스트가 wantedSans를 쓴다.
- next.config.ts: `/fonts/*`를 no-store에서 제외. public/ 자산이라 그대로 두면 페이지를
열 때마다 2.2MB를 다시 받는다.
- proxy.ts: 폰트 확장자를 matcher에서 제외. 세션 없는 방문자의 폰트 요청이 `/login`으로
리다이렉트돼 로그인 화면만 대체 서체로 그려졌다.
- @fox/README.md: "호스트 앱과의 계약"이 @fox 컴포넌트도 `--app-font-sans`를 읽는 것처럼
적혀 있었으나 사실이 아니다. 실제 요구사항(Pretendard @font-face·400/600/800)으로 교체.
FoxCarouselIndicator number형 56.92px → 59.69px. 시안 62px과의 잔여 2.31px은 Figma가 텍스트
상자 폭을 정수로 올리고(+1.57) 마지막 글자의 음수 자간을 빼지 않는(+0.75) 표기 차이다.
Co-Authored-By: Claude Opus 5
Figma Navigation 섹션(391:4035)의 7개 컴포넌트 타입을 5개로 옮겼다.
- FoxBreadcrumb (breadcrumb)
- FoxPagination (pagination + pagination-item)
- FoxCarouselIndicator (carousel-dot + carousel-number)
- FoxCarouselButton (carousel-arrow + carousel-atomic)
- FoxCarouselArea (carousel-area)
합친 근거는 껍데기가 같고 안에 든 것만 다르다는 것이다. 원형 버튼 둘은
테두리·그림자만 달라 type으로, 위치 표시 둘은 흰 알약이 같아 type으로 갈랐다.
pagination의 시안 device 축은 프롭이 아니라 화면 폭이 정한다 — 768px 미만에서
이전·다음이 첫 줄로 빠지고 번호가 아래 줄에 선다. 마크업은 한 벌이고 DOM 순서는
읽는 순서 그대로다.
@fox/core/icons 신설 — 손으로 그리지 않고 Figma export 자산의 path를 옮겼다.
시안이 아이콘을 정사각 박스 안에 여백을 두고 앉히므로 그 여백까지 translate로
재현했다. CaretLeft/Right는 Regular·Bold 두 벌이다(시안이 자리마다 다르게 물렸다).
승인된 비토큰 값: 숫자칸 3.2rem/2.4rem, 캐러셀 점 0.8rem, 활성 막대 2.4rem,
가운뎃점 0.4rem — 모두 시안 값이나 Figma가 변수로 export하지 않는다.
Co-Authored-By: Claude Opus 5
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