카탈로그 등록 파일에서 충돌 일곱 구역이 났다. 양쪽이 같은 배열 끝에 항목을 붙였는데
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
시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) login 5435:14598.
시안의 입력·라벨·버튼이 디자인시스템 컴포넌트 인스턴스라 @fox로 거의 그대로 덮인다 — 치수가
우연이 아니라 정확히 같다. FoxInput lg가 h56·px20·radius12·17px·아이콘 24, FoxButton lg가
h56·px20·radius12·17px다. `label`을 주면 FoxInput이 FoxFormLabel까지 함께 그린다.
- login.module.scss 신설. 남는 건 화면 셸(배경·중앙 정렬)과 카드(440·padding 40·radius 24·
2겹 그림자), 비밀번호 토글 자리뿐이라 @fox에 넣지 않고 화면 전용으로 뒀다(사용자 확정).
- 비밀번호 보기 토글은 로그인 전용이다(사용자 확정). FoxInput에 그 기능이 없고, `icon` 슬롯은
`aria-hidden="true"`라 버튼을 넣으면 보조기술에서 사라진다. 그래서 슬롯 대신 같은 자리에
버튼을 겹쳐 두고 aria-pressed·aria-controls로 상태와 대상을 알린다.
- 오류 문구는 FoxInput의 `message`가 아니라 시안의 안내문 자리에 함께 둔다. 어느 칸이 틀렸는지
알리지 않는 편이 맞고(계정 존재 여부가 새지 않는다), 입력칸 아래 메시지가 끼면 토글의 세로
위치도 어긋난다 — 토글을 아래에서 띄우므로 입력칸이 래퍼의 마지막 요소여야 한다.
- components/ui/auth-card를 더 이상 쓰지 않는다. 시안에 로고가 없고 그쪽은 Tailwind 제거 이후
무스타일이다(헤더·풋터와 같은 처리).
검증 — 실측이 시안과 일치한다. 배경 #f4f5f6, 카드 440/40/24/gap16 + 그림자 2겹
(1·1·4 / 4·4·8, alpha-black10), 제목 32px 800 #1e2124, 라벨 15px 600, 입력 56/12/20/#58616a,
버튼 56/12/#256ef4/17px, 토글 24×24가 입력칸 세로 정중앙(차이 0px)에 오른쪽 여백 20px.
토글은 password↔text 전환과 aria-pressed·aria-label 갱신까지 확인했다.
Co-Authored-By: Claude Opus 5
관리자페이지 파일(QsFVFFUKGH68xAPVkF3SJ9)의 tab(391:4497)·tab-item(391:4431)을
FoxTab / FoxTabItem 둘로 옮겼다. 계열(line·fill) × 크기(lg·md) × 상태 다섯이 모두
시안에 있다.
크기가 배치를 정한다. lg는 탭이 폭을 고르게 나눠 갖고 목록 전체에 옅은 밑줄이 깔리며,
md는 글자만큼만 차지하고 8px 간격으로 선다. 아래 여백도 크기마다 다르다.
계열·크기 수식어는 항목이 아니라 묶음에만 붙인다 — 항목이 홀로 쓰이는 자리가 없어
`.fox-tab--line .fox-tab-item`으로 내려받는 편이 단순하다. 탭은 탭 안에 들어가지 않으므로
후손 선택자가 새는 문제도 없다.
line 계열은 기본 상태에도 투명한 1px 밑줄을 둔다(시안 그대로) — 고름으로 바뀔 때 글자가
밀리지 않으려면 자리를 미리 잡아야 한다. 고른 탭의 4px 밑줄은 목록의 1px 선 위로
끌어올려 겹치는 자리에서 굵은 선이 이기게 했다.
키보드는 ARIA 탭 규약을 따른다 — 좌우 화살표·Home·End로 옮기고 탭키는 고른 탭에만
닿으며, 잠긴 탭은 건너뛴다. 항목이 href를 가지면 주소가 바뀌는 이동이라 보고 로
내며 탭 역할을 붙이지 않는다.
검증: 여백·글자·색·모서리·마크업 23항목이 시안과 일치. 화살표 이동이 양끝에서 감기고
잠긴 탭을 건너뛰는 것을 확인했다. sass 550규칙 · tsc · eslint 통과.
Co-Authored-By: Claude Opus 5
셸은 `flex min-h-screen flex-col` 같은 Tailwind 클래스로만 잡혀 있었는데 Tailwind 제거
(e3d3133) 이후 그 클래스를 정의한 CSS가 없어 **배치가 하나도 적용되지 않고 있었다**. 헤더·
사이드바·본문·풋터가 그냥 블록으로 쌓여 사이드바가 본문 옆에 서지도 않았다.
@fox에는 넣지 않았다(사용자 확정) — 로고·프로필처럼 이 앱에만 있는 구성을 전제로 한 배치라
포터블 디자인시스템에 들어갈 성격이 아니다. 대신 여백·색·서체는 전부 @fox 토큰을 거친다.
- admin-shell.module.scss 신설. 뷰포트를 채우고 그 안에서만 스크롤한다 — 헤더는 항상 보이고
사이드바와 본문이 각자 스크롤한다. 사이드바가 이미 `overflow-y: auto` 전제로 만들어져 있어
(시안 높이 1251px이 뷰포트보다 크다) 행 높이가 뷰포트에 묶여야 그 스크롤이 동작한다.
- 본문 열에 `min-inline-size: 0`을 줬다. flex 자식 기본값이 auto라 넓은 표가 들어오면 줄지 않고
사이드바를 밀어낸다.
- main 여백은 헤더의 좌우 여백과 같은 padding(8)=24px이라 세로 기준선이 맞는다.
- 풋터가 components/ui/footer를 떠나 셸이 직접 그린다 — 그쪽도 무스타일이라 그대로 쓰면
테두리도 여백도 없다. 헤더가 components/ui/header를 떠난 것과 같은 이유다.
검증 — 1440×900에서 헤더 y0/64/전폭, 사이드바 y64(시안과 같은 위치)/240/잔여 높이,
본문열 x240/1200/자체 스크롤/흰 배경, main 여백 24, 풋터 border-top 1px·여백 16·24·
13px font-neutral-subtler·가운데 정렬. 페이지 자체는 스크롤되지 않고, 본문을 끝까지 내려도
사이드바 scrollTop은 0으로 유지된다(독립 스크롤).
Co-Authored-By: Claude Opus 5
표의 선택 열처럼 네모(동그라미)만 필요한 자리가 있는데 글자를 감출 방법이 없었다.
표 예시에서는 카탈로그 전용 CSS로 가려 두었던 것을 컴포넌트가 갖게 한다.
`display: none`으로 지우지 않는다 — 그러면 접근성 트리에서도 빠져 이름 없는 조작기가
된다. 자리만 없애고 DOM에는 남긴다. 루트가 `
시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) logo 5199:9639.
헤더 구현 때는 28px 정사각 심볼 자산이 없어 170×62 워드마크로 대신했는데, 이번 시안이 그
심볼을 제공한다. export SVG를 public/images/logo-foxpay-symbol.svg로 반입했다 — Figma 자산
URL은 7일 뒤 만료되므로 원본 바이트를 저장소에 둔다.
- 글자를 "에듀페이 관리자" 한 덩어리에서 시안의 2색 구성으로 나눴다. 제품명 "FOX PAY"는
ExtraBold + font-neutral-strong, 역할 "ADMIN"은 SemiBold + font-neutral-subtler다. 시안이
리터럴로 적은 #1E2124·#8A949E가 각각 그 토큰의 라이트 값과 같다.
- 심볼은 가로세로를 둘 다 적었다. ``의 width/height 속성이 만드는 1:1 기본 비율 때문에
한쪽만 적으면 28.12가 28로 눌린다.
- 심볼 alt는 비웠다 — 바로 옆 글자가 같은 내용을 이미 말하므로 채우면 두 번 읽힌다.
검증 — 실측이 시안과 일치한다. 심볼 28.12×28, gap 8, "FOX PAY" #1E2124/800/24px,
"ADMIN" #8A949E/600/24px, 헤더 높이 64 유지.
기존 logo-foxpay.svg는 그대로 둔다 — auth-card와 components/ui/header가 아직 쓴다.
Co-Authored-By: Claude Opus 5
시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F) snb-area 3041:11860.
메뉴는 화면이 소유하지 않는다. 계정 권한마다 항목이 달라 매 요청 백엔드가 구성해 주는 값이라,
라우트 목록을 컴포넌트 상수로 두던 구조를 걷어내고 서버에서 조회해 내려주는 흐름으로 바꿨다.
그래서 새 화면이 생겨도 사이드바 컴포넌트는 손대지 않는다.
- lib/domain/sidebar-menu.ts: section > 1depth > 2depth > 3depth + 즐겨찾기 타입. 아이콘은
컴포넌트가 아니라 이름 문자열로 받는다 — 백엔드가 넘길 수 있는 형태가 그것뿐이다.
- lib/data/repositories/sidebar-menu-repository.ts: 조회 지점. 아직 API가 없어 mock을 돌려주되
권한(roleCode)을 인자로 받아 `cache()`가 권한별로 따로 기억하게 했다 — API가 생기면 이 함수
본문만 교체하면 호출부는 그대로다.
- layout.tsx가 세션 권한으로 메뉴를 조회해 AdminShell을 거쳐 사이드바에 넘긴다. 클라이언트에서
불러오면 첫 페인트에 메뉴가 비었다가 채워지고 조회용 토큰도 브라우저로 내려가야 한다.
- sidebar-menu-icon.tsx: 이름 → @fox 아이콘 화이트리스트. 동적 import로 하면 번들러가 1,512종을
전부 싣게 되므로 적힌 것만 번들에 넣는다. 모르는 이름이 와도 기본 아이콘으로 그려진다.
- use-sidebar-nav.ts: 3단 트리용으로 다시 썼다. 펼침은 "현재 경로에서 파생된 기본값 + 사용자가
누른 값" 2층이라, 이동하면 그 가지가 저절로 열리고 직접 접으면 그 선택이 유지된다.
@fox 사용 범위(사용자 확정) — 1·2·3depth 항목은 @fox에 없어 사이드바 전용으로 짜되 토큰·아이콘은
전부 @fox를 쓰고, 있는 것은 그대로 재사용했다.
- 토글 버튼: FoxIconButton xsm/ghost가 24×24·radius 4·아이콘 20으로 시안과 정확히 일치한다.
- 검색창: FoxInput md가 크기·radius·여백까지 시안과 같고 테두리 색만 달라(input-border는
neutral-60, 시안은 border-neutral-subtler) 그 한 줄만 덮었다.
mock 메뉴는 실제로 존재하는 화면만 담았다 — 시안의 통합플랫폼 메뉴(기관 관리·역할 관리 등)를
그대로 넣으면 전부 갈 곳 없는 죽은 링크가 된다.
검증 — 브라우저 실측이 시안과 일치한다. 셸 240px·패딩 12·우측 1px, 배경 그라데이션
#f4f5f6 70% → #ecf2fe, 검색창 40/radius 8/테두리 #e6e8ea, 토글 24×24/radius 4/아이콘 20,
1depth 40/패딩 10·8/radius 8/14px ExtraBold/mix-blend multiply/아이콘 20, 활성 배경 #063a74 +
흰 글자, 2depth 판 흰 배경+1px+radius 8+패딩 8, 2depth 행 28, 별·캐럿 12(자리는 유지하고
opacity 0으로 감춤 — 시안과 같다).
AdminShell이 더 이상 useSidebar를 쓰지 않는다. 헤더 구현 때 시안에 없어 걷어낸 토글의 후속이다.
Co-Authored-By: Claude Opus 5
시안 tbl-list(699:6304)를 카탈로그에 넣으려 보니 두 가지가 빠져 있었다.
- FoxTableRow에 `selected` — 고른 줄의 바탕이 action/primary-hover, 글자가
font/theme/primary가 된다. `aria-selected`도 함께 나간다.
- FoxTableCell에 `emphasis` — 그 줄을 대표하는 칸. 시안 list 표의 제목 열이
heading/xsm(15px ExtraBold)를 쓴다.
고른 줄 규칙은 자식 결합자로 자기 칸만 고른다 — 후손 선택자를 쓰면 칸 안에 든 표까지
파랗게 물든다(글머리 목록에서 겪은 것과 같은 문제다).
예시는 시안 그대로 체크 열·정렬 머리·제목 강조·버튼 묶음·더보기·고른 줄을 담았고,
전체 선택 체크박스와 줄 선택이 실제로 동작한다. 정렬 아이콘과 더보기 아이콘은 이미
이식된 Phosphor 세트의 CaretUpDown·DotsThreeVertical을 그대로 쓴다.
검증: 고른 줄 바탕 #ecf2fe·글자 #0b50d0, 강조 칸 15px w800 lh 21px이 시안과 일치.
sass · tsc · eslint 통과.
Co-Authored-By: Claude Opus 5