[FE-02] 공용 UI 프리미티브
작업 내용 (설계 의도)
근거 설계: 20260722-공고알림앱-design-fe-web.md — “컴포넌트 트리”, “테마 토큰 정의”
변경 사항
모든 화면이 공유하는 도메인 무관 UI 프리미티브를 src/components/ui/**에 만듭니다. 화면 티켓 11건이 이 컴포넌트로 조립되므로 wave 2의 병목이지만, 도메인 지식이 0이라 도메인 로직(FE-03)·목 핸들러(FE-04)와 서로 의존하지 않아 같은 wave에서 병렬 진행됩니다.
기성 컴포넌트 라이브러리를 도입하지 않는 근거는 설계 “방안 5”에 있습니다 — 토스 시각 언어를 기준으로 하는데 라이브러리 기본 스타일을 덮어쓰는 비용이 직접 만드는 비용보다 크고, 필요한 프리미티브가 10여 개뿐입니다.
대상 컴포넌트:
| 컴포넌트 | 요점 |
|---|---|
Button | variant: primary(accent 채움) / secondary(surface + border) / text. loading 시 스피너 + disabled. 화면당 primary 1개 원칙은 사용처가 지킵니다 |
TextField | label · placeholder · errorMessage(인라인 에러) · description. 에러 시 aria-invalid + aria-describedby |
Select | 네이티브 <select> 기반 — 접근성·키보드 조작을 공짜로 얻습니다 |
DateField | 네이티브 <input type="date"> 기반. null 허용(“선택 안 함”) |
Chip | fill: filled / outlined / none. tone: accent·positive·warning·danger·neutral. 확신도 위계(채움/테두리/없음)를 표현할 수 있는 형태 축이 핵심 |
Badge | Chip보다 작은 상태 표시 |
Card | surface + radius-card + 내부 패딩. as="button"이면 링크·버튼 role |
BottomSheet | overlay + 하단 슬라이드. 포커스 트랩 · Esc 닫기 · 배경 스크롤 잠금 · role="dialog" + aria-modal |
Segment | 2~3개 세그먼트 전환 (role="tablist") |
Skeleton | --skeleton 토큰 기반 플레이스홀더 |
EmptyState | 제목 · 설명 · 선택적 액션. 일러스트 없이 텍스트 중심 |
ErrorState | 제목 · 설명 · [다시 시도] 액션 |
규칙: 색은 시맨틱 토큰만 사용합니다(하드코딩 시 lint 에러). 모든 컴포넌트는 라이트·다크 두 모드에서 동작해야 완료입니다 — 토큰만 쓰면 자동으로 만족하지만, 테스트로 두 모드 렌더를 확인합니다.
의존
- FE-01
다이어그램
처리 흐름
sequenceDiagram participant P as 화면 컴포넌트 participant S as BottomSheet participant F as 포커스 트랩 participant B as document.body P->>S: open=true S->>B: 스크롤 잠금 S->>F: 첫 포커서블로 포커스 이동 P->>S: Esc 또는 오버레이 클릭 S->>P: onClose() S->>B: 스크롤 복원 S->>F: 트리거 요소로 포커스 복귀
클래스 의존
flowchart LR subgraph Tokens["theme"] CSS[tokens.css 시맨틱 토큰] end subgraph Input["입력"] Button TextField Select DateField end subgraph Display["표시"] Chip Badge Card Skeleton end subgraph Layer["레이어·상태"] BottomSheet Segment EmptyState ErrorState end Input --> CSS Display --> CSS Layer --> CSS
테스트 케이스
Button의loading이 true면 스피너가 보이고 클릭 핸들러가 호출되지 않는다Buttonvariantprimary·secondary가 서로 다른 클래스로 렌더된다TextField에errorMessage를 주면 에러 문구가 보이고aria-invalid가 true가 된다TextField의 label 클릭으로 입력에 포커스가 이동한다Chip이 fillfilled/outlined/none에 따라 다른 형태로 렌더된다 (확신도 위계의 기반)BottomSheet가 열리면role="dialog"가 노출되고 첫 포커서블 요소로 포커스가 이동한다BottomSheet에서 Esc를 누르면onClose가 호출된다BottomSheet가 열린 동안 배경 스크롤이 잠기고 닫히면 복원된다BottomSheet가 닫히면 포커스가 트리거 요소로 되돌아온다Segment에서 다른 항목을 클릭하면onChange가 선택 값과 함께 호출된다Segment가 좌우 방향키로 이동 가능하다EmptyState가 제목·설명·액션 버튼을 모두 렌더하고 액션 클릭이 콜백을 호출한다ErrorState의[다시 시도]클릭이onRetry를 호출한다- 전 컴포넌트가
.dark클래스가 적용된 트리에서도 오류 없이 렌더된다 Select가 옵션 0개일 때도 크래시 없이 렌더된다