Files
CODE_ASSISTANT/docs/superpowers/specs/2026-07-16-snap-mate-react-client-design.md
T
2026-09-16 17:22:14 +09:00

213 lines
11 KiB
Markdown

# Snap Mate Client — React 이식 설계
> new-chat.html 목업(3뷰 채팅 클라이언트)을 기존 2_frontend 코드베이스 위에 React로 구현.
> 최종 목적지: .NET 윈도우 애플리케이션의 웹뷰.
작성일: 2026-07-16
---
## 1. 목표 & 범위
`new-chat.html`(958줄 self-contained 목업)의 경험을 React로 옮긴다. 3개 뷰:
- **홈** — 세션 목록 + 검색
- **채팅** — 세션 열어 과거대화 + 코드블럭 + 진짜 스트리밍 전송
- **새 대화** — 히어로 + 추천카드 → 첫 전송
**방침: 3화면 비주얼을 목업만큼 꽉 채운다.** 백엔드는 무조건 나중, 전제는 mock.
부가 UI(nav 레일·detail 패널·클립보드 배너)도 **이번에 비주얼로 다 보이게** 만들되,
깊은 로직(단축키 세트·실시간 클립보드 OS감지·real 백엔드)만 가볍게/2차.
### 이번 범위 (비주얼 완성)
- 전용 풀블리드 라우트 3개 (`/snap`, `/snap/new`, `/snap/s/:id`)
- 세션 목록 + 검색(클라 필터, 클릭 진입)
- 세션 열어 과거대화 렌더 (markdown + 코드블럭 + 복사)
- 스트리밍 전송 (컨트랙트만 뚫고 mock 토큰 방출 — 백엔드는 나중에)
- 새 대화 히어로 + 추천카드 → 전송 → 세션 진입
- **source-code 네비게이터 레일** — 코드블럭 목록 + 클릭 점프 (기본 동작까지)
- **세션 detail 패널** (Sheet) — 배지 클릭 시 세션 메타 표시
- **클립보드 감지 배너** — 목업처럼 타이머 데모(2.6s 후 등장) 수준의 비주얼
### 2차로 미룸 (범위 밖)
real 백엔드 부착 · 키보드 단축키 세트(↑↓/Ctrl+J/Alt화살표/Tab순환/Ctrl+Shift+C) 배선 · 실시간 클립보드 OS 읽기 · syntax highlight 강화 · 가짜 OS 타이틀바 · rename/delete/search 엔드포인트
---
## 2. 핵심 결정 (확정됨)
| 항목 | 결정 |
|---|---|
| 디자인 | 기존 **shadcn 컴포넌트로 흡수**. 별도 CSS 팔레트 왕국 안 만듦. editorial 성격(serif 이탤릭 타이틀·mono 대문자 eyebrow·따뜻한 accent)은 Tailwind 클래스로만 얹음 |
| 앱 셸 | **전용 풀블리드 라우트** (사이드바/DashboardLayout 밖). 기존 대시보드 라우트는 안 건드림 |
| 백엔드 계약 | **base-backend(000)의 세션 기반 계약에 정렬**. 지금은 **CONTRACT만 뚫고 mock**, UX/UI 집중 후 백엔드 부착 |
| 스트리밍 | 기존 `lib/streaming/streamLLM`(범용) 재사용. 기존 `useChatStream`/`chatStore`(무상태 `{messages}` 계약)는 안 건드림 |
| 폰트 | 웹뷰 오프라인 대비 — 새 CDN 폰트 안 붙임. 기존/시스템 폰트 + serif 폴백 |
---
## 3. 백엔드 계약 (base-backend `modules/chat`)
현재 2_frontend 채팅은 **무상태**(`{messages}` 통째 전송, 세션 없음 — echo 시대)라 그대로 못 쓴다.
base-backend는 **세션 기반**으로 진화했고, 이 UI는 그 계약에 맞춘다:
```
GET /chat/sessions → Envelope<ChatSessionResponse[]> (목록, 페이지네이션)
GET /chat/sessions/{id}/messages → Envelope<ChatSessionDetailResponse>
POST /chat/sessions → Envelope<ChatSessionResponse> (새 대화 = 세션 발급)
PATCH /chat/sessions/{id} → rename (2차)
DELETE /chat/sessions/{id} → soft delete (2차)
GET /chat/sessions/search?query= → 메시지 검색 (2차, 지금은 클라 필터)
POST /chat/stream {sessionId, content, forcedSkill?} → SSE
```
### 타입 (base-backend schema.py 와 1:1, camelCase)
```ts
interface ChatSessionResponse {
id: string
title: string | null
titleLlm: string | null
isGenerating: boolean
createdAt: string
updatedAt: string
}
interface ChatMessageResponse {
sessionId: string
role: "user" | "assistant" | "system"
content: string // markdown
createdAt: string
}
interface ChatSessionDetailResponse extends ChatSessionResponse {
messages: ChatMessageResponse[]
}
interface SnapStreamRequest { sessionId: string; content: string; forcedSkill?: string }
```
### 목업 ↔ 실제 필드 매핑
| 목업 세션카드 | 실제 | 처리 |
|---|---|---|
| title | `titleLlm ?? title` | ✓ |
| time | `updatedAt` (상대시간 포맷, date-fns) | ✓ |
| 라이브 dot | `isGenerating` | ✓ |
| tag / tokens / snippet | **백엔드에 없음** | mock엔 넣되 `// UI-only` 표기. real 스왑 시 snippet=마지막 메시지 미리보기 / tag·tokens 드롭·파생 |
---
## 4. 모듈 구조
```
src/features/snap/
contract/
types.ts # 위 계약 타입 (types/api.ts 에서 재사용/확장)
mock/ # ← 목업 격리. 백엔드 부착 = 이 폴더만 지우고 api 본문 스왑
sessions.ts # ChatSessionResponse[] + UI-only 확장(tag/snippet)
conversations.ts # Record<sessionId, ChatMessageResponse[]> (content=markdown, 코드펜스 포함)
stream.ts # mock 토큰 방출기 (실제 SSE 흉내)
api/
snap.api.ts # useSessionList / useSessionMessages / useCreateSession (TanStack Query)
# 지금 mock 반환. 주석에 "real = axios 이 줄" 스왑 지점 명시
snap.stream.ts # send({sessionId,content,forcedSkill?}) → 지금 mock/stream / 나중 streamLLM('/chat/stream')
store/
snapChatStore.ts # 현재 세션 1개의 messages + 스트리밍 상태 (chatStore 패턴 복제, 세션화)
components/
SnapLayout.tsx # 풀블리드 셸 + Toaster (가짜 타이틀바 없음, 얇은 브랜드 스트립)
SessionCard.tsx / SessionSearch.tsx
ChatHeader.tsx / Composer.tsx
Message.tsx # markdown 렌더 + 코드펜스 → CodeBlock. 라이브는 StreamingText
CodeBlock.tsx # mono 블럭 + 복사 버튼(navigator.clipboard). highlight 최소
NavRail.tsx # source-code 네비게이터 — 코드블럭 목록 + 클릭 점프
DetailPanel.tsx # 세션 메타 Sheet (배지 클릭)
ClipBanner.tsx # 클립보드 감지 배너 (타이머 데모 비주얼)
Hero.tsx / SuggestCard.tsx
pages/
SessionListPage.tsx # /snap
NewChatPage.tsx # /snap/new
SessionChatPage.tsx # /snap/s/:id
index.ts
```
### 재사용 (새로 안 짬)
- `lib/streaming/streamLLM` — 범용 `{path, body, signal, handlers}` 러너. snap.stream 이 여기에 얹힘
- `lib/streaming``StreamingText`(타자기) · `StoppedNotice` · `ClarifyChoices` 렌더
- `shared/ui/*` shadcn 프리미티브 (Button/Input/Card/Badge/Separator)
- `react-markdown` + `remark-gfm` (이미 설치됨) — 메시지 markdown 렌더
- `sonner` toast · `lucide-react` 아이콘 · `date-fns` 상대시간
### 안 건드림 (surgical)
- 기존 `features/chat/*` (무상태 계약 그대로, `/chat` 페이지 유지)
- `shared/components/DashboardLayout` · `AppSidebar` · 기존 라우트
---
## 5. 라우팅 & 셸
`config/routes.ts` PATHS 추가:
```ts
SNAP: "/snap",
SNAP_NEW: "/snap/new",
SNAP_SESSION: "/snap/s/:id",
```
`routes.tsx`(라우트 정의)에 `ProtectedRoute`(인증 필요) 안, **`DashboardLayout` 밖**에 `SnapLayout`(Outlet) 라우트 그룹으로 추가. 3 페이지가 이 셸을 공유. 웹뷰에선 .NET이 진짜 창을 주므로 가짜 min/max/close 타이틀바는 버리고 얇은 브랜드 스트립만.
---
## 6. 스트리밍 흐름
`snapChatStore` = 기존 `chatStore` 패턴(messages / isStreaming / isRevealing / controller / stop / retry)을 복제하되 "현재 세션 1개" 모델. 기존 chatStore·chat 페이지는 안 건드림.
- **세션 열기** (`/snap/s/:id`): `useSessionMessages(id)` → 받은 messages 로 store seed (스트리밍 아님)
- **전송**: `snapChatStore.addUserMessage` + `startAssistantMessage``snap.stream.send({sessionId, content})` → onToken 마다 `appendChunk`. 라이브 버블은 `StreamingText`
- **새 대화** (`/snap/new`): 첫 전송 시 `useCreateSession()` → 세션 id 받음 → `/snap/s/:id` 이동 → seed 없이 바로 전송
- 지금은 `snap.stream` 이 mock 토큰 방출. 백엔드 부착 = `streamLLM({ path:'/chat/stream', body:{sessionId,content,forcedSkill} })` 로 본문 교체
`forcedSkill`/clarify(라우팅 후보) 배선은 계약엔 넣되 UI(ClarifyChoices)는 2차에 붙여도 됨 — 슬라이스에선 토큰 스트림만 확실히.
---
## 7. 메시지 렌더링
지금 chat은 content가 평문(타자기)뿐. 목업엔 코드블럭이 있고 실제 LLM도 markdown을 뱉는다.
- **`Message`**: assistant content 를 `react-markdown`+`remark-gfm` 로 렌더. 코드펜스는 `components.code` 로 가로채 `CodeBlock` 렌더. user 는 평문(whitespace-pre-wrap)
- **`CodeBlock`**: 상단 바(lang 라벨 + 복사 버튼) + mono `<pre>`. 복사 = `navigator.clipboard` + toast. 블럭 내부 스크롤(목업처럼). syntax 색은 최소(2차 강화)
- **라이브 스트리밍 중**: 부분 markdown 이 깨질 수 있으니, 완료 전까지는 `StreamingText`(평문 타자기) → done 후 markdown 재렌더. (구현 시 부분 markdown 렌더 허용 여부 결정 — 기본은 완료 후 markdown)
---
## 8. 디자인 방침 (shadcn 흡수)
기존 테마 토큰(light/dark) + shadcn 컴포넌트 그대로. editorial 정체성은 Tailwind 클래스로만:
- 타이틀: `font-serif italic`
- eyebrow/메타: mono 대문자 + tracking
- 카드/보더: 기존 토큰(`bg-card`/`border`), 목업의 따뜻한 종이톤은 accent 정도로만 차용
- 별도 `.css` 팔레트 파일 안 만듦. 필요하면 Tailwind config 토큰만 최소 추가
---
## 9. Mock 격리 원칙 ("금방 없앨 수 있도록")
- 모든 mock 은 `features/snap/mock/` 한 폴더에만
- api/stream 훅은 **base-backend 계약 타입**을 반환/수신 (mock 도 그 모양)
- 스왑 지점 = `snap.api.ts` / `snap.stream.ts``// TODO(backend): mock → real` 주석 한 곳씩
- UI-only 필드(tag/snippet/tokens)는 명시적으로 표기해 real 스왑 시 안전하게 드롭/파생
---
## 10. 검증 기준 (verify)
각 페이지가 "됐다"를 화면으로 보여줄 수 있어야 (CLAUDE.md 10번 세로슬라이스):
1. `/snap` → mock 세션 목록 렌더, 검색어 입력 시 클라 필터 동작, 카드 클릭 → `/snap/s/:id` 이동
2. `/snap/s/:id` → 해당 세션 mock 과거대화 표시, 코드블럭 복사 버튼 동작(클립보드+toast)
3. 채팅 입력 후 전송 → user 버블 + mock 스트리밍 assistant 버블(타자기)
4. `/snap/new` → 히어로+추천카드, 추천 클릭 시 입력창 채움, 첫 전송 → 히어로 사라지고 세션 진입
5. **nav 레일** → 코드블럭 있는 세션에서 레일에 블럭 목록 뜨고, 클릭 시 해당 블럭으로 스크롤 점프
6. **detail 패널** → 채팅 헤더 배지 클릭 시 세션 메타 Sheet 열림/닫힘
7. **클립보드 배너** → 채팅 진입 후 타이머로 배너 등장, 무시/붙여넣기 버튼 동작
8. `npm run lint` 깨끗 · `npm run build` 성공 · 기존 `/chat` 등 대시보드 라우트 안 깨짐
---
## 11. 열린 질문 (구현 중 확정)
- 라이브 스트리밍 중 부분 markdown 렌더 허용 여부 (기본: 완료 후 markdown, 스트리밍 중엔 평문 타자기)
- Tailwind config 에 warm accent 토큰 추가할지 vs 기존 토큰으로만
- 상대시간 포맷 로케일(ko) — date-fns `formatDistanceToNow`