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

11 KiB

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)

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/streamingStreamingText(타자기) · 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 추가:

SNAP: "/snap",
SNAP_NEW: "/snap/new",
SNAP_SESSION: "/snap/s/:id",

routes.tsx(라우트 정의)에 ProtectedRoute(인증 필요) 안, DashboardLayoutSnapLayout(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 + startAssistantMessagesnap.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