Files
CODE_ASSISTANT/docs/superpowers/specs/2026-07-18-snap-backend-connect-design.md
T
2026-09-16 17:22:14 +09:00

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 진행 UI
  • usage 토큰 카운터
  • 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(/apilocalhost: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, updatedAt
  • ChatMessageResponse: sessionId, role, content, createdAt
  • ChatSessionDetailResponse: 세션 필드 + messages: ChatMessageResponse[]

프론트 contract/types.tsSnapSession/SnapMessage/SnapSessionDetail 와 1:1 (단, SnapSession 의 UI-only 필드 tag?/snippet?/tokens? 는 백엔드에 없음 — 옵셔널이라 없어도 안 깨지고, 화면에서 안 쓰면 그만).

3.2 스트림 엔드포인트

POST /chat/streamsse_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_*) → 다수 tokenusagedone, 또는 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 유지.

  • useSessionListapiList<SnapSession>("/chat/sessions")items
  • useSessionMessages(id)apiGet<SnapSessionDetail>(\/chat/sessions/${id}/messages`)`
  • useCreateSessionapiPost<SnapSession>("/chat/sessions")
  • MOCK_SESSIONS/MOCK_CONVERSATIONS import 제거.

4.2 features/snap/api/snap.stream.ts — 플래그 + title 핸들러

  • USE_MOCK = false.
  • SnapStreamHandlersonTitle?(title: string) 추가.
  • real 경로: streamLLM({ path: "/chat/stream", body: req, signal, handlers })streamLLM 에도 onTitle 배선 필요(4.4).
  • mockStream import 제거.

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 이벤트 파싱

  • 이벤트 스위치에 titlehandlers.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. 검증

  1. 백엔드 로컬 8001 기동 + LLM(NVIDIA NIM) 설정 → 프론트 로그인 → /snap 세션 목록이 실제 DB 세션으로 뜸.
  2. 새 대화 → 첫 메시지 전송 → 실제 LLM 토큰이 타이핑되듯 스트리밍 → 완료 후 사이드바 제목이 LLM 이 지은 제목으로 자동 변경(title 이벤트).
  3. 기존 세션 재진입 → 과거 메시지 렌더 정상.
  4. 생성 중 재전송 → 막힘 + toast.
  5. 기존 프론트 테스트(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 에서.