시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) container 5199:9014.
@fox에 없는 조각은 없었다 — 지난 작업의 FoxListContainer가 머리말·도구 줄·표·페이지네이션을
한 번에 덮고, 정렬 셀렉트는 FoxSelectText, 검색은 FoxListSearch, 버튼은 FoxButton이다.
데이터도 이미 맞물려 있었다: StudentMember에 시안의 13열이 전부 있고 StudentMemberQuery에
검색필드·정렬·페이지크기·buildStudentMemberHref가 있다.
- students/_components/student-list.tsx 신설. 이 화면 고유의 것만 갖는다 — 13열 정의,
정렬·페이지 크기 셀렉트, 엑셀 다운로드, URL 갱신 방법.
- page.tsx는 조회만 하고 넘긴다(Server Component 유지).
- student-search-bar · student-list-toolbar · student-table 3종을 지웠다. 셋이 하던 일을
FoxListContainer가 대신한다.
이동은 두 갈래다. 페이지네이션은 `buildHref`로 링크가 되고(주소가 곧 상태), 검색·정렬·페이지
크기는 `router.replace`로 같은 주소 규칙을 태운다. 조건이 바뀌면 늘 1페이지로 되돌린다.
사용자 확정 2건
- 열 폭 합계가 1880px로 컨테이너(1552)보다 넓어 FoxListContainer에 가로 스크롤 래퍼를 넣었다.
좁은 화면에서는 목록표가 카드로 바뀌므로(FoxTable의 mobile 규칙) PC에서만 건다.
- 관리 열을 "학생 회원 조회" 텍스트 버튼으로 바꿨다. 앞서 기획 시안을 근거로 돋보기 아이콘을
넣었으나 이 Figma 시안이 더 최신이다. 누르면 상세 팝업이 열리는 동작은 그대로다.
지킨 것 — 순번은 저장된 값이 아니라 현재 페이지 기준 표시 순번((page-1)*pageSize+index+1)으로
종전 표와 같은 규칙이다. 엑셀은 네이티브 GET 폼이라야 첨부파일 응답을 브라우저가 받는다.
검증 — 실측이 시안과 일치한다. 제목·총 123건·정렬 셀렉트("가입일순"·"10개씩 보기")·검색 대상
"회원명"·엑셀 버튼, 13열 이름 전부, 관리 버튼 "학생 회원 조회", 표 1880/래퍼 1552로 가로
스크롤, 페이지 링크 `/students?page=2`, 모바일용 셀 라벨은 PC에서 display:none.
Co-Authored-By: Claude Opus 5
시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) container 3019:8608.
시안 조각 대부분은 이미 @fox에 있었다(FoxPageHeader·FoxTable·FoxPagination·FoxSelectText·
FoxChipArea·FoxBadge·FoxButtonGroup). 없던 것은 list-toolbar 하나였고, 그 안의 검색 상자까지
합쳐 셋을 새로 만들었다.
- FoxListSearch: 앞쪽에 검색 대상 셀렉트가 들어가는 상자. FoxInput에는 그 슬롯이 없어 따로
만들었다. `
아코디언(194:3563 box · 194:3680 line)과 카드(441:6006)를 옮겼다.
FoxAccordion — 계열 둘이 성격이 다르다. box는 색을 깐 상자로 여백·모서리를 상자가 갖고
제목이 강조 색이며, line은 위쪽 선 하나로 나뉘는 목록형이라 바탕이 없고 제목 줄이 세로
여백을 갖는다. 크기 lg·md가 여백·글자·모서리를 함께 바꾼다.
열림은 넘기면 호출부가, 안 넘기면 컴포넌트가 쥔다 — 여러 개 중 하나만 열리게 하려면
호출부가 쥐면 된다. 닫히면 내용을 DOM에서 뺀다. 안의 초점 가능한 것들이 탭 순서에 남으면
보이지 않는 곳으로 초점이 사라진다.
FoxCard — 크기 넷은 여백과 모서리만 바꾼다. 폭은 놓이는 자리가 정한다(시안은 320px 자리에
뒀을 뿐 카드 자체의 값이 아니다). 아래쪽 동작 줄은 좌우·아래 여백만 갖는다 — 위쪽은 글
영역의 아래 여백이 이미 띄워 준다.
검증: 아코디언 16항목 중 15개 일치(나머지 1건은 내 예상값 오류였고 코드가 맞다 —
action/secondary는 투명 검정이 아니라 투명 흰색이다). 카드 18항목 전부 일치.
sass 694규칙 · tsc · eslint 통과.
승인된 비토큰 값: 그림 비율 16/9.
Co-Authored-By: Claude Opus 5
제목을 안 넘기면 제목 줄이 통째로 빠져 닫기 아이콘이 내용 위로 내려앉았다. 시안의 tit
프레임은 제목이 없어도 28px(제목 글자 크기 × 줄높이)을 차지한다.
줄은 항상 내고 `
`만 제목이 있을 때 낸다 — 빈 제목을 심으면 이름 없는 heading이 되어
스크린리더가 헛읽는다. 높이는 토큰에서 계산해 브레이크포인트가 바뀌어도 따라간다.
검증: 제목이 있든 없든 제목줄 높이 28 · 위치 41 · 내용 시작 85로 같다. 제목이 없을 때
닫기 아이콘 아래끝 56이 내용 시작 85보다 위라 겹치지 않는다.
Co-Authored-By: Claude Opus 5
시안은 버튼이 80×40인데 51×40으로 나왔다. 최소 폭 80px이 빠져 있었다.
btn 컴포넌트 세트 전체를 뒤져 보니 최소 폭 표기가 한 건도 없다 — 버튼 자체의 성질이
아니라 모달 아래쪽 자리의 규칙이다. 그래서 FoxButton이 아니라 모달 쪽에 준다. 글자가
짧아도 두 버튼의 폭이 같아 보이게 하려는 값이다.
검증: 버튼 80×40 · 묶음 168 · 오른쪽 끝이 내용 영역과 맞는다(시안과 동일).
Co-Authored-By: Claude Opus 5
디자인시스템 파일의 modal(190:1136)을 옮겼다. 크기 넷(sm 400 · md 560 · lg 760 · full)이
모두 폭 토큰이고, 모바일에서는 넷 다 같은 폭으로 모이며 여백·모서리·닫기 버튼도 함께
줄어든다 — 반응형 토큰이 그렇게 정의돼 있다.
네이티브 `
오른쪽 끝에 붙기는 했지만 그건 제목 칸이 `flex: 1`이라 밀려난 결과였을 뿐, 정렬을 선언한
것이 아니었다. 제목 칸이 좁아지거나 빠지면 자리가 흔들린다.
`justify-content: flex-end`와 `margin-inline-start: auto`로 못박고, 안에 든 것이 여러 개라
줄이 넘어가도 오른쪽에 맞도록 자식에도 같은 정렬을 준다.
세로는 시안대로 위 정렬(`align-items: flex-start`)을 유지한다.
Co-Authored-By: Claude Opus 5
관리자페이지 파일의 page-tit-area(3002:6823)를 옮겼다. 현재 위치와 제목을 얹고 본문과의
간격(아래 여백 36px)까지 갖는다 — 시안이 그렇게 묶어 놓아 화면마다 따로 띄우지 않아도 된다.
현재 위치는 FoxBreadcrumb를 그대로 쓴다(시안에서도 breadcrumb 인스턴스다). 항목 배열만
넘기면 되고, 넘기지 않으면 그 줄을 그리지 않는다.
제목은 화면에 하나뿐인 이름이라 `
`으로 낸다. 시안은 `
`지만 그건 Figma에 문단밖에
없어서고, 마크업에서는 제목이 맞다.
`actions` 자리를 열어 뒀다. 시안에 들어간 것은 없지만 배치가 자리를 비워 두고 있다 —
제목 칸이 `flex: 1`이고 오른쪽에 24px 간격이 남는다. 값을 지어내지 않고 그 자리만 연다.
검증: 바깥 간격 24 · 아래 여백 36 · 안쪽 간격 8 · 제목 32px w800 #1e2124 줄높이 48 ·
자간 -0.8px이 시안과 일치. 오른쪽 자리를 쓸 때 제목이 남는 폭을 먹고 24px이 유지되며,
제목이 길어져도 오른쪽을 침범하지 않는다. sass 634규칙 · tsc · eslint 통과.
Co-Authored-By: Claude Opus 5
카탈로그 등록 파일에서 충돌 일곱 구역이 났다. 양쪽이 같은 배열 끝에 항목을 붙였는데
git이 서로 다른 항목의 앞뒤를 맞물려 갈라 놓아, 구역별로 이어 붙이면 남의 항목 머리에
내 항목 꼬리가 붙는다.
그래서 충돌 파일을 손보지 않고 양쪽 원본을 꺼내 항목 단위로 다시 합쳤다. 중괄호를 세어
최상위 항목을 잘라내므로 JSX 안의 괄호나 타입 표기의 대괄호에 속지 않는다.
- 겹치는 항목 없음: 우리 12개(breadcrumb·pagination·carousel 3·bullet-list·badge 3·
table·tab·spinner), 상대 6개(chip·chip-area·select-text·segmented-control 2·
toggle-quantity)
- fox-select는 상대만 고쳤으므로(다중 선택 지원) 그쪽 판을 쓴다
- fox-toggle-switch는 둘 다 고친 것으로 보였지만 뒤에 항목이 붙으며 닫는 괄호가 옮겨진
것뿐이라 실제 충돌이 아니다
components.scss는 자동 병합되며 `@use "fox-toggle-switch"`가 두 번 들어가 Sass가 같은
네임스페이스 중복으로 컴파일을 멈췄다. 중복을 지웠다.
검증: 카탈로그 45개 항목이 모두 뜨고 양쪽 새 컴포넌트가 화면에 그려진다. 파티셜 50개가
빠짐없이 등록돼 있고 배럴도 폴더 수와 같다. sass 630규칙 · tsc · eslint 통과.
Co-Authored-By: Claude Opus 5
전환 중 뜨는 표시가 Tailwind 클래스(animate-spin·border-t-transparent·size-8)로만
그려져 있었는데, Tailwind가 제거된 뒤로 그 클래스들이 정의되지 않아 아무것도 보이지
않는 상태였다. 감싸던 배치 클래스(flex·flex-1·items-center)도 마찬가지였다.
- components/ui/spinner.tsx가 FoxSpinner를 그리도록 바꿨다. size·tone 축은 그대로 둬서
호출부 다섯 곳을 건드리지 않는다(loading.tsx 넷 + 전역 오버레이 하나).
- 계열 색은 CSS 모듈로 옮겼다. FoxSpinner 기본이 테마색이라 inverse·muted만 덮는다.
- 화면 넷의 loading.tsx가 같은 내용이라 PageLoading 공용 컴포넌트로 모으고, 죽어 있던
배치를 CSS 모듈로 되살렸다.
확인한 것: 두 CSS 모듈이 토큰 var()로 컴파일되고, FoxSpinner가 32px에서 시안대로
그려지며, tsc·eslint가 통과한다.
확인하지 못한 것: 전환 화면 자체는 로그인이 필요한 라우트라 눈으로 보지 못했다.
Co-Authored-By: Claude Opus 5
관리자페이지 파일의 img-loading(5384:9710)을 옮겼다. 시안은 80px 그림 한 장으로 왔지만
래스터를 그대로 쓰지 않았다 — 크기를 바꾸면 뭉개지고 색도 토큰을 못 쓴다. 원본 픽셀을
재서 같은 도형을 CSS로 다시 그렸다.
잰 값: 바깥 반지름 40 · 안쪽 28 → 굵기 12px(지름의 15%), 앞머리 원 12px이 6시 자리 고리
중심선 위. 색은 6시에서 시작해 시계방향 60°까지 알파 1.00이고 나머지 300°에 걸쳐 0까지
선형으로 준다(5°마다 샘플링해 확인).
굵기·앞머리를 퍼센트로 잡아 어떤 지름에서도 비율이 같다. `size`만 주면 된다.
FoxButton의 로딩 자리는 "시안 미정"으로 비어 있었는데 이제 이 스피너가 들어간다.
버튼 안에서는 `color: inherit`으로 되돌려 계열별 글자색을 따라간다 — 요소에 준 색이
상속보다 강해서, 기본색을 두는 한 감싸기만으로는 바뀌지 않기 때문이다.
승인된 비토큰 값: 지름 8rem, 굵기·앞머리 15%, 마스크 정지점 70%, 그라디언트 60°.
한 바퀴 도는 시간 1초는 시안이 정지 그림이라 정보가 없어 흔한 기본값을 썼다.
검증: 지름 80 · 색 #256ef4 · 앞머리 12×12가 중심에서 아래로 34px(시안과 동일) ·
그라디언트 from 180deg 0~60deg 불투명. 버튼 세 크기에서 스피너 색이 글자색과 일치하고
아이콘 슬롯 크기를 그대로 채운다.
Co-Authored-By: Claude Opus 5
사이드바 폭 문제의 뿌리가 전역 리셋 부재였다. @fox 컴포넌트는 각자 `box-sizing: border-box`를
선언하지만 앱 쪽에는 기준이 없어 모듈마다 따로 챙겨야 했고, 빠뜨리면 `inline-size: 100%`에
패딩이 더해져 자식이 부모보다 넓어졌다.
globals.scss에 리셋을 넣고, 직전 커밋에서 사이드바 모듈에 넣었던 범위 한정 리셋은 지웠다 —
같은 일을 두 곳에서 하지 않는다.
@fox는 이미 스스로 선언하고 있어 영향이 없다. 검증도 그렇게 나왔다 — 헤더 64/pill 44/아이콘
버튼 40, 사이드바 240·216·216·216·200(3개), 본문 x240·여백 24, 로그인 카드 440·입력 56×360·
버튼 360×56·토글 세로중앙차 0으로 전부 종전과 같다.
Co-Authored-By: Claude Opus 5
원인이 둘이었다.
1. `box-sizing`이 기본값 `content-box`였다. 앱에 전역 리셋이 없고(@fox 컴포넌트는 각자
선언한다) 이 모듈은 `.sidebar`에만 줘서, `inline-size: 100%`에 패딩·테두리가 더해졌다.
2depth 판이 233, 그 안의 행이 231이 됐다(시안 216/200). 이 파일이 그리는 조각 전체에
`border-box`를 적용했다.
2. 테두리가 폭을 먹었다. Figma stroke는 안쪽에 그려지지만 CSS `border`는 박스를 줄인다.
사이드바 오른쪽 구분선 때문에 안쪽 폭이 216에서 215로(사용자가 본 값), 2depth 판의
테두리 때문에 행이 200에서 198로 줄었다. 둘 다 레이아웃에 관여하지 않는 inset 그림자로
같은 선을 그렸다 — 헤더 프로필 pill에서 쓴 것과 같은 처리다.
검증 — 실측이 시안과 일치한다. 사이드바 240, 내부 리스트·1depth 행·2depth 판 216,
2depth 행 200(3개 모두), 즐겨찾기 행 216. 흰 판과 구분선은 그대로 보인다.
Co-Authored-By: Claude Opus 5
버튼·입력칸이 Pretendard가 아니라 Arial 13.33px으로 그려지고 있었다. 폼 조작기는 CSS
명세상 `font`를 상속받지 않는데 그걸 되돌리는 규칙이 없어, 아무도 서체를 지정하지 않은
자리는 브라우저 기본값이 이겼다. 그래서 제목·본문은 Pretendard인데 그 옆 버튼만 다른
서체로 나와 한 화면에 두 서체가 섞였다.
app/globals.scss에 조작기 리셋을 한 번 둔다. 타입 선택자(0,0,1)라 클래스로 서체·크기를
정한 곳은 그대로 이긴다.
확인한 범위:
- @fox 컴포넌트: 글자 크기(17/15/13px)·상자 크기(360×40) 모두 그대로. 서체를 지정하지
않았던 FoxInput의 지우기 버튼만 Arial → Pretendard로 바뀐다(아이콘만 있어 보이는 변화 없음).
- 카탈로그: Arial이던 버튼 49개가 전부 Pretendard로. 크기·굵기는 유지되고 높이만 1px
줄어드는데 서체가 바뀌면서 `line-height: normal` 계산이 달라진 결과다.
- 로그인 화면: 조작기 4개 모두 Pretendard. Arial 0건.
폰트 파일 자체와 시안 대조는 이상이 없었다 — 파일은 정품 Pretendard 400/600/700/800이고,
탭 label(20px/800)의 잉크 폭이 자간 반영 시 42px로 Figma 렌더와 같다.
Co-Authored-By: Claude Opus 5
로그인 카드 안에만 두었던 `:autofill` 우회를 `_form-field.scss`의 `field` 믹스인으로 옮긴다.
자동완성이 칠하는 배경은 특정 화면 문제가 아니라 텍스트 입력 전부에 해당한다.
- `_autofilled` 믹스인 신설. 배경은 `background`로 덮이지 않아 안쪽을 가득 채우는 inset
그림자로 가리고, 글자색은 `color`가 먹지 않아 `-webkit-text-fill-color`로 맞춘다.
- 처음엔 표준(`:autofill`)과 접두사(`:-webkit-autofill`)를 따로 적었으나, Lightning CSS가
browserslist 타깃을 보고 접두사를 표준형으로 합쳐 같은 규칙이 두 번 나오기만 했다
(컴파일 결과 8건 → 정리 후 4건). 표준 하나만 남겼다.
적용 범위 — 자체 ``을 가진 fox-input · fox-text-area · fox-certified ·
fox-phone-number 넷에 직접 붙고, fox-email · fox-address · fox-file-upload는 내부에서
fox-input을 조립하므로 `.fox-input__field` 규칙으로 함께 덮인다. 즉 @fox의 모든 텍스트
입력이 커버된다.
Co-Authored-By: Claude Opus 5
FoxInput에 `state`를 넘긴 것이 원인이었다. `state`(=`data-state`)는 카탈로그처럼 상태를 눈으로
비교할 때 쓰는 **고정** 스위치라, 주는 순간 자동 규칙이 통째로 꺼진다 — form-field.scss의
자동 규칙이 `.fox-input:not([data-state])` 안에 들어 있다. 그래서 클릭해도 테두리가 굵어지지
않았고 오류 표시도 살아 있지 않았다. 오류는 `invalid`로 넘기는 것이 정식 경로이며
(`aria-invalid`가 되어 자동 규칙이 잡는다) 그렇게 고쳤다.
자동완성으로 채워진 칸은 Chrome이 자기 배경(연한 파랑)을 칠하는데 @fox에도 앱에도
`:-webkit-autofill` 처리가 없어 그대로 노출된다. 일반 CSS로는 못 덮으므로 표준 우회인 inset
그림자로 가리고 글자색은 `-webkit-text-fill-color`로 맞췄다. 우선 로그인 카드 안으로 범위를
좁혔다 — 모든 FoxInput이 같은 상태라 디자인시스템에 올릴지는 별도 판단이 필요하다.
검증 — data-state가 사라졌고, 포커스 시 테두리가 1px에서 2px #256ef4로, aria-invalid 시
2px #de3412로 바뀐다. autofill 우회 규칙이 스타일시트에 실린 것도 확인했다.
Co-Authored-By: Claude Opus 5
디자인시스템 파일의 tab(391:4433)을 다시 보니 fill lg가 낱개 탭이 아니라 세그먼트
컨트롤이었다. 목록이 테두리·둥근 모서리·흰 바탕을 갖고 항목이 그 안에 갇힌다.
`overflow: clip`이 있어야 끝 칸의 채운 색이 둥근 모서리를 넘지 않는다.
칸 사이 구분선은 고르지 않은 칸이 자기 오른쪽에 그린다 — 고른 칸은 색이 바뀌어 경계가
이미 보이므로 선을 두지 않는다(시안 그대로). 어느 칸이 골라져도 선이 맞아떨어진다.
fill md는 낱개 알약이 맞아 그대로 둔다.
고른 탭에 hover·pressed가 걸리던 것도 고쳤다. 규칙 특이도가 0,5,0이라 고름 규칙(0,2,0)을
이겨서, 고른 탭에 마우스를 올리면 남색·파란 밑줄이 옅은 파랑으로 덮였다.
검증: 목록 테두리 1px #cdd1d5 · 모서리 12px · clip · 흰 바탕, 구분선이 고르지 않은
칸에만 붙는 것을 확인. hover 규칙이 고른 탭 6개 어디에도 걸리지 않는다.
Co-Authored-By: Claude Opus 5