diff --git a/.github/ISSUE_TEMPLATE/system-change.yml b/.github/ISSUE_TEMPLATE/system-change.yml new file mode 100644 index 0000000..a2b1dca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/system-change.yml @@ -0,0 +1,37 @@ +name: 시스템 변경 제안 +description: 새 토큰·컴포넌트·패턴이 필요하거나 기존 규칙을 바꾸자고 제안합니다. (docs/process/change-flow.md) +title: '[System]: ' +labels: ['system-change'] +body: + - type: dropdown + id: kind + attributes: + label: 무엇을 바꾸나요 + options: + - 토큰 (색·글자·모서리·모션) + - 컴포넌트 (새 ui 컴포넌트 · variant/size 추가) + - 패턴 (결정 트리) + - 문구 규칙 (톤·용어집) + validations: + required: true + - type: textarea + id: need + attributes: + label: 왜 필요한가요 + description: 어떤 화면에서 무엇을 하려는데 막혔는지. 스크린샷이 있으면 좋아요. + validations: + required: true + - type: textarea + id: existing + attributes: + label: 기존 것으로는 왜 안 되나요 + description: docs/registry.md · docs/patterns · docs/foundations.md 에서 검토한 것 + validations: + required: true + - type: textarea + id: proposal + attributes: + label: 제안 + description: 이름·값·쓸 때/피할 때. 다른 대안이 있었다면 함께. + validations: + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..7e60955 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ +## 요약 + + + +## 화면이 바뀌는 곳 + + + +## UI 를 바꿨다면 (`docs/ai-review-checklist.md`) + +- [ ] `docs/registry.md` 의 기존 컴포넌트를 썼다 (새로 만들었다면 JSDoc + `*.meta.ts` + 스토리) +- [ ] 상황이 패턴에 해당하면 `docs/patterns/README.md` 의 결정 트리를 따랐다 +- [ ] 문구는 `docs/copy.md` — 해요체, `…`, 용어집 +- [ ] 로딩·오류·빈 상태·성공을 확인했다 +- [ ] 스토리가 있다면 Storybook Accessibility 패널 위반 0 +- [ ] 새 토큰·컴포넌트·패턴이면 문서를 먼저 고쳤다 (change-flow(`docs/process/change-flow.md`)) + +## 검증 + +- [ ] CI 통과 (lint · typecheck · test · tokens:check · build) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..28e3aa0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +name: CI + +# develop·main 으로 가는 PR 마다 검수 5종을 돌린다 (AGENTS.md "명령어", docs/process/change-flow.md). +# 로컬 git hook 은 lint·typecheck·build 만 돌고, 건너뛸 수도 있어서 여기서 다시 막는다. +on: + pull_request: + branches: + - develop + - main + workflow_dispatch: + +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + # 검사용 빌드라 실제 API 주소가 필요 없다. index.html 의 %VITE_API_HOST% 치환만 채운다. + VITE_API_BASE_URL: http://localhost:3000 + VITE_API_HOST: localhost:3000 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Setup Yarn via Corepack + run: | + corepack enable + corepack prepare yarn@3.6.3 --activate + + - name: Install dependencies + run: corepack yarn install --immutable + + - name: Lint + run: corepack yarn lint + + - name: Typecheck + run: corepack yarn typecheck + + - name: Test + run: corepack yarn test + + - name: Tokens check + run: corepack yarn tokens:check + + # tokens:check 가 다시 만든 docs/registry.md·contrast-matrix.md 가 커밋본과 다르면 + # meta·토큰을 고치고 생성 파일을 커밋하지 않은 것이다. + - name: Generated docs are committed + run: git diff --exit-code -- docs/registry.md docs/tokens/contrast-matrix.md + + - name: Build + run: corepack yarn build diff --git a/.gitignore b/.gitignore index 1bdee94..98cc832 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,4 @@ web-dist.tar.gz #ai /.omx /.omc +storybook-static diff --git a/.prettierignore b/.prettierignore index 9c6b791..0f99381 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,3 +4,6 @@ pnpm-lock.yaml LICENSE.md tsconfig.json tsconfig.*.json +# yarn tokens:check 가 생성 — 스크립트 출력 그대로 둔다 +docs/tokens/contrast-matrix.md +docs/registry.md diff --git a/.storybook/main.ts b/.storybook/main.ts index a7be3e3..1d98529 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -6,7 +6,7 @@ import svgr from 'vite-plugin-svgr'; const config: StorybookConfig = { stories: ['../src/renderer/src/components/**/*.stories.@(ts|tsx)'], - addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'], + addons: ['@storybook/addon-essentials', '@storybook/addon-interactions', '@storybook/addon-a11y'], framework: { name: '@storybook/react-vite', options: {} diff --git a/AGENTS.md b/AGENTS.md index 42a495f..0417471 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,13 +9,15 @@ yarn dev # Electron 개발 모드 (main + preload + renderer HM yarn web:dev # 렌더러만 브라우저에서 (src/renderer/vite.config.ts) yarn lint # ESLint yarn typecheck # tsc: node(main/preload) + web(renderer) 두 프로젝트 모두 +yarn test # vitest yarn build # typecheck + electron-vite build yarn web:build # 웹 정적 빌드 → src/renderer/dist yarn storybook # 컴포넌트 샌드박스 (포트 6006) +yarn tokens:check # 토큰·대비·패턴 규칙 검사 + docs/registry.md 생성 yarn swagger:local # OpenAPI → src/renderer/src/api/generated 재생성 ``` -테스트 러너는 없다. 검증 수단은 `lint` + `typecheck` + Storybook. +테스트는 `yarn test`(vitest, `*.test.ts(x)`). 검증 수단은 `lint` + `typecheck` + `test` + `build` + `tokens:check` + Storybook. `yarn swagger:local`은 package.json에서 EC2 주소를 가리킨다. 로컬 백엔드(`../Qode-Server`) 스펙으로 생성하려면 스크립트의 `-p` URL을 `http://localhost:3000/docs/json`으로 바꿔서 실행한다. @@ -82,6 +84,16 @@ vanilla-extract 플러그인이 세 곳(electron.vite, renderer vite, storybook) 아이콘은 SVGR(`?react`)로 불러 `components/icons/iconRegistry.ts`에 등록하고 ``로 쓴다. 새 아이콘은 `icons/raw/`에 넣고 레지스트리에 추가해야 타입에 잡힌다. +### 디자인 시스템 (Quire) — UI 작업 전에 읽을 것 + +1. [`docs/registry.md`](docs/registry.md) — 컴포넌트별 쓸 때·피할 때·옵션. 새로 만들기 전에 여기서 찾는다. +2. [`docs/patterns/`](docs/patterns/README.md) — 확인·에러·피드백·빈 상태·로딩의 결정 트리. +3. [`docs/foundations.md`](docs/foundations.md) — 토큰(색·글자·모서리·모션)의 의미. 정체성·원칙은 [`SYSTEM.md`](SYSTEM.md). + 화면 문구는 [`docs/copy.md`](docs/copy.md) — 해요체, `…`, 용어집. +4. 끝나면 [`docs/ai-review-checklist.md`](docs/ai-review-checklist.md) 로 검수하고 `yarn tokens:check` 를 돌린다. + +새 ui 컴포넌트는 JSDoc(✅ Use / ❌ Don't) + `Name.meta.ts` + 스토리를 함께 만든다. `docs/registry.md` 는 생성 파일이라 직접 고치지 않는다. + ### 컴포넌트 배치 `ui/`(범용, Storybook 스토리 동반) → `layout/`(AppShell·AuthFrame·ProjectTabs) → `feature/`(도메인 모달·테이블) → `pages/`. @@ -93,6 +105,12 @@ vanilla-extract 플러그인이 세 곳(electron.vite, renderer vite, storybook) - pre-push: `build` (즉 typecheck가 두 번 돌아 푸시가 느리다) - commit-msg: 브랜치 이름의 숫자를 이슈 번호로 뽑아 커밋 제목 끝에 ` (#123)` 자동 추가 +## CI + +`.github/workflows/ci.yml` — develop·main 으로 가는 PR 마다 lint · typecheck · test · tokens:check · build. +`tokens:check` 가 다시 만든 `docs/registry.md`·`docs/tokens/contrast-matrix.md` 가 커밋본과 다르면 실패하니 함께 커밋한다. +토큰·컴포넌트·패턴을 새로 만들거나 바꿀 때는 [`docs/process/change-flow.md`](docs/process/change-flow.md), 문서끼리 어긋나면 [`docs/process/sot-priority.md`](docs/process/sot-priority.md). + ## 배포 `v*` 태그 푸시 → Electron 3플랫폼 패키징(`release.yml`). `main` 브랜치 푸시 → 웹 정적 빌드 EC2 배포(`web-deploy-ec2.yml`). diff --git a/DESIGN.md b/DESIGN.md index eda3fba..0da966d 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -2,8 +2,8 @@ ## Source of truth — 문서 상태와 근거 -- 상태: **In sync** — 실제 구현과 코드가 이 문서와 정합한 상태로 유지된다. 이후 컴포넌트를 추가하거나 스타일을 손볼 때 이 기준서를 먼저 갱신하고 코드를 맞춘다. -- 갱신일: 2026-09-16. +- 상태: **In sync** (2026-09-26) — 실제 구현과 코드가 이 문서와 정합한 상태로 유지된다. 이후 컴포넌트를 추가하거나 스타일을 손볼 때 이 기준서를 먼저 갱신하고 코드를 맞춘다. +- 갱신일: 2026-09-26. (Quire Phase E — 컴포넌트별 기준을 `docs/registry.md` 로 옮김) - 목적: Qode 의 화면을 일관된 시각 규칙 아래 유지하기 위한 참고 기준서. 새 컴포넌트나 페이지도 아래 토큰·간격·패턴을 재사용해 낯설지 않은 경험을 제공한다. - 실제 스타일 구현의 기준 파일: `src/renderer/src/assets/main.css`. 컬러·타이포·간격 토큰은 여기서 시작한다. Tailwind v4 유틸리티는 이 토큰을 참조한다. - 확인한 구현: `components/layout/AppShell.tsx`, `components/ui/DrawerHeader.tsx`, `components/ui/ProjectSwitcher.tsx`, `components/ui/Button.tsx`, `components/ui/ChatComposer.tsx`, `components/ui/CodeBlock.tsx`, `components/ui/MarkdownAnswer.tsx`, `pages/ProjectDetailPage.tsx`, `pages/LoginPage.tsx`, `pages/SignupPage.tsx`, `api/capabilities.ts`. 경로는 별도 표시가 없으면 `src/renderer/src/` 기준이다. @@ -65,203 +65,161 @@ ## Visual language — 시각 언어 +토큰 체계와 결정 이유는 [`docs/foundations.md`](./docs/foundations.md)(Quire Foundations)가 기준이다. 이 절은 화면을 만들 때 바로 쓰는 요약이다. 값의 SoT 는 `main.css`, 대비 검증은 `yarn tokens:check`([매트릭스](./docs/tokens/contrast-matrix.md)). + +- 컴포넌트는 **semantic 토큰만** 쓴다. raw 팔레트(`--palette-*`)는 `@theme` 밖에 있어 유틸리티가 생성되지 않는다. +- Tailwind 기본 팔레트·폰트 크기·radius·그림자는 차단돼 있다(`bg-zinc-800`, `text-sm`, `rounded-md` 는 스타일이 생기지 않는다). `yarn tokens:check` 가 이런 클래스를 찾아 실패시킨다. +- 새 색·크기·radius 가 필요하면 하드코딩하지 말고 `docs/foundations.md` 에 먼저 등록한다. + ### 컬러 토큰 -`main.css` 에 정의된 시맨틱 토큰. Tailwind 는 `bg-primary`, `text-text-subtle`, `border-line` 형태로 참조한다. 새 원시 팔레트를 추가하지 말고 아래 시맨틱만 확장한다. - -| 시맨틱 토큰 | 값 | 대표 사용처 | -| ------------------------------------ | ------------ | -------------------------------------------------------------------------------- | -| `app-bg` | `#faf6f1` | 앱 캔버스. 사이드바와 메인 카드 바깥의 여백. | -| `sidebar` | `#fbfaf8` | 좌측 사이드바 배경. 캔버스와 살짝 다른 크림. | -| `surface` | `#ffffff` | 메인 채팅 카드, AI 답변 카드, 모달, 입력 카드. | -| `surface-muted` | `#faf6f1` | 보조 콘텐츠 면. (app-bg 와 같은 값) | -| `line` | `#dfddd8` | 카드·박스 장식 경계. | -| `line-soft` | `#efefef` | 아주 약한 내부 구분선. `ChatComposer` 기본 배경으로도 사용. | -| `control-line` | `#61646b` | 입력창처럼 식별에 필수적인 경계. | -| `text-base` | `#0e0e0f` | 제목·핵심 본문. | -| `text-subtle` | `#52545a` | 보조 본문·라벨. | -| `text-soft` | `#61646b` | 설명·시간·플레이스홀더·참조 소스 텍스트. | -| `primary` | `#ff6900` | 주요 액션 채움, AI 아바타 테두리, 활성 표시 인디케이터. | -| `primary-strong` | `#e05e00` | 주요 액션 hover·pressed. | -| `primary-foreground` | `#0e0e0f` | 오렌지 위 글자·아이콘. `#ff6900` 위 4.5:1+ 확보. | -| `primary-soft` | `#fff0e5` | 활성 채팅 배경, 사용자 말풍선 배경, ChatComposer focus 배경, 프로필 아바타 배경. | -| `accent` | `#ff6900` | 좌측 인디케이터 등 브랜드 강조. | -| `accent-strong` | `#b54700` | 밝은 면의 작은 링크·강조 텍스트 (예: 회원가입/로그인 링크). | -| `danger`, `danger-bg`, `danger-line` | 기존 값 유지 | 오류·삭제 경고. | +전경은 `fg-*`, 배경은 장소 이름, 선은 `line-*`. Tailwind 에선 `text-fg-muted`, `bg-surface`, `border-line` 처럼 쓴다. + +| 배경 | 값 | 대표 사용처 | +| ---------------------- | --------------------- | ----------------------------------------------------------------- | +| `canvas` | `#faf6f1` | 앱 캔버스. 사이드바와 메인 카드 바깥의 여백. | +| `sidebar` | `#fbfaf8` | 좌측 사이드바 배경. 캔버스와 살짝 다른 크림. | +| `surface` | `#ffffff` | 메인 채팅 카드, AI 답변 카드, 모달, 입력 카드. | +| `surface-muted` | `#faf6f1` | 보조 콘텐츠 면, 표 헤더, 인라인 코드. | +| `primary` | `#ff6900` | 주요 액션 채움, 활성 인디케이터, 진행 바. | +| `primary-strong` | `#e05e00` | 주요 액션 hover·pressed. | +| `primary-soft` | `#fff0e5` | 활성 채팅, 사용자 말풍선, ChatComposer focus, 브랜드 아바타 배경. | +| `danger` | `#c62828` | 위험 액션 버튼 면, 강한 오류 경계. | +| `danger-soft` | `#fff3f3` | 오류 알림 면, 위험 메뉴 hover. | +| `success-soft` | `#ecfdf5` | 성공 알림 면. | +| `code` / `code-raised` | `#18181b` / `#27272a` | 코드 블록 / 코드 블록 hover. | +| `inverse` | `#27272a` | 어두운 버튼. | +| `scrim` | `#000000` | 모달·드로어 뒤 덮개. 불투명도는 `/25`, `/40` 으로 조절. | + +| 전경 `fg-*` | 값 | 대표 사용처 | +| --------------------------- | --------------------- | ------------------------------------------------------------- | +| `fg-default` | `#0e0e0f` | 제목·핵심 본문, 기본 아이콘. | +| `fg-subtle` | `#52545a` | 보조 본문·라벨. | +| `fg-muted` | `#61646b` | 설명·시간·플레이스홀더·참조 소스 텍스트. | +| `fg-primary` | `#b54700` | 오렌지 글자·링크. **`#ff6900` 은 글자에 쓰지 않는다**(2.9:1). | +| `fg-on-primary` | `#0e0e0f` | 오렌지 면 위 글자·아이콘 (6.68:1). | +| `fg-danger` | `#c62828` | 오류 글자. | +| `fg-success` | `#007a55` | 성공 글자. | +| `fg-code` / `fg-code-muted` | `#efefef` / `#9f9fa9` | 코드 블록 위 강조 / 보조·줄번호. | +| `fg-on-dark` | `#ffffff` | `danger`·`inverse` 면 위 글자. | + +| 선 `line-*` | 값 | 대표 사용처 | +| -------------- | --------- | ----------------------------------------------------------- | +| `line` | `#dfddd8` | 카드·박스 장식 경계, 1px 구분선(`bg-line`). | +| `line-soft` | `#efefef` | 아주 약한 내부 구분선. `ChatComposer` 기본 배경으로도 사용. | +| `line-strong` | `#61646b` | 입력창처럼 식별에 필수적인 경계 (3:1+). | +| `line-primary` | `#e05e00` | 선택 테두리, 입력 focus, AI 아바타 테두리, 스피너 (3.64:1). | +| `line-danger` | `#f1b5b5` | 옅은 오류 경계. | +| `line-success` | `#a4f4cf` | 성공 알림 경계. | +| `line-code` | `#27272a` | 코드 블록 경계. | **색 조합 원칙** -- 사용자 말풍선은 `bg-primary-soft` + `text-text-base` 조합. 테두리는 두지 않는다. -- AI 답변 카드는 `bg-surface` + `text-text-base` + 테두리 없음. -- 활성 채팅 아이템은 `bg-primary-soft` + `rounded-[6px]`. 좌측 오렌지 stripe 는 사용하지 않는다. -- 링크는 `text-accent-strong` + `font-medium` + 상시 밑줄. -- 밝은 면 위 작은 텍스트를 오렌지로 쓸 때는 `accent-strong` 만 사용한다. `#ff6900` 은 대비 부족으로 body 텍스트에 쓰지 않는다. +- 사용자 말풍선은 `bg-primary-soft` + `text-fg-default` 조합. 테두리는 두지 않는다. +- AI 답변 카드는 `bg-surface` + `text-fg-default` + 테두리 없음. +- 활성 채팅 아이템은 `bg-primary-soft` + `rounded-control`. 좌측 오렌지 stripe 는 사용하지 않는다. +- 링크는 `text-fg-primary` + `font-medium` + 상시 밑줄. +- 오렌지는 용도에 따라 셋으로 나뉜다: 면 `primary`(`#ff6900`), 선 `line-primary`(`#e05e00`), 글자 `fg-primary`(`#b54700`). ### 타이포그래피 - 폰트: **Pretendard Variable**. `main.tsx` 에서 `pretendard/dist/web/variable/pretendardvariable-dynamic-subset.css` 를 import 한다. 새 폰트 파일이나 CDN 을 추가하지 않는다. -- 기본 스케일: `--font-size-base: 0.8125rem` (= 13px). 사용자 글자 확대 옵션 `data-font-size="large"` 는 `0.9375rem` (= 15px). 모든 크기 토큰은 base 에 비례하는 rem 값으로 계산되므로 px 를 코드에 박지 않는다. - -| 역할 | Tailwind 유틸리티 | 계산 크기 (base 13px) | 굵기 | 대표 사용처 | -| ---------------- | -------------------------- | --------------------- | ------- | ------------------------------------------- | -| 아주 작은 라벨 | `text-ui-10` | 8.1px | 400–500 | 시간·상태 마이크로 텍스트, 액션 버튼 라벨. | -| 작은 라벨 | `text-ui-11` | 8.9px | 400 | 사이드바 sync 상태 인라인. | -| 보조 텍스트 | `text-ui-12` | 9.8px | 400 | 참조 소스 카드 헤더·항목, 로딩 안내. | -| 내비·컨트롤 | `text-ui-14` | 11.4px | 400–500 | 채팅 리스트 항목, ChatComposer 입력 텍스트. | -| 답변·말풍선 본문 | `text-ui-16` / `text-base` | 13px | 400–500 | 사용자 말풍선, AI MarkdownAnswer 본문. | -| 화면 제목 | `text-ui-20` | 16.3px | 600 | 채팅 제목 헤더. | -| 인증 화면 헤딩 | `text-ui-32` | 26px | 500 | 로그인·회원가입의 큰 문구. | - -- 행간 기본 `leading-[1.5]`. 답변 본문과 말풍선은 `leading-[1.6]` 으로 조금 넉넉하게. +- 기본 스케일: `--font-size-base: 0.8125rem` (= 13px). 사용자 글자 확대 옵션 `data-font-size="large"` 는 `0.9375rem` (= 15px). 모든 크기 토큰은 base 를 따라가므로 px 를 코드에 박지 않는다. 예외는 iOS 자동확대 방지용 `max-sm:text-[16px]`(ChatComposer) 하나. +- 역할 이름 7단계. 토큰에 줄간격이 함께 들어 있다. 최소 크기는 10px. + +| 토큰 | 크기 (base 13 / 15) | 줄간격 | 대표 사용처 | +| -------------- | ------------------- | ------ | ------------------------------------------------- | +| `text-micro` | 10 / 12px | 1.4 | 배지·카운트, 메시지 액션, 코드 블록 헤더. | +| `text-caption` | 11 / 13px | 1.4 | 메타·타임스탬프, 참조 소스, 로딩 안내, sync 상태. | +| `text-label` | 12 / 14px | 1.45 | 버튼·메뉴·채팅 리스트, ChatComposer 입력, 코드. | +| `text-body` | 13 / 15px | 1.5 | 답변·말풍선 본문, 인증 입력. | +| `text-title` | 16.25 / 18.75px | 1.4 | 채팅 제목 헤더, 모달 제목. | +| `text-heading` | 19.5 / 22.5px | 1.3 | 페이지 제목. | +| `text-display` | 26 / 30px | 1.2 | 인증 화면·프로젝트 목록의 큰 문구. | + +- 굵기: 본문 400, 버튼·라벨·선택 상태 500, 제목·표 헤더 600. 700 은 쓰지 않는다. +- 답변 본문과 말풍선은 `leading-[1.6]` 으로 조금 넉넉하게. - 한국어 본문 자간은 기본값을 그대로 사용한다. 제목만 필요할 때 `-0.02em` 까지 허용. - 코드 블록은 시스템 고정폭 폰트 + 다크 테마 (`prism-react-renderer` 의 `themes.oneDark`). ### 간격·형태·표면 -- 기본 간격 단위 4px. 주로 4·8·12·16·20·24·32px 를 사용한다. Tailwind `p-3` = 12px, `p-4` = 16px 등을 그대로 활용. -- 라운딩: - - 내비·버튼·입력창: `rounded-md` (= 6px). - - 참조 소스 카드·코드 블록: `rounded-[8px]`. - - 말풍선·AI 답변 카드: `rounded-[12px]`. - - 사이드바 트리거·소형 아이콘 배지: `rounded-[6px]`. - - 메인 채팅 카드 (사이드바 옆 흰 영역): `rounded-[16px]`. - - 아바타는 `rounded-full` 원형만 사용한다. -- 버튼은 **알약형 금지**. 6px 사각 라운딩만 쓴다. +- 기본 간격 단위 4px (Tailwind 기본). 주로 4·8·12·16·24px 를 사용한다. +- 라운딩은 역할 이름을 쓴다: + +| 토큰 | 값 | 대표 사용처 | +| ----------------- | ---- | ----------------------------------------------- | +| `rounded-inline` | 4px | 인라인 코드, 메시지 액션, 작은 배지. | +| `rounded-control` | 6px | 버튼·입력창·메뉴 아이템·활성 채팅·ChatComposer. | +| `rounded-card` | 8px | 참조 소스 카드, 팝오버. | +| `rounded-panel` | 12px | 말풍선·AI 답변 카드·모달. | +| `rounded-shell` | 16px | 메인 채팅 카드, 인증 폼 카드, 바텀시트. | +| `rounded-full` | — | 아바타·원형 인디케이터만. | + +- 버튼은 **알약형 금지**. `rounded-control` 만 쓴다. - 콘텐츠 세로 간격은 12–24px 를 기본으로 하고, 메시지 사이는 `gap-6` (= 24px). -- **레이아웃**: 캔버스 `bg-app-bg` 위에 사이드바(240px, 무테두리) 와 흰색 메인 카드(`rounded-[16px] border border-line bg-surface`)가 떠 있는 구조. 메인 카드의 우측·상하 여백 12px (`main p-3 pl-0`) 로 캔버스가 보이도록 한다. -- 그림자와 그라데이션은 최소화. 필요한 경우는 다음 두 가지뿐: - - 채팅 제목 헤더 아래 스크롤 페이드: `bg-gradient-to-b from-surface to-transparent` 16px 오버레이. - - 어떤 요소도 heavy shadow (예: `shadow-lg`) 를 새로 도입하지 않는다. +- **레이아웃**: 캔버스 `bg-canvas` 위에 사이드바(240px, 무테두리) 와 흰색 메인 카드(`rounded-shell border border-line bg-surface`)가 떠 있는 구조. 메인 카드의 우측·상하 여백 12px (`main p-3 pl-0`) 로 캔버스가 보이도록 한다. +- 그림자는 `shadow-overlay` 하나뿐이다. 드롭다운·컨텍스트 메뉴·팝오버처럼 떠서 다른 콘텐츠를 가리는 요소에만 쓴다. 그라데이션은 채팅 제목 헤더 아래 스크롤 페이드(`bg-gradient-to-b from-surface to-transparent` 16px) 하나. - 아이콘은 기존 SVGR 레지스트리와 `Icon` 컴포넌트를 사용한다. - 애니메이션은 상태 전달에 한정한다: 스트리밍 점(`LoadingDots`), 오렌지 진행 바(`bg-primary/60`) 정도. 지속적인 장식 움직임은 쓰지 않는다. -## Components — 컴포넌트 기준 +## Components — 화면 구성 기준 + +ui 컴포넌트 하나하나의 쓸 때·피할 때·옵션·쓰는 토큰은 [`docs/registry.md`](docs/registry.md) 가 기준이다(각 `ui/*.meta.ts` 와 JSDoc 에서 생성). 이 절은 컴포넌트를 조합한 **화면 규칙**만 다룬다. ### AppShell 레이아웃 -- 좌측 240px 사이드바(`bg-sidebar`, **우측 경계선 없음**) + 우측 메인 영역(`bg-app-bg p-3 pl-0`). 메인 자식이 흰색 rounded-16 카드로 감싸진다. +- 좌측 240px 사이드바(`bg-sidebar`, **우측 경계선 없음**) + 우측 메인 영역(`bg-canvas p-3 pl-0`). 메인 자식이 흰색 rounded-16 카드로 감싸진다. - 사이드바 내부 구조 (위→아래): - 1. `DrawerHeader` — 두 줄. 첫 줄: `[favicon 20px] [QodeLogo.svg h-4]`. 둘째 줄: `[ProjectSwitcher 트리거][설정 아이콘 IconButton]`. 여백 `gap-2 pt-4 px-3 pb-3`. - 2. Sync 버튼 (OWNER 에게만) — `w-full h-9 rounded-md border border-line bg-surface`, 좌측 아이콘+`동기화`, 우측 상태 텍스트(`text-ui-11 text-text-soft`). 채팅창 본문에는 동기화 시각을 추가로 표시하지 않는다. - 3. `내 채팅` 섹션 헤더 + 채팅 리스트. 활성 항목: `bg-primary-soft rounded-[6px]` + `text-text-base font-medium`. **좌측 stripe 없음**. + 1. `DrawerHeader` — 두 줄. 첫 줄: ``. 둘째 줄: `[ProjectSwitcher 트리거][설정 IconButton outline md]`. 여백 `gap-2 pt-4 px-3 pb-3`. + 2. Sync 버튼 (OWNER 에게만) — `w-full h-9 rounded-control border border-line bg-surface`, 좌측 아이콘+`동기화`, 우측 상태 텍스트(`text-micro text-fg-muted`). 채팅창 본문에는 동기화 시각을 추가로 표시하지 않는다. + 3. `내 채팅` 섹션 헤더 + 채팅 리스트. 활성 항목: `bg-primary-soft rounded-control` + `text-fg-default font-medium`. **좌측 stripe 없음**. 4. `팀 채팅` 섹션 헤더 + 채팅 리스트 (capability 따라 표시). 5. Flex `spacer` — 다음 프로필 행을 하단으로 밀어냄. 6. Profile Row — `px-3` outer + inner `border-t border-line pt-3 pr-0 pb-4 pl-1`. `[UserAvatar brand][이름][⋮]`. Border-t 가 사이드바 좌우를 꽉 채우지 않고 양옆 여백을 두는 게 규칙. -### DrawerHeader - -- 첫 줄에 브랜드 마크: `/favicon.ico` (원형 20px) + `/QodeLogo.svg` (h-4 오토 폭). SVG 로고를 직접 텍스트로 대체하지 않는다. -- 둘째 줄은 `flex items-center gap-2` — `ProjectSwitcher` 가 flex-1, 설정 IconButton 은 `size-9 rounded-md border border-line bg-surface`. - -### ProjectSwitcher - -- 트리거: `h-9 w-full rounded-md border border-line bg-surface px-2` + hover `bg-surface-muted`. -- 좌측에 **선택된 프로젝트 첫 글자 아바타**: `size-5 rounded-[6px] border border-line bg-surface text-[11px] font-semibold text-text-subtle`. (드롭다운 항목 아바타와 동일 스타일.) -- 우측 ▾ 셰브런: 열림 시 180° 회전 + `text-text-subtle`. -- 드롭다운: 트리거 아래 8px, `w-full min-w-[220px] rounded-md border border-line bg-surface py-1`. 항목 padding `px-2 py-1.5` 로 트리거 아이콘 위치와 정렬 유지. 현재 선택 항목은 `bg-surface-muted`. 하단 `+ 새 프로젝트` 링크 유지. - -### Button (`Button.tsx`) - -- 공통: `inline-flex items-center justify-center gap-2 rounded-md border font-medium transition-colors`. `rounded-md` (6px), 알약 금지. -- 사이즈: `sm`=`h-9 px-3 text-xs`, `md`=`h-11 px-4 text-sm`. -- Variant: - - `primary`: `border-primary bg-primary text-primary-foreground hover:brightness-95`. 그라데이션 없음. - - `secondary`: `border-line bg-surface text-text-base hover:bg-surface-muted`. `border-line`(장식 border) 사용, `control-line` 을 쓰지 않는다. - - `ghost`: `border-transparent bg-transparent text-text-subtle hover:bg-surface-muted`. - - `danger`: `border-danger bg-danger text-white hover:brightness-95`. -- Focus: `focus-visible:ring-2 focus-visible:ring-text-base focus-visible:ring-offset-2`. -- Disabled: `disabled:cursor-not-allowed disabled:opacity-60`. -- 로딩 시 좌측에 3.5px spinner 를 넣고 `aria-busy` 를 세팅한다. - -### IconButton / TextField / OverlayModal - -- 같은 토큰·포커스·disabled 규칙을 재사용한다. -- 입력창 border 는 `control-line` (#61646b). 아이콘 버튼은 장식 border (`line`) 사용. - ### LoginPage / SignupPage (인증 화면) -- 중앙 정렬 `max-w-[520px]` 컨테이너, `gap-8`. 상단 타이틀 블록(`text-ui-32 font-medium`), 아래 폼 카드(`max-w-[360px] rounded-[20px] border border-line bg-surface p-6`). -- 입력창: `h-10 w-full rounded-md border border-control-line bg-surface px-3 text-base font-medium`. Focus: `border-2 border-text-base`. 오류: `border-danger`. -- 제출 버튼: `h-10 w-full rounded-md bg-primary px-3 text-base font-medium text-primary-foreground`. 알약 금지. -- 하단 링크: `text-sm text-text-subtle` + `Link` 컴포넌트 (`font-medium text-accent-strong underline`). - -### ChatComposer - -- 컨테이너: `flex items-end gap-2 rounded-[6px] bg-line-soft px-3 py-2 transition-colors focus-within:bg-primary-soft`. **테두리 없음, 배경 변화로 focus 표시**. -- textarea: 컨테이너 내부, `flex-1 border-0 bg-transparent text-ui-14 leading-[1.6]`. 브라우저 기본 outline 은 `style={{ outline: 'none', boxShadow: 'none' }}` 로 제거. 컨테이너의 focus-within 배경 전환이 접근성 focus 표시를 대체한다. -- 전송 버튼: **textarea 옆** (아래가 아님). `size-8` 정사각형 `rounded-[6px] bg-primary`, disabled 시 `bg-line`. 이전 시안의 48px 아래 배치는 폐기. -- 2000자 초과 경고는 컨테이너 밖 아래에 `text-ui-12 text-danger` 로 필요할 때만 노출한다. +- 중앙 정렬 `max-w-[520px]` 컨테이너, `gap-8`. 상단 타이틀 블록(`text-display font-medium`), 아래 폼 카드(`max-w-[360px] rounded-shell border border-line bg-surface p-6`). +- 입력창: `h-10 w-full rounded-control border border-line-strong bg-surface px-3 text-body font-medium`. Focus: `focus:border-line-primary`. 오류: `border-danger`. +- 제출 버튼: `h-10 w-full rounded-control bg-primary px-3 text-body font-medium text-fg-on-primary`. 알약 금지. +- 하단 링크: `text-label text-fg-subtle` + `Link` 컴포넌트 (`font-medium text-fg-primary underline`). ### 채팅 메시지 렌더링 - 뷰포트 최대 폭 `max-w-[48rem]` (`768px`). 메시지 간 간격 `gap-6`. -- 사용자 말풍선 (개인 채팅): `flex justify-end` → `max-w-[70%] rounded-[12px] bg-primary-soft px-3 py-3 text-ui-16 font-medium leading-[1.6] text-text-base`. **테두리 없음**. -- 팀 채팅 본인 말풍선: `bg-primary-soft` 동일 톤, `rounded-xl px-3 py-2.5 text-ui-16`. -- 팀 채팅 상대 말풍선: `border border-line bg-surface rounded-xl` + 좌측 이니셜 아바타. +- 사용자 말풍선 (개인 채팅): `flex justify-end` → `max-w-[70%] rounded-panel bg-primary-soft px-3 py-3 text-body font-medium leading-[1.6] text-fg-default`. **테두리 없음**. +- 팀 채팅 본인 말풍선: `bg-primary-soft` 동일 톤, `rounded-panel px-3 py-2.5 text-body`. +- 팀 채팅 상대 말풍선: `border border-line bg-surface rounded-panel` + 좌측 이니셜 아바타. ### AI 답변 카드 -- 완료 카드: `article rounded-[12px] bg-surface p-3`. **테두리 없음**. 헤더 아래 여백 12px. +- 완료 카드: `article rounded-panel bg-surface p-3`. **테두리 없음**. 헤더 아래 여백 12px. - 스트리밍 카드: 완료 카드와 시각적으로 같다. 테두리를 두지 않는다. 상태 라벨은 헤더 오른쪽에 `· 요청 중...` / `· 스트리밍 중` 형태로 붙인다. -- **AI 아바타** — `inline-flex size-7 items-center justify-center overflow-hidden rounded-full border border-primary bg-surface` + 내부 `/favicon.ico` 이미지 (size-4). 이전 시안의 오렌지 채움 `Q` 원은 폐기. -- 라벨: `text-ui-14 font-semibold text-text-base` = `Qode AI`. +- **AI 아바타** — ``(오렌지 테두리 원 + Qode 심볼). 이전 시안의 오렌지 채움 `Q` 원은 폐기. +- 라벨: `text-label font-semibold text-fg-default` = `Qode AI`. - 스트리밍 표시: - `postPersonalMessage.isPending` 이 true 인 동안만 스트리밍 카드 렌더. onDone 즉시 hide. - streamContent 가 있으면 `MarkdownAnswer` 로 실시간 표시. 없으면 `찾아보는 중이에요...` + `LoadingDots`. - streamSources 는 완료 후 마지막 assistant 카드에 merge 되어 서버 미저장 케이스에서도 카드 유지. -### MarkdownAnswer - -- Wrapper: `.markdown-answer text-base leading-[1.6] text-text-base`. `memo` 로 감싸 스트리밍 중 불필요한 re-render 를 줄인다. -- h1~h4 여백 `margin: 1em 0 0.5em`, p 여백 `margin: 0.6em 0`. 첫 문단은 `font-weight: 500` 로 살짝 강조. -- 인라인 code 는 `bg-surface-muted border border-line-soft rounded-[4px]`. -- 링크는 새 탭 `noopener noreferrer` + `text-accent-strong underline`. -- 코드 펜스는 `CodeBlock` 이 자체 `
` 를 렌더하므로 `
` 래퍼는 벗겨서 중복을 방지한다.
-
-### CodeBlock (다크 테마)
-
-- 컨테이너: `rounded-md overflow-hidden border border-zinc-800 bg-zinc-900`.
-- 헤더: `border-b border-zinc-800 bg-zinc-900 px-3 py-1 text-ui-10`. 좌: 언어 라벨 (`text-zinc-400 font-mono`). 우: `복사` 버튼 (`text-zinc-400 hover:text-zinc-100`).
-- 본문: `prism-react-renderer` + `themes.oneDark`. `text-ui-14 leading-[1.55] p-3`. 배경은 컨테이너 색을 그대로 사용하도록 `style.background: 'transparent'` 로 덮어쓴다.
-- 라인 번호: `w-6 text-right text-zinc-500 mr-3`.
-- 폰트 크기는 본문(text-base = 13px) 보다 살짝 작은 text-ui-14 (~11.4px) 로 두어 코드 가독성과 카드 밀도 균형을 잡는다. 이보다 작으면 (text-ui-12) 코드가 판독이 어려워진다.
-
-### MessageSources (참조 소스 카드)
-
-- 카드: `mt-3 rounded-[8px] border border-line bg-surface`.
-- 헤더: `flex w-full items-center justify-between px-3 py-1.5 text-ui-12 text-text-soft hover:bg-surface-muted`. 좌측 `•` 불릿 마커 + `참조한 소스 N개`. 셰브런은 사용하지 않는다.
-- 본문 리스트: 상단 `border-t border-line-soft`. 각 항목 `flex items-center justify-between gap-3 px-3 py-1 text-ui-12 text-text-soft`.
-- 항목 좌측: `min-w-0 flex-1 truncate` 파일 경로.
-- 항목 우측: `shrink-0` `(start-end)` 형식. 실제 응답값만 표시하고 목데이터를 넣지 않는다.
-
 ### 메시지 액션 (답변 하단)
 
 - 3개 고정: `복사`, `팀 공유`, `팀 채팅 생성`. 순서 유지.
-- 버튼 스타일: `flex items-center gap-[2px] rounded-[4px] px-1 py-[2px] text-ui-10 font-medium text-text-soft hover:bg-surface-muted hover:text-text-base`.
+- 버튼 스타일: `flex items-center gap-[2px] rounded-inline px-1 py-[2px] text-micro font-medium text-fg-muted hover:bg-surface-muted hover:text-fg-default`.
 - 비활성 상태는 `API_CAPABILITIES` 와 권한으로 결정하고, 비활성 시 `title`(툴팁) 로 이유를 안내한다.
 
 ### 채팅 제목 헤더 + 스크롤 페이드
 
-- `flex justify-center bg-surface px-6 py-3` 헤더. 하단 border 없음. 인라인 편집 input 은 `rounded-md border border-control-line`.
+- `flex justify-center bg-surface px-6 py-3` 헤더. 하단 border 없음. 인라인 편집 input 은 `rounded-control border border-line-strong`.
 - 헤더 바로 아래 messages viewport 상단에 `pointer-events-none absolute inset-x-0 top-0 z-10 h-4 bg-gradient-to-b from-surface to-transparent` 오버레이. 스크롤 시 콘텐츠가 헤더 뒤로 부드럽게 페이드된다.
 
-### InlineAlert / Toast
-
-- 오류 원인과 복구 행동을 함께 안내한다. 오렌지를 성공·실패 공통 상태색으로 사용하지 않는다.
-- 재시도 버튼은 alert 안에 `Button size="sm" variant="secondary"` 로 붙인다.
-
-### UserAvatar (`AppShell.tsx` 내부)
-
-- 기본 variant: `border border-line bg-surface-muted font-medium text-text-soft`.
-- brand variant: `bg-primary-soft font-semibold text-primary`. 사이드바 하단 프로필과 프로젝트 아바타 등 브랜드 강조 자리에 사용.
-- `avatarUrl` 있으면 이미지가 우선, 로드 실패 시 이니셜로 fallback. URL 이 바뀌면 실패 플래그를 초기화한다.
-
 ## SSE 스트리밍 렌더 라이프사이클
 
 새 답변 UI 를 만들 때 참고할 상태 규칙:
 
 - 스트리밍 article 렌더는 `postPersonalMessage.isPending || streamError` 만 사용한다. Timestamp gate·useEffect 감지 조합은 지양한다.
-- 스트리밍 카드에는 완료 카드와 같은 시각(테두리 없음, `bg-surface`, `rounded-[12px]`) 을 쓰고, 헤더 상태 라벨만 다르게 붙인다.
+- 스트리밍 카드에는 완료 카드와 같은 시각(테두리 없음, `bg-surface`, `rounded-panel`) 을 쓰고, 헤더 상태 라벨만 다르게 붙인다.
 - 완료된 assistant 카드에서 서버 sources 가 비어있으면 `streamSources` 를 마지막 assistant 메시지에만 merge 한다. 중간 assistant 는 자신의 서버 sources 만 사용.
 - 서버가 body 없이 assistant 자리만 만든 경우 (`content trim === ''` && `sources.length === 0`) 그 카드는 `return null` 로 렌더하지 않는다. 스트리밍 카드가 실제 응답을 계속 보여준다.
 - AI 본문에 인라인으로 남긴 `(참고: `x.ts` 1-17)` / `- 파일 경로: x.ts` / `filepath (L101-116)` 등 참조 표기는 `extractInlineSources` 파서가 뽑아 소스 카드로 옮기고 본문에서 제거한다.
@@ -271,14 +229,14 @@
 - 목표: WCAG 2.1 AA. 이 문서는 준수 인증이 아니라 유지 목표다.
 - 일반 텍스트 대비 4.5:1 이상, 큰 글자·비텍스트 UI 대비 3:1 이상.
 - 알려진 대비값(sRGB):
-  - `#ff6900` / 흰색 = 2.89:1 → 작은 텍스트 금지.
-  - `#ff6900` / `#0e0e0f` = 6.68:1 → 오렌지 버튼 위 진한 글자 OK.
-  - `#b54700` / 흰색 = 5.43:1 → 밝은 면의 링크 OK.
-  - `#b54700` / 크림 = 5.05:1 → 브랜드 강조 텍스트 OK.
-- 밝은 면의 작은 오렌지 텍스트/링크는 `accent-strong` 만 사용한다.
+  - `#ff6900` / 흰색 = 2.89:1 → 글자·선 모두 금지. 면(`primary`)으로만 쓴다.
+  - `#ff6900` / `#0e0e0f` = 6.68:1 → 오렌지 버튼 위 진한 글자(`fg-on-primary`) OK.
+  - `#e05e00` / 흰색 = 3.64:1 → 선택 테두리·입력 focus(`line-primary`) OK.
+  - `#b54700` / 흰색 = 5.43:1, 크림 = 5.05:1 → 오렌지 글자·링크(`fg-primary`) OK.
+- 전체 조합표는 [`docs/tokens/contrast-matrix.md`](./docs/tokens/contrast-matrix.md). 새 fg×bg 조합을 쓰면 `scripts/tokens-check.ts` 의 REQUIRED 에 추가하고 `yarn tokens:check` 로 검증한다.
 - **키보드 포커스 통일 규칙** — 전역 `outline` 규칙은 없음. 요소별 focus 스타일만 사용한다.
