8.2 KiB
Snap 백엔드 연결 설계
snap 목업을 걷어내고 base-backend(000)의 세션 기반 chat 계약에 real 연결. 이전 spec(
2026-07-16-snap-mate-react-client-design.md)의 "2차로 미룸 — real 백엔드 부착"을 실행. 최종 목적지: .NET 윈도우 애플리케이션의 웹뷰(WebView2).
작성일: 2026-07-18
1. 목표 & 범위
snap 은 이미 교체 seam 이 다 잡혀 있다. 새 구조를 만드는 게 아니라, 기존 seam 에 real 구현을 끼우고 목업을 제거하는 작업이다. UI/store/컴포넌트는 건드리지 않는다 (반환 타입만 유지하면 화면 코드는 안 바뀐다).
이번 범위 (세로 슬라이스)
| # | 대상 | 붙일 곳 |
|---|---|---|
| 1 | useSessionList |
GET /chat/sessions (apiList) |
| 2 | useSessionMessages |
GET /chat/sessions/{id}/messages (apiGet) |
| 3 | useCreateSession |
POST /chat/sessions (apiPost) |
| 4 | snap.stream (USE_MOCK=false) |
POST /chat/stream SSE (token/done/error/title) |
| 5 | 목업 3파일 제거 | mock/sessions.ts · mock/conversations.ts · mock/stream.ts + import 정리 |
| 6 | Bearer 토큰 provider seam | client.ts 요청 인터셉터 + sse.ts 헤더 |
2차로 미룸 (범위 밖)
- rename(PATCH) / delete / search 엔드포인트
subagent_start/subagent_done진행 UIusage토큰 카운터- Bearer 모드 refresh(호스트 위임) 실제 배선 — .NET 호스트 생기면
2. 핵심 결정 (확정됨)
| 항목 | 결정 |
|---|---|
| 방침 | 기존 seam 에 real 구현 주입 + 목업 제거. UI/store/컴포넌트 0 변경 |
| 범위 | 세로 슬라이스 — list/get/create/stream 까지. rename/delete/search 는 2차 |
| envelope | 비스트림 응답은 Envelope[T] → apiGet/apiPost/apiList 헬퍼가 .data 자동 언랩 |
| title 이벤트 | 살린다 — 첫 메시지 후 백엔드가 쏘는 title{title} 로 세션 제목 자동 갱신 |
| subagent/usage | 무시 — streamLLM 이 unknown 이벤트를 흘려서 안 살려도 안 깨짐. 2차 |
| 409 | isGenerating 이면 send 막고, stream open 이 409(CHAT_GENERATION_IN_PROGRESS)면 toast |
| 인증 | 쿠키 now + Bearer 주입 seam. 토큰 소스를 getAccessToken() 한 곳으로. 있으면 Authorization: Bearer, 없으면(=오늘) 쿠키 폴백 |
| refresh | 쿠키 모드는 지금대로. Bearer 모드 refresh 는 .NET 호스트 책임 → 지금은 TODO 만 |
| dev 환경 | Vite proxy(/api→localhost:8001)로 same-origin → 쿠키/CORS 이슈 없음. 건드릴 것 없음 |
3. 백엔드 계약 (base-backend modules/chat)
베이스 URL: http://localhost:8001 · 프리픽스 /api/v1 · dev 는 Vite proxy 로 /api/v1/....
3.1 비스트림 엔드포인트 (Envelope 래핑, camelCase)
| Method + Path | 용도 | 반환 data |
|---|---|---|
POST /chat/sessions |
세션 생성 | ChatSessionResponse |
GET /chat/sessions |
세션 목록(페이지네이션) | ChatSessionResponse[] |
GET /chat/sessions/{id}/messages |
세션 + 메시지 | ChatSessionDetailResponse |
ChatSessionResponse:id, title, titleLlm, isGenerating, createdAt, updatedAtChatMessageResponse:sessionId, role, content, createdAtChatSessionDetailResponse: 세션 필드 +messages: ChatMessageResponse[]
프론트 contract/types.ts 의 SnapSession/SnapMessage/SnapSessionDetail 와 1:1
(단, SnapSession 의 UI-only 필드 tag?/snippet?/tokens? 는 백엔드에 없음 — 옵셔널이라
없어도 안 깨지고, 화면에서 안 쓰면 그만).
3.2 스트림 엔드포인트
POST /chat/stream — sse_starlette.EventSourceResponse (POST + JSON body 라 브라우저
네이티브 EventSource 불가 → @microsoft/fetch-event-source 사용, 이미 sse.ts 가 그럼).
요청 바디 ChatStreamRequest: { sessionId, content, forcedSkill? }.
이벤트 (named event: + JSON data:):
| event | data | 이번 처리 |
|---|---|---|
token |
{delta} |
appendChunk(delta) |
title |
{title} |
세션 제목 갱신 (react-query 캐시 + 필요 시 store) |
done |
{} / {traceId?} |
스트림 종료 |
error |
{message, code, detail?} |
toast + 마지막 assistant 메시지 frozen |
result |
{items} |
(이번엔 pass — snap 은 안 씀) |
subagent_start/subagent_done |
{name,...} |
무시 (2차) |
usage |
{used,limit,ratio,elapsed_ms} |
무시 (2차) |
순서: (optional title/result/subagent_*) → 다수 token → usage → done,
또는 error 종결. SSE 는 200 으로 열려서 중간에 상태코드 못 바꿈 → LLM 설정/생성 실패는
HTTP 에러가 아니라 error 이벤트로 옴. code: LLM_NOT_CONFIGURED /
LLM_RATE_LIMITED / LLM_ERROR / UNKNOWN_SKILL.
4. 유닛별 변경
4.1 features/snap/api/snap.api.ts — 목업 바디 → real 호출
세 함수의 queryFn/mutationFn 바디만 교체. 시그니처·반환타입·queryKey 유지.
useSessionList→apiList<SnapSession>("/chat/sessions")의itemsuseSessionMessages(id)→apiGet<SnapSessionDetail>(\/chat/sessions/${id}/messages`)`useCreateSession→apiPost<SnapSession>("/chat/sessions")MOCK_SESSIONS/MOCK_CONVERSATIONSimport 제거.
4.2 features/snap/api/snap.stream.ts — 플래그 + title 핸들러
USE_MOCK = false.SnapStreamHandlers에onTitle?(title: string)추가.- real 경로:
streamLLM({ path: "/chat/stream", body: req, signal, handlers })—streamLLM에도onTitle배선 필요(4.4). mockStreamimport 제거.
4.3 features/snap/hooks/useSnapChat.ts — title 반영 + 409
onTitle→ react-query 세션 캐시(["snap","sessions"],["snap","session",id])의title패치(queryClient.setQueryData).- send 진입 가드: 스토어
isStreaming이거나 세션isGenerating이면 막음. - stream open 이 409 면
onError로 흘러오니 code 보고 "이미 생성 중" toast.
4.4 lib/streaming/streamLLM.ts — title 이벤트 파싱
- 이벤트 스위치에
title→handlers.onTitle?.(payload.title)추가. LLMStreamHandlers타입에onTitle?추가. 기존 소비자(다른 chat)는 옵셔널이라 영향 없음.
4.5 인증 seam — getAccessToken() provider
- 새 파일
lib/auth/tokenProvider.ts:getAccessToken(): string | null(기본null) +setAccessToken(t: string | null). .NET 호스트가 나중에window.chrome.webview로 주입하면 여기만 채움. lib/api/client.ts: 요청 인터셉터 추가 — 토큰 있으면Authorization: Bearer세팅 (없으면 아무것도 안 함 → 쿠키 그대로). 응답 인터셉터는 유지.lib/streaming/sse.ts:open()헤더에 토큰 있으면Authorization추가.- refresh: 쿠키 모드 유지. Bearer 모드는
// TODO(host): .NET 호스트 refresh 위임주석만.
4.6 목업 제거
mock/sessions.ts·mock/conversations.ts·mock/stream.ts 삭제. 남은 import 없는지 확인
(4.1/4.2 에서 이미 끊음).
5. 검증
- 백엔드 로컬 8001 기동 + LLM(NVIDIA NIM) 설정 → 프론트 로그인 →
/snap세션 목록이 실제 DB 세션으로 뜸. - 새 대화 → 첫 메시지 전송 → 실제 LLM 토큰이 타이핑되듯 스트리밍 → 완료 후 사이드바 제목이 LLM 이 지은 제목으로 자동 변경(title 이벤트).
- 기존 세션 재진입 → 과거 메시지 렌더 정상.
- 생성 중 재전송 → 막힘 + toast.
- 기존 프론트 테스트(
snap.api/snap.stream/sse/streamLLM) 통과 + real 경로 유닛 테스트 추가(핸들러 매핑·title·409·Bearer 헤더 주입).
6. 리스크 / 열린 항목
- title 갱신 위치: react-query 캐시만 패치할지, snap 스토어에도 반영할지 —
사이드바가
useSessionList캐시를 보면 캐시 패치로 충분. 구현 때 확정. - Bearer refresh: .NET 호스트 부재로 이번엔 TODO. 호스트 붙을 때 별도 phase.
- usage/subagent: 지금 무시. 진행 UI 는 후속 spec 에서.