-  - **입력창 (input, textarea)**: 보더 색만 변경. Default `border-control-line` → Focus `focus:border-primary`. 에러는 `border-danger` (focus 무시하고 error 우선). `outline-none` 는 브라우저 기본 outline 을 명시적으로 억제한다. Ring 은 얹지 않는다.
-  - **버튼, IconButton, MenuItem, 링크 등 인터랙션 요소**: `focus-visible:ring-2 focus-visible:ring-text-base focus-visible:ring-offset-2` 링 패턴. 키보드 focus 만 링이 보이고 마우스 클릭엔 안 보이도록 `focus-visible` 을 명시적으로 쓴다.
+  - **입력창 (input, textarea)**: 보더 색만 변경. Default `border-line-strong` → Focus `focus:border-line-primary`. 에러는 `border-danger` (focus 무시하고 error 우선). `outline-none` 는 브라우저 기본 outline 을 명시적으로 억제한다. Ring 은 얹지 않는다.
+  - **버튼, IconButton, MenuItem, 링크 등 인터랙션 요소**: `focus-visible:ring-2 focus-visible:ring-fg-default focus-visible:ring-offset-2` 링 패턴. 키보드 focus 만 링이 보이고 마우스 클릭엔 안 보이도록 `focus-visible` 을 명시적으로 쓴다.
   - **ChatComposer textarea (예외)**: 컨테이너 `focus-within:bg-primary-soft` 배경 전환으로 시각 포커스를 대체. textarea 자체 outline 은 style 인라인으로 완전히 제거. 이 예외를 다른 곳에 확장하지 않는다.
   - 위 세 규칙 밖의 새 컴포넌트는 기본적으로 입력창 패턴 또는 링 패턴 둘 중 하나를 골라 쓴다. 두 방식을 한 요소에 섞지 않는다.
 - 색상 외에 텍스트/형태로도 선택·오류·비활성을 구분한다.
@@ -290,30 +248,32 @@
 ## Responsive behavior — 화면 크기 대응
 
 - 우선 대상은 Electron 과 데스크톱 웹.
-- 사이드바 240px 고정. 메인 영역은 크림 캔버스 위에 `rounded-[16px]` 흰 카드로 부유한다. 대화 본문 최대 폭은 `max-w-[48rem]` (768px).
+- 사이드바 240px 고정. 메인 영역은 크림 캔버스 위에 `rounded-shell` 흰 카드로 부유한다. 대화 본문 최대 폭은 `max-w-[48rem]` (768px).
 - 좁은 창의 사이드바 전환 방식은 후속 검증 항목.
 - 긴 제목은 `truncate`, 파일 경로는 소스 카드 내부에서 truncate. 코드 블록은 자체 가로 스크롤을 갖는다.
 - hover 로 드러나는 작업은 키보드 focus 로도 접근 가능하게 한다.
 
 ## Interaction states — 상태별 표현
 
-| 상태                    | 표현과 행동                                                                           |
-| ----------------------- | ------------------------------------------------------------------------------------- |
-| 초기 로딩               | 해당 영역의 로딩 안내 (`text-ui-12 text-text-soft`), 전체 화면 교체 지양.             |
-| 프로젝트·대화 없음      | 현재 맥락과 실제 가능한 다음 행동 안내.                                               |
-| 답변 요청 직후          | 스트리밍 카드에 `찾아보는 중이에요...` + `LoadingDots`.                               |
-| 답변 스트리밍 중        | 스트리밍 카드에서 토큰이 실시간으로 축적. 상태 라벨 `· 스트리밍 중`.                  |
-| 답변 완료               | `isPending: false` 즉시 스트리밍 카드 hide. 완료 assistant 카드로 자연 전환.          |
-| 답변 실패               | 스트리밍 카드에 `text-danger` 메시지 + `Button variant="secondary" size="sm"` 재시도. |
-| 동기화 대기·진행        | 사이드바 sync 버튼 비활성 + `동기화 진행 중` 인라인 텍스트. 실제 진행률만 사용.       |
-| 동기화 성공             | 서버 `lastSyncedAt` 을 상대 시간으로 표시 (`마지막 동기화: N분 전`).                  |
-| 동기화 실패             | `지난 동기화 실패` 인라인 + 상단 `InlineAlert` + 재시도.                              |
-| 조회·전송 실패          | 기존 내용을 보존하고 해당 요청의 복구 행동만 안내한다.                                |
-| 권한 부족·미지원        | 실제 권한·`API_CAPABILITIES` 에 따른 숨김 또는 비활성. 비활성 시 이유 툴팁.           |
-| 느린 네트워크·연결 끊김 | 팀 채팅 상단에 spinner + `연결이 끊어졌습니다 / 재연결 중...` 배너.                   |
+| 상태                    | 표현과 행동                                                                              |
+| ----------------------- | ---------------------------------------------------------------------------------------- |
+| 초기 로딩               | 해당 영역의 로딩 안내 (`text-caption text-fg-muted`), 전체 화면 교체 지양.               |
+| 프로젝트·대화 없음      | 현재 맥락과 실제 가능한 다음 행동 안내.                                                  |
+| 답변 요청 직후          | 스트리밍 카드에 `찾아보는 중이에요...` + `LoadingDots`.                                  |
+| 답변 스트리밍 중        | 스트리밍 카드에서 토큰이 실시간으로 축적. 상태 라벨 `· 스트리밍 중`.                     |
+| 답변 완료               | `isPending: false` 즉시 스트리밍 카드 hide. 완료 assistant 카드로 자연 전환.             |
+| 답변 실패               | 스트리밍 카드에 `text-fg-danger` 메시지 + `Button variant="secondary" size="sm"` 재시도. |
+| 동기화 대기·진행        | 사이드바 sync 버튼 비활성 + `동기화 진행 중` 인라인 텍스트. 실제 진행률만 사용.          |
+| 동기화 성공             | 서버 `lastSyncedAt` 을 상대 시간으로 표시 (`마지막 동기화: N분 전`).                     |
+| 동기화 실패             | `지난 동기화 실패` 인라인 + 상단 `InlineAlert` + 재시도.                                 |
+| 조회·전송 실패          | 기존 내용을 보존하고 해당 요청의 복구 행동만 안내한다.                                   |
+| 권한 부족·미지원        | 실제 권한·`API_CAPABILITIES` 에 따른 숨김 또는 비활성. 비활성 시 이유 툴팁.              |
+| 느린 네트워크·연결 끊김 | 팀 채팅 상단에 spinner + `연결이 끊어졌습니다 / 재연결 중...` 배너.                      |
 
 ## Content voice — 문구
 
+상황별 톤·용어집·전후 예시는 [`docs/copy.md`](docs/copy.md) 가 기준이다(해요체, 말줄임표 `…`).
+
 - 한국어 기본, 짧고 구체적인 문장.
 - 버튼은 실행 결과를 말한다: `동기화`, `다시 시도`, `새 채팅`, `이름 바꾸기`, `팀 공유`, `팀 채팅 생성`.
 - 같은 동작은 흐름 전체에서 같은 이름으로 부른다.
@@ -327,36 +287,35 @@
 - 렌더러는 Electron 전용 API 에 의존하지 않는다. 해시 라우팅, API·SSE·Socket 동작, 인증 정책을 보존한다.
 - API 생성 파일 (`src/renderer/src/api/generated/`) 은 직접 수정하지 않는다.
 - **React Compiler 규칙**: 새 컴포넌트에서 수동 `useMemo` / `useCallback` 을 남발하지 않는다. Compiler 가 자동 메모이제이션을 해주므로 필요한 경우에만 사용.
-- 스타일 구현 후에는 `yarn typecheck` + 대상 파일 스코프 `yarn eslint` 를 실행한다.
+- 스타일 구현 후에는 `yarn typecheck` + `yarn tokens:check` + 대상 파일 스코프 `yarn eslint` 를 실행한다.
 
 ## 새 컴포넌트 추가 절차
 
-1. 기존 컴포넌트로 표현 가능한지 먼저 확인한다. 가능하면 확장을 우선.
+1. [`docs/registry.md`](docs/registry.md) 에서 기존 컴포넌트로 표현 가능한지 먼저 확인한다. 가능하면 확장을 우선.
 2. 사용하는 색·크기·간격이 위 토큰과 일치하는지 검토한다. 어긋나면 이 문서에 새 규칙을 추가하고 코드를 맞춘다.
 3. 접근성 체크: label, focus 표시, 색상 외 구분 수단, keyboard 조작 가능 여부.
 4. 스트리밍/비동기 상태가 있는 UI 는 위 **SSE 스트리밍 렌더 라이프사이클** 규칙을 따른다.
 5. 테스트 시 최소 4가지 상태를 봐야 한다: 기본 / hover · focus / disabled · loading / 오류.
+6. `ui/` 에 추가했다면 JSDoc(✅ Use / ❌ Don't) + `Name.meta.ts` + 스토리를 함께 만들고 `yarn tokens:check` 로 registry 를 다시 생성한다.
 
 ## 시안·구현 검토 체크리스트
 
+공통 항목(토큰·재사용·상태·접근성·안티패턴)은 [`docs/ai-review-checklist.md`](docs/ai-review-checklist.md). 아래는 Qode 화면 전용 항목이다.
+
 - [ ] 주요 과제(질문·답변)가 시각적으로 가장 분명하고 동일 정보가 중복되지 않는다.
 - [ ] 사이드바 섹션이 숨겨져 있고, sync 버튼이 w-full + 인라인 상태 텍스트로 표시된다.
 - [ ] `내 채팅` · `팀 채팅` 헤더는 `+` 버튼만 있고 접기 화살표·`+ 추가` 링크가 없다.
 - [ ] 사이드바 하단 프로필 행은 아바타·이름·`⋮` 만 노출한다. Border-t 가 좌우 여백을 두고 시작한다.
-- [ ] 완료 답변·스트리밍 답변 모두 오렌지 테두리 원 + favicon 아바타 + `Qode AI` 라벨을 쓴다.
-- [ ] 참조 소스 카드는 `•` 불릿 + `text-ui-12 text-text-soft` + 좌(경로) / 우(줄 번호) 양쪽 정렬.
+- [ ] 완료 답변·스트리밍 답변 모두 오렌지 테두리 원 + Qode 심볼 아바타 + `Qode AI` 라벨을 쓴다.
+- [ ] 참조 소스는 `SourceList` 로만 표시한다(같은 파일 묶음, 파일명 강조 + 흐린 폴더 + 오른쪽 줄 범위).
 - [ ] 답변 하단 액션은 `복사`·`팀 공유`·`팀 채팅 생성` 3개 순서 유지, capability 에 맞춘 비활성 + 툴팁.
-- [ ] 인증 화면 버튼·입력창은 `rounded-md` (6px), 알약 없음.
+- [ ] 인증 화면 버튼·입력창은 `rounded-control` (6px), 알약 없음.
 - [ ] ChatComposer 컨테이너 배경은 기본 `bg-line-soft`, focus-within 시 `bg-primary-soft`. 테두리·outline 이중 표시 없음. 전송 버튼은 textarea 옆.
-- [ ] 메인 채팅 영역이 크림 캔버스 위에 `rounded-[16px]` 흰 카드로 부유한다.
+- [ ] 메인 채팅 영역이 크림 캔버스 위에 `rounded-shell` 흰 카드로 부유한다.
 - [ ] 스트리밍 article 이 `postPersonalMessage.isPending` false 되는 즉시 사라진다. 완료 카드와 겹치는 순간이 없다.
 - [ ] AI 답변 카드에 테두리 없음 (완료·스트리밍 모두).
-- [ ] 미구현 기능이나 가공한 운영 지표가 추가되지 않았다.
-- [ ] 빈 화면·로딩·생성 중·실패·비활성 상태가 정의되어 있다.
 - [ ] 긴 한국어 제목·긴 답변·긴 파일 경로에서 레이아웃이 유지된다.
 - [ ] 글자 확대(`data-font-size='large'`) 와 키보드 조작에서 입력창·메뉴·동기화에 접근 가능하다.
-- [ ] 기본·hover·focus 상태의 대비와 토큰 참조를 확인했다.
-- [ ] 같은 요소의 재구현 없이 기존 컴포넌트를 재사용한다.
 
 ## Open questions
 
diff --git a/SYSTEM.md b/SYSTEM.md
new file mode 100644
index 0000000..d9aba39
--- /dev/null
+++ b/SYSTEM.md
@@ -0,0 +1,110 @@
+# Quire
+
+> **Every decision, remembered.**
+
+- 상태: **Draft** (2026-09-26) — Phase A(정체성 & 브랜딩) 결정 반영. 팀 리뷰 후 확정한다.
+- 갱신일: 2026-09-26. (로고 확정)
+- 관계: 이 문서는 Qode 디자인 시스템의 상위 정체성 문서다. 시각·화면 규칙은 [`DESIGN.md`](./DESIGN.md), 컴포넌트 규칙은 [`docs/registry.md`](./docs/registry.md), 실제 토큰 값은 `src/renderer/src/assets/main.css`가 기준이다.
+
+---
+
+## 1. 정의
+
+**Quire는 Qode를 만드는 사람과 AI가 같은 결정을 두 번 내리지 않도록, 결정과 그 이유를 기록해 두는 디자인 시스템이다.**
+
+토큰·컴포넌트·패턴을 모아 두는 데서 그치지 않는다. 각 요소가 왜 그렇게 정해졌는지, 언제 쓰고 언제 피하는지를 함께 남긴다. 독자는 사람과 AI 둘 다다.
+
+## 2. 코어 밸류
+
+우선순위 순서다. 두 밸류가 부딪히면 앞의 것을 따른다.
+
+### ① Context — 맥락
+
+결정에는 이유를, AI에게는 맥락을 남긴다. 관련된 정보는 가까이 둔다.
+
+- 시스템에서: 토큰은 값이 아니라 의미로, 컴포넌트는 이름이 아니라 "언제 쓰고 언제 피하는지"로 기술한다. 패턴에는 결정 트리와 실제 결정 기록을 붙인다.
+- 판단 질문: _6개월 뒤의 팀원이나 AI가 이걸 보고 왜 이렇게 됐는지 알 수 있는가?_
+
+### ② Trust — 신뢰
+
+실제 상태와 근거만 표현하고 과장하지 않는다. 오류가 나도 다음에 무엇을 해야 하는지 알 수 있어야 한다.
+
+- 시스템에서: 상태 표시·참조 소스 표기·복구 안내가 필수 요소다. 임의 통계, 장식용 배지, 미구현 기능을 암시하는 컨트롤은 만들지 않는다.
+- 판단 질문: _화면에 보이는 것이 지금 실제로 사실인가?_
+
+### ③ Density — 밀도
+
+개발 도구답게 정보량은 넉넉하게, 그러면서도 차분하고 읽기 편하게.
+
+- 시스템에서: 작업 화면에 랜딩 페이지식 대형 여백을 쓰지 않는다. 여백보다 리듬으로 정리하고, 강조는 중요도에 비례해 아낀다.
+- 판단 질문: _이 화면은 한눈에 더 많이 보여주면서도 시끄럽지 않은가?_
+
+> **Calm은 어디로 갔나** — 후보였던 Calm(차분함)은 독립 밸류로 두지 않고 Density 안에 넣었다. Quire에서 차분함은 목표라기보다, 밀도를 높일 때 지켜야 하는 조건이다. Qode 앱의 "차분하고 신뢰감 있는" 브랜드 톤은 그대로 유지된다.
+
+## 3. 이름 & 유래
+
+**Quire** /ˈkwaɪər/ — 종이를 접어 묶은 한 첩(약 24장). 책을 제본할 때 쓰는 기본 단위다.
+
+- 문서화, 그리고 코드와 문서를 뒤적이며 오가는 일에 가장 가까운 단어라서 골랐다.
+- 낱장(토큰·컴포넌트)을 묶어 하나의 기록(시스템)을 만든다는 서사가 Context 밸류와 맞닿는다.
+- Q로 시작해 Qode와 첫 글자를 공유한다.
+- 비교했던 후보: Quilt(조각보), Qube(블록), Compass(나침반). 최상위 밸류가 Compose(조립)가 아니라 Context(맥락)로 정해지면서 기록의 은유를 택했다.
+
+## 4. Qode와의 거리
+
+> 원칙: _Qode에서 출발하되 Qode에 갇히지 않는다._
+
+| 항목      | 결정                              | 내용                                                                                   |
+| --------- | --------------------------------- | -------------------------------------------------------------------------------------- |
+| 컬러 계승 | **오렌지만 유지**                 | `#ff6900`만 물려받는다. 서브 컬러는 도입하지 않는다.                                   |
+| 톤 거리   | **살짝 벌림**                     | 앱은 따뜻한 작업 공간, 시스템 문서는 조금 더 전문가향(구조적·기술적).                  |
+| 로고 관계 | **독립 심볼, 오렌지만 공유**      | Qode 로고를 변형하지 않은 별도 심볼. 이어짐은 오렌지 `#ff6900` 한 조각으로만. §5 참고. |
+| 태그라인  | **"Every decision, remembered."** | Context 서사. Quire(기록의 묶음)라는 이름과 짝을 이룬다.                               |
+
+## 5. 시각 정체성 방향 (컨셉만)
+
+실제 값은 Phase B(파운데이션 재편)에서 확정한다. 여기서는 방향만 정한다.
+
+- **시그니처 컬러**: `#ff6900` 단일 액센트. 강조는 오렌지 하나로만 한다.
+- **문서 바탕**: 웜 그레이 모노톤. 앱의 크림(`#faf6f1`)에서 채도를 뺀 뉴트럴 계열로, 부모 브랜드와 이어지면서도 더 차분하고 기술적으로 보이게 한다. (참고 후보: 웜 그레이 `#7a7368`)
+- **타이포**: Pretendard Variable을 계승한다.
+- **로고**: 확정 (2026-09-26) — 도브테일(Dovetail) 심볼 + `Quire` 워드마크.
+
+  ![Quire lockup](./docs/assets/quire/quire-lockup.png)
+
+  | 심볼                                                                            | 설명                                                                                                                                                                                                         |
+  | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+  | Quire 심볼 | 육각형을 이루는 검정 두 조각이 도브테일 이음처럼 맞물리고, 그 사이를 오렌지 쐐기 하나가 잇는다. 낱장을 묶어 한 첩을 만드는 Quire 의 서사, 그리고 Qode 에서 물려받은 유일한 색(`#ff6900`)을 한 형태에 담는다. |
+  - 구성: 검정(`#171717` 근처, 원본 측정) 두 조각 + 오렌지 `#ff6900` 하나. 다른 색을 더하지 않는다.
+  - 바탕: 크림·밝은 배경 위. 어두운 배경용 반전판은 필요해지면 만든다.
+  - 파일: `docs/assets/quire/quire-lockup.png`(1200×600), `quire-symbol.png`(512×512) — 문서용 PNG. 벡터(SVG) 원본은 아직 없다.
+
+## 6. 이후 로드맵
+
+| Phase | 이름                 | 요약                                                                                           | 상태           |
+| ----- | -------------------- | ---------------------------------------------------------------------------------------------- | -------------- |
+| **A** | 정체성 & 브랜딩      | 이 문서                                                                                        | 완료           |
+| B     | 파운데이션 재편      | 토큰 3-tier(raw → semantic → component), 색상 역할 분리, 접근성 매트릭스, 타이포·radius 명시화 | 완료 (#53)     |
+| C     | 컴포넌트 정비        | 인벤토리·상태 판정, API 컨벤션, "언제 쓰고 언제 피하나" 명세                                   | 완료 (#54~#56) |
+| D     | 패턴 & 의사결정 기록 | 확인·피드백·에러 등 반복 패턴의 결정 트리와 Do/Don't                                           | 완료 (#57)     |
+| E     | AI 컨텍스트          | `DESIGN.md` 스키마 표준화, 컴포넌트 registry, AI 검수 체크리스트                               | 완료 (#58)     |
+| F     | 화면 밖 경험         | 모션 토큰, 카피 톤, loading/error/empty/success 표준                                           | 완료 (#59)     |
+| G     | 운영                 | 변경 프로세스, Storybook addon, SoT 우선순위 규칙                                              | 완료           |
+
+Phase B~G의 결정도 Context 밸류에 따라 결정 사항과 이유를 함께 남긴다.
+
+## 7. 관련 문서
+
+| 문서                                                           | 담당                  | 관계                                                            |
+| -------------------------------------------------------------- | --------------------- | --------------------------------------------------------------- |
+| `SYSTEM.md`                                                    | 정체성·철학·로드맵    | 최상위. 다른 문서와 철학이 어긋나면 여기를 먼저 검토한다.       |
+| [`DESIGN.md`](./DESIGN.md)                                     | 시각·화면 규칙        | 브랜드·시각 언어·화면 구성·접근성 기준서.                       |
+| [`docs/foundations.md`](./docs/foundations.md)                 | 토큰 의미             | 토큰 이름과 쓰임.                                               |
+| [`docs/registry.md`](./docs/registry.md)                       | 컴포넌트 규칙         | `ui/*.meta.ts` 에서 생성. 쓸 때·피할 때·옵션·토큰.              |
+| [`docs/process/`](./docs/process/change-flow.md)               | 운영                  | 변경 흐름·문서 우선순위·상태 헤더 규칙.                         |
+| [`docs/copy.md`](./docs/copy.md)                               | 화면 문구             | 해요체·상황별 톤·용어집. 🤖 일부 자동 검사.                     |
+| [`docs/patterns/`](./docs/patterns/README.md)                  | 패턴 결정             | 확인·에러·피드백·빈 상태·로딩.                                  |
+| [`docs/ai-review-checklist.md`](./docs/ai-review-checklist.md) | 검수                  | 머지 전 체크리스트. 🤖 항목은 `yarn tokens:check` 가 자동 검사. |
+| `src/renderer/src/assets/main.css`                             | 토큰 실제 값          | 값의 기준.                                                      |
+| [`AGENTS.md`](./AGENTS.md)                                     | AI 에이전트 작업 안내 | 코드 구조·명령어. UI 작업 전 읽을 Quire 문서를 안내한다.        |
+| [`README.md`](./README.md)                                     | 프로젝트 소개         | —                                                               |
diff --git a/docs/ai-review-checklist.md b/docs/ai-review-checklist.md
new file mode 100644
index 0000000..17a4433
--- /dev/null
+++ b/docs/ai-review-checklist.md
@@ -0,0 +1,76 @@
+# AI 검수 체크리스트
+
+- 상태: **In sync** (2026-09-26)
+
+> UI 를 만들거나 고친 뒤, 사람이든 AI 든 머지 전에 이 순서로 확인한다.
+> 🤖 = `yarn tokens:check` 가 자동으로 잡는 항목. 나머지는 눈으로 본다.
+
+## 0. 시작 전에 읽었나
+
+- [ ] [`docs/registry.md`](./registry.md) 에서 쓸 컴포넌트를 찾았다 — "피할 때"에 걸리면 → 로 안내된 것을 쓴다
+- [ ] 상황이 패턴에 해당하면 [`docs/patterns/`](./patterns/README.md) 의 결정 트리를 따랐다
+- [ ] 새 색·크기·모션이 필요하면 [`docs/foundations.md`](./foundations.md) 의 토큰부터 찾았다
+- [ ] 문구는 [`docs/copy.md`](./copy.md) 의 톤·용어집을 따랐다
+
+## 1. 토큰
+
+- [ ] 🤖 Tailwind 기본 팔레트(`bg-blue-500`)·`white`/`black`·임의 색(`bg-[#..]`)이 없다
+- [ ] 🤖 `'#ff6900'`·`rgb(...)` 같은 하드코딩 색이 없다
+- [ ] 🤖 글자 크기는 `text-micro`~`text-display`, 모서리는 `rounded-inline/control/card/panel/shell`, 그림자는 `shadow-overlay` 만
+- [ ] 글자색은 쓰임에 맞는 `fg-*` — 본문 `fg-default`, 보조 `fg-subtle`, 흐린 안내 `fg-muted`
+- [ ] 새 fg × bg 조합이면 `scripts/tokens-check.ts` 의 REQUIRED 에 추가해 대비를 검증했다
+
+## 2. 컴포넌트 재사용
+
+- [ ] 같은 역할의 컴포넌트가 registry 에 있으면 그것을 썼다(버튼·입력·모달·아바타·아이콘 버튼을 손으로 만들지 않았다)
+- [ ] 비슷하지만 조금 다르면 새로 만들기 전에 기존 것에 `variant`/`size`/`tone` 을 더하는 쪽을 검토했다
+- [ ] 새 ui 컴포넌트를 만들었다면 JSDoc(✅/❌) + `*.meta.ts` + 스토리를 함께 추가했다 (🤖 meta·JSDoc 누락)
+
+## 3. 상태 — 로딩 / 에러 / 빈 상태 / 성공
+
+- [ ] 🤖 버튼 처리 중은 `Button isLoading` — 라벨을 "~ 중..." 으로 바꾸지 않는다
+- [ ] 🤖 스피너는 `Spinner`/`StateMessage` 로만(손 `animate-spin` 금지)
+- [ ] 영역 첫 로딩은 `StateMessage kind="loading"` — 무엇을 불러오는지 글자로
+- [ ] 비어 있으면 `StateMessage kind="empty"` — 채울 수 있으면 다음 행동 한 줄 (그 자리에 컨트롤이 실제로 있을 때만)
+- [ ] 실패는 시작한 자리에 — 모달은 모달 안 `InlineAlert`, 영역은 `InlineAlert` + 다시 시도, 제자리 동작만 토스트
+- [ ] 성공은 화면에 보이면 조용히, 안 보이거나 보던 화면이 사라지면 토스트 "~했어요"
+
+## 4. 확인
+
+- [ ] 🤖 `window.confirm`·`alert`·`prompt` 가 없다
+- [ ] 되돌릴 수 없는 동작만 `ConfirmDialog` 로 묻는다 — 제목은 질문형, 버튼은 결과 동사("삭제", "나가기"), "확인" 금지
+- [ ] 모달 안에서는 모달을 겹치지 않고 그 줄 안에서 확인한다
+
+## 5. 접근성
+
+- [ ] 아이콘만 있는 버튼에 `aria-label` (IconButton 은 타입이 강제)
+- [ ] 인터랙션 요소에 focus 표시가 보인다
+- [ ] 입력에는 보이는 label — placeholder 로 대신하지 않는다
+- [ ] 로딩·결과 안내는 `role="status"`, 오류만 `role="alert"`
+- [ ] 키보드만으로 열고·이동하고·닫을 수 있다(메뉴·모달)
+- [ ] 스토리가 있다면 Storybook **Accessibility** 패널 위반 0 (스크롤 영역은 `tabIndex={0}`, 잠긴 영역은 `inert`)
+
+## 6. 레이아웃·밀도
+
+- [ ] 작업 화면에 랜딩 페이지식 큰 여백·큰 제목을 쓰지 않았다
+- [ ] 긴 한국어 제목·긴 파일 경로·좁은 폭(모바일)에서 깨지지 않는다 — 넘칠 곳에 `min-w-0`·`truncate`·`break-keep`
+- [ ] 글자 확대(`data-font-size="large"`)에서도 겹치지 않는다
+
+## 7. 안티패턴
+
+- [ ] 알약형(`rounded-full`) 버튼·입력이 없다
+- [ ] 장식용 그림자·그라디언트가 없다
+- [ ] 미구현 기능을 동작하는 것처럼 보이게 하거나 가짜 수치를 넣지 않았다 — 서버가 없으면 `API_CAPABILITIES` 로 끄고 안내 문구
+- [ ] 렌더러 코드가 Electron API(`window.electron`)에 의존하지 않는다(웹 배포가 깨진다)
+
+## 8. 문구 — [`docs/copy.md`](./copy.md)
+
+- [ ] 🤖 해요체 — `~습니다`·`~입니다` 없음(개발자용 `throw new Error` 제외)
+- [ ] 🤖 말줄임표는 `…` 한 글자, 🤖 용어집(팀 채팅·저장소·방장 넘기기)
+- [ ] 실패는 무엇을 못 했는지 + 다음 행동, 로딩은 "~를 불러오는 중…", 성공은 "~했어요"
+- [ ] 버튼은 결과 동사, 같은 동작은 메뉴·모달 제목·버튼에서 같은 이름
+
+## 9. 확인 방법
+
+- [ ] `yarn lint` · `yarn typecheck` · `yarn test` · `yarn build` · `yarn tokens:check`
+- [ ] 바뀐 화면을 실제로 열어 기본·hover/focus·로딩·오류·빈 상태를 봤다
diff --git a/docs/assets/quire/quire-lockup.png b/docs/assets/quire/quire-lockup.png
new file mode 100644
index 0000000..a2e3ec3
Binary files /dev/null and b/docs/assets/quire/quire-lockup.png differ
diff --git a/docs/assets/quire/quire-symbol.png b/docs/assets/quire/quire-symbol.png
new file mode 100644
index 0000000..a3b8cb5
Binary files /dev/null and b/docs/assets/quire/quire-symbol.png differ
diff --git a/docs/components.md b/docs/components.md
new file mode 100644
index 0000000..b24bb7d
--- /dev/null
+++ b/docs/components.md
@@ -0,0 +1,136 @@
+# Quire Components — 컴포넌트 인벤토리 & API 컨벤션
+
+- 상태: **In sync** (2026-09-26) — Phase C 1차(2026-09-25). 판정은 기록만 하고, 실행한 것은 §3에 표시한다.
+- 상위 문서: [`SYSTEM.md`](../SYSTEM.md) · 토큰: [`docs/foundations.md`](./foundations.md) · 시각 규칙: [`DESIGN.md`](../DESIGN.md) · 컴포넌트 registry: [`docs/registry.md`](./registry.md)
+- 집계 기준: `src/renderer/src`(generated·stories 제외) import 검색, 2026-09-25.
+
+---
+
+## 0. 결정 요약
+
+| #    | 항목           | 결정                                                    | 이유                                                                            |
+| ---- | -------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------- |
+| C1   | 이번 실행 범위 | SourceList 추출, `cn()` 도입. 나머지는 판정만 기록      | 사용자가 불편을 직접 겪은 곳(참조 코드 목록)과 모든 컴포넌트에 걸리는 기반부터. |
+| C2-1 | Props 이름     | `variant` / `size` / `tone` 세 개로 고정                | 이미 Button·IconButton·InlineAlert·Toast 가 이 형태. 규칙만 명문화한다.         |
+| C2-2 | className 병합 | `cn()` (clsx + tailwind-merge, Quire 토큰 등록)         | 문자열 이어붙이기는 `p-3 p-4` 같은 충돌을 못 거른다.                            |
+| C2-3 | asChild        | 보류                                                    | 라우터 링크형 버튼이 거의 없다. 필요해지면 도입.                                |
+| C2-4 | ref            | React 19 — `ref` 는 일반 prop. `forwardRef` 쓰지 않는다 | 로드맵의 "forwardRef 강제" 항목은 React 19에서 의미가 없어졌다.                 |
+| C3   | Do/Don't 위치  | 컴포넌트 파일 상단 JSDoc (5줄 이내)                     | IDE hover 와 AI 가 파일을 열 때 함께 읽힌다. 상세 registry 는 Phase E.          |
+
+---
+
+## 1. API 컨벤션
+
+### Props
+
+| prop        | 뜻                       | 값                                                           |
+| ----------- | ------------------------ | ------------------------------------------------------------ |
+| `variant`   | 모양                     | 컴포넌트별 (`primary` / `secondary` / `ghost` / `danger` 등) |
+| `size`      | 크기                     | `sm` / `md` (필요할 때만 `lg`, 이유를 JSDoc 에 적는다)       |
+| `tone`      | 의미 색                  | `neutral` / `info` / `danger` / `success` 중 필요한 것       |
+| `className` | 바깥에서 덧붙이는 클래스 | 항상 `cn(기본, 조건부..., className)` 순서로 맨 뒤에 합친다  |
+
+- 새 이름(`intent`, `color`, `kind` 등)을 만들지 않는다.
+- 불리언 prop 은 상태만: `disabled`, `loading`, `defaultExpanded`. 모양을 불리언으로 늘리지 않는다(`isBig` ✗ → `size`).
+
+### className
+
+```tsx
+import { cn } from '../../lib/cn';
+
+
-       setOpen(false)}
-        onSubmit={() => setOpen(false)}
-      />
-    
-  );
-};
-
-export const Interactive: Story = {
-  render: () => 
-};
diff --git a/src/renderer/src/components/feature/CreateChatModal.tsx b/src/renderer/src/components/feature/CreateChatModal.tsx
deleted file mode 100644
index 5911a2e..0000000
--- a/src/renderer/src/components/feature/CreateChatModal.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { useMemo, useState } from 'react';
-import { Button } from '../ui/Button';
-import { OverlayModal } from '../ui/OverlayModal';
-
-type Props = {
-  open: boolean;
-  type: 'personal' | 'team' | null;
-  onClose: () => void;
-  onSubmit: (input: { type: 'personal' | 'team'; name: string }) => void;
-  isSubmitting?: boolean;
-};
-
-export const CreateChatModal = ({
-  open,
-  type,
-  onClose,
-  onSubmit,
-  isSubmitting
-}: Props): React.JSX.Element | null => {
-  const [name, setName] = useState('');
-  const [touched, setTouched] = useState(false);
-
-  const title = type === 'team' ? '팀 채팅 생성' : '개인 채팅 생성';
-  const placeholder = type === 'team' ? '새 팀 채팅' : '새 개인 채팅';
-
-  const nameError = useMemo(() => {
-    if (!touched) return '';
-    if (!name.trim()) return '채팅 이름을 입력해주세요.';
-    return '';
-  }, [name, touched]);
-
-  if (!type) return null;
-
-  const resetAndClose = (): void => {
-    setName('');
-    setTouched(false);
-    onClose();
-  };
-
-  return (
-    
-      
{ - e.preventDefault(); - setTouched(true); - if (!name.trim()) return; - onSubmit({ type, name: name.trim() }); - }} - > - - -
- - -
-
-
- ); -}; diff --git a/src/renderer/src/components/feature/CreateProjectModal.tsx b/src/renderer/src/components/feature/CreateProjectModal.tsx index db00677..862de54 100644 --- a/src/renderer/src/components/feature/CreateProjectModal.tsx +++ b/src/renderer/src/components/feature/CreateProjectModal.tsx @@ -8,7 +8,6 @@ import { useGetProjectSyncStatus, usePostProjects } from '../../api/auth/useProj import { handleApiError } from '../../api/axios'; import { friendlyErrorMessage } from '../../api/errorMessages'; import type { GithubOauthDeviceStartCreateData } from '../../api/generated/data-contracts'; -import { useToast } from '../../hooks/useToast'; import { clearCachedOauthFlow, readCachedOauthFlow, @@ -18,6 +17,8 @@ import { navigate } from '../../lib/hashRouter'; import { Button } from '../ui/Button'; import { InlineAlert } from '../ui/InlineAlert'; import { OverlayModal } from '../ui/OverlayModal'; +import { TextField } from '../ui/TextField'; +import { StateMessage } from '../ui/StateMessage'; type Props = { open: boolean; @@ -30,7 +31,6 @@ const PROJECT_NAME_MAX = 50; const PROJECT_DESCRIPTION_MAX = 200; export const CreateProjectModal = ({ open, onClose }: Props): React.JSX.Element | null => { - const toast = useToast(); const create = usePostProjects(); const startGithubOauth = usePostGithubOauthDeviceStart(); @@ -81,14 +81,14 @@ export const CreateProjectModal = ({ open, onClose }: Props): React.JSX.Element // 서버 createProjectBodySchema 와 같은 한도다(명세 A-3). maxLength 가 51자째를 // 막아주지만 2자 미만은 입력 단계에서 막을 수 없어 문구로 알린다. if (name.trim().length < PROJECT_NAME_MIN) - return `프로젝트 이름은 ${PROJECT_NAME_MIN}자 이상이어야 합니다.`; + return `프로젝트 이름은 ${PROJECT_NAME_MIN}자 이상이어야 해요.`; return ''; }, [name, touched.name]); const repoError = useMemo(() => { if (!touched.repo) return ''; if (!activeOauthFlow) return 'GitHub 인증을 시작해주세요.'; - if (!isAuthorized) return 'GitHub 인증이 완료되어야 합니다.'; + if (!isAuthorized) return 'GitHub 인증을 먼저 완료해주세요.'; if (!selectedRepo) return '연결할 저장소를 선택해주세요.'; return ''; }, [activeOauthFlow, isAuthorized, selectedRepo, touched.repo]); @@ -168,8 +168,8 @@ export const CreateProjectModal = ({ open, onClose }: Props): React.JSX.Element const stepTone = (ready: boolean): string => ready - ? 'border-emerald-300 bg-emerald-50 text-emerald-700' - : 'border-line bg-surface-muted text-text-soft'; + ? 'border-line-success bg-success-soft text-fg-success' + : 'border-line bg-surface-muted text-fg-muted'; const closeAndReset = (): void => { setName(''); @@ -180,6 +180,8 @@ export const CreateProjectModal = ({ open, onClose }: Props): React.JSX.Element setCreatedProjectId(''); autoHandledFlowRef.current = null; startedFlowRef.current = null; + startGithubOauth.reset(); + create.reset(); onClose(); }; @@ -190,20 +192,12 @@ export const CreateProjectModal = ({ open, onClose }: Props): React.JSX.Element setSelectedRepoFullName(''); saveCachedOauthFlow(data); startedFlowRef.current = data.data.flowId; - }, - onError: (error) => { - toast.error(friendlyErrorMessage(error, 'github.connect')); } }); }; return ( - +
{ e.preventDefault(); @@ -225,65 +219,54 @@ export const CreateProjectModal = ({ open, onClose }: Props): React.JSX.Element { onSuccess: (data) => { setCreatedProjectId(data.data.id); - }, - onError: (error) => { - toast.error(friendlyErrorMessage(error, 'project.create')); } } ); }} >
-
+
-

1. 프로젝트 정보

+

1. 프로젝트 정보

{hasProjectInfo ? '완료' : '필수 입력'}
- - - + setName(e.target.value)} + placeholder="예) Qode-Fe" + autoFocus + error={nameError || undefined} + /> + + setDescription(e.target.value)} + placeholder="예) 고객 대시보드 개선 프로젝트" + />
-
+
-

2. GitHub 인증

-

승인 후 저장소 목록을 불러옵니다.

+

2. GitHub 인증

+

승인하면 저장소 목록을 불러와요.

{isAuthorized ? '인증 완료' : '대기'} @@ -314,15 +297,15 @@ export const CreateProjectModal = ({ open, onClose }: Props): React.JSX.Element
{activeOauthFlow ? ( -
-

+

+

인증 코드:{' '} - + {activeOauthFlow.data.userCode}

-

상태: {oauthStatusLabel}

-

+

상태: {oauthStatusLabel}

+

{activeOauthFlow.data.verificationUri}

@@ -340,39 +323,43 @@ export const CreateProjectModal = ({ open, onClose }: Props): React.JSX.Element
{!oauthOpenUrl ? ( -

GitHub 인증 URL이 올바르지 않습니다.

+

+ GitHub 인증 URL이 올바르지 않아요. +

) : null}
) : null}
-

3. 저장소 선택

-

연결할 GitHub 저장소를 1개 선택하세요.

+

3. 저장소 선택

+

연결할 GitHub 저장소를 1개 선택하세요.

{hasRepo ? '선택 완료' : '미선택'}
{isAuthorized && repos.isLoading ? ( -

저장소 목록을 불러오는 중...

+ 저장소 목록을 불러오는 중… ) : null} {isAuthorized && !repos.isLoading && repoItems.length === 0 ? ( -

연결 가능한 저장소가 없습니다.

+

연결할 수 있는 저장소가 없어요.

) : null} {!isAuthorized ? ( -

- GitHub 인증을 완료하면 저장소를 선택할 수 있습니다. +

+ GitHub 인증을 완료하면 저장소를 선택할 수 있어요.

) : null} @@ -380,7 +367,7 @@ export const CreateProjectModal = ({ open, onClose }: Props): React.JSX.Element {repoItems.map((repo) => ( ))}
- {repoError ?

{repoError}

: null} + {repoError ?

{repoError}

: null} @@ -417,6 +404,22 @@ export const CreateProjectModal = ({ open, onClose }: Props): React.JSX.Element ) : null} + {startGithubOauth.isError ? ( +
+ + {friendlyErrorMessage(startGithubOauth.error, 'github.connect').description} + +
+ ) : null} + + {create.isError ? ( +
+ + {friendlyErrorMessage(create.error, 'project.create').description} + +
+ ) : null} + {createdProjectId ? (
- 프로젝트가 생성되었습니다. ID: {createdProjectId} + 프로젝트를 만들었어요. ID: {createdProjectId} {syncStatus.data?.data.latestJob?.errorMessage ? ` (${syncStatus.data.data.latestJob.errorMessage})` : ''} diff --git a/src/renderer/src/components/feature/digest/DigestSharedCard.tsx b/src/renderer/src/components/feature/digest/DigestSharedCard.tsx index 601b523..317dd63 100644 --- a/src/renderer/src/components/feature/digest/DigestSharedCard.tsx +++ b/src/renderer/src/components/feature/digest/DigestSharedCard.tsx @@ -3,6 +3,9 @@ import { Button } from '../../ui/Button'; import { ChatItemMenu, type ChatItemMenuAction } from '../../ui/ChatItemMenu'; import { Icon } from '../../ui/Icon'; import { MarkdownAnswer } from '../../ui/MarkdownAnswer'; +import { SourceList } from '../../ui/SourceList'; +import { extractReferenceLines, mergeSources } from '../../../lib/inlineSources'; +import { Avatar } from '../../ui/Avatar'; // 팀채팅에 도착한 개인채팅 답변 요약 공유 카드. // 일반 팀채팅 메시지(회색/유저 말풍선)와 시각적으로 구분해 카드 형태로 렌더한다. @@ -14,7 +17,6 @@ type Props = { content: string; sources: SourceItem[]; senderName: string; - senderInitial?: string; createdAt: string; isMe: boolean; hasSourceLink?: boolean; @@ -34,7 +36,6 @@ export const DigestSharedCard = ({ content, sources, senderName, - senderInitial, createdAt, isMe, hasSourceLink, @@ -43,7 +44,6 @@ export const DigestSharedCard = ({ isDeleting }: Props): React.JSX.Element => { const time = formatTime(createdAt); - const initial = (senderInitial ?? senderName.charAt(0) ?? '?').toUpperCase(); const menuActions: ChatItemMenuAction[] = isMe ? [ @@ -58,56 +58,35 @@ export const DigestSharedCard = ({ ] : []; + // 요약 본문의 '근거 코드' 목록은 SourceList 와 중복이라 걷어낸다. 문장 속 참조는 둔다. + const body = extractReferenceLines(content); + return (
-
- {initial} -
+
-
- +
+ 팀 공유 - {senderName} - {time ? · {time} : null} + {senderName} + {time ? · {time} : null} {menuActions.length > 0 ? (
) : null}
-
- +
+ - {sources.length > 0 ? ( -
-

- 참조 코드 {sources.length}개 -

-
    - {sources.map((s, idx) => ( -
  • - - {s.filePath} - - - ({s.startLine ?? '-'}-{s.endLine ?? '-'}) - -
  • - ))} -
-
- ) : null} +