Initial Commit
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
# 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`
|
||||
@@ -0,0 +1,158 @@
|
||||
# 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(`/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, updatedAt`
|
||||
- `ChatMessageResponse`: `sessionId, role, content, createdAt`
|
||||
- `ChatSessionDetailResponse`: 세션 필드 + `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")` 의 `items`
|
||||
- `useSessionMessages(id)` → `apiGet<SnapSessionDetail>(\`/chat/sessions/${id}/messages\`)`
|
||||
- `useCreateSession` → `apiPost<SnapSession>("/chat/sessions")`
|
||||
- `MOCK_SESSIONS`/`MOCK_CONVERSATIONS` import 제거.
|
||||
|
||||
### 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).
|
||||
- `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 이벤트 파싱
|
||||
- 이벤트 스위치에 `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. 검증
|
||||
|
||||
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 에서.
|
||||
@@ -0,0 +1,59 @@
|
||||
# 하이퍼워터폴 접목 설계 — spec-kit + superpowers 워크플로우 보강
|
||||
|
||||
- 날짜: 2026-07-27
|
||||
- 출처: rhwp Hyper-Waterfall 방법론 정리 문서 (myVault topics/2026-07/0005)
|
||||
- 결정 방식: brainstorming 스킬로 4문답 → 방안 A 승인
|
||||
|
||||
## 목적 (사용자 선택)
|
||||
|
||||
AI 페어프로그래밍의 두 약점을 문서로 막는다.
|
||||
|
||||
1. 세션 간 기억 유지 — 세션 끊겨도 "지금 뭐 하지 / 어디까지 / 왜 이렇게"가 0초에 복원
|
||||
2. 방향 교정 게이트 — 틀린 방향으로 확신하며 달리는 걸 계획·완료 승인에서 잡음
|
||||
3. 피드백·실패 자산화 — 사람 교정(feedback)과 실패 기록(troubleshootings) 영구 보존
|
||||
4. 문서 체계 — 위 셋을 담는 최소 폴더 구조
|
||||
|
||||
## 결정 사항
|
||||
|
||||
| 항목 | 결정 |
|
||||
|---|---|
|
||||
| 앞단 | spec-kit 설치해서 결합 (하이퍼워터폴은 보강재, plan 집은 `specs/<feature>/` 하나 유지) |
|
||||
| 승인 강도 | 계획(plan/tasks) + 완료(report)만 승인. 중간 단계는 보고서만 남기고 멈추지 않음 |
|
||||
| 식별자 | spec-kit feature 이름. 이슈 번호·manifest 4필드는 스킵 (문서 늘어나 헷갈리면 그때) |
|
||||
|
||||
## 폴더 구조
|
||||
|
||||
```
|
||||
specs/<feature>/ # spec-kit 표준 (spec.md, plan.md, tasks.md)
|
||||
stage-N.md # [추가] 단계 보고: 한 일 / 검증 결과 / 다음
|
||||
report.md # [추가] 완료 보고: 계획 vs 결과 + 달라진 점 + 남긴 것
|
||||
|
||||
docs/
|
||||
orders/YYYYMMDD.md # [추가] 오늘 할 일 + 진행 중 feature 포인터 (ztodo.md 승격)
|
||||
feedback/yyyy-mm-dd-<keyword>.md # [추가] 사람 교정 원문 + 왜 + 앞으로
|
||||
tech/<topic>.md # [추가] 기술 사실 영구화 (topic 당 1파일, 갱신형)
|
||||
troubleshootings/ # 기존 그대로
|
||||
```
|
||||
|
||||
## 파이프라인 훅 5개
|
||||
|
||||
1. **세션 부트스트랩**: orders 최신 → 진행 중 feature 의 tasks.md + 최신 stage-N.md → 필요시 feedback/tech
|
||||
2. **executing-plans 도중**: task 묶음 끝날 때마다 stage-N.md 기록, 승인 대기 없이 진행
|
||||
3. **완료 시**: verification-before-completion → report.md → 사용자 최종 승인 → merge
|
||||
4. **사용자 교정 시**: Claude 가 자발적으로 feedback/ 기록 (자동 트리거)
|
||||
5. **기술 사실 발견 시**: tech/ 기록, plan 때 docs-lib 와 같이 참조
|
||||
|
||||
## 스킵한 것 (YAGNI)
|
||||
|
||||
- rhwp 이슈 번호 파일명 규칙 — 이슈·PR 안 쓰는 솔로 로컬 워크플로우
|
||||
- manifest 4필드(kind/status/canonical/last_verified) — 문서 수십 개 넘어 캐논이 헷갈리면 도입
|
||||
- PR 리뷰 라우팅·CI 게이트 — GitHub Actions 파이프라인 없음
|
||||
|
||||
## 도입 작업 (이 설계의 구현 범위)
|
||||
|
||||
1. spec-kit 설치 (`specify init --here --ai claude --script ps`)
|
||||
2. `docs/orders`·`docs/feedback`·`docs/tech` 생성 + 각 README 양식
|
||||
3. CLAUDE.md 갱신 (파이프라인 훅 + 부트스트랩 섹션)
|
||||
4. ztodo.md 항목을 첫 orders 파일로 이사
|
||||
|
||||
도입 자체는 spec-kit 파이프라인 안 태움 (폴더 + 문서 편집뿐이라 너무 작음).
|
||||
@@ -0,0 +1,82 @@
|
||||
# 윈도우 데스크톱 런처 (WebView2 로 2_frontend 감싸기) — 설계
|
||||
|
||||
작성일: 2026-08-04
|
||||
스코프: **B (껍데기 + 창 관리)** — raycast/wox/alfred 스타일 런처 셸
|
||||
상태: 설계 승인됨 → 구현 계획(writing-plans) 대기
|
||||
|
||||
## 1. 뭘 만드나 / 왜
|
||||
|
||||
지금 `2_frontend/` React SPA 를 **윈도우 닷넷(WPF + WebView2)** 앱 안에 담아서, 전역 단축키로 소환하는 런처처럼 쓴다. 알맹이(챗·기능)는 지금 React 앱 그대로. 이번에 만드는 건 **껍데기(셸)** — 어떻게 뜨고 사라지고 상주하냐.
|
||||
|
||||
## 2. 핵심 전략 — V1 껍데기 재활용
|
||||
|
||||
`d:\project\021.code-assistant\3_windowsApp\` 에 V1 이 있음. 런처 껍데기(단축키·트레이·창관리)는 거기 다 만들어져 있어서 **그대로 이식**하고, "바뀐 로직"(닷넷이 챗·인증을 네이티브로 하던 부분)만 버린다. V2 는 React 가 `/api` 를 직접 부르니 그 네이티브 로직이 필요 없음.
|
||||
|
||||
| V1 요소 | V2 처리 |
|
||||
|---|---|
|
||||
| `CodeAssist.Shell` 전체 (HotKeyService·SingleInstanceGuard·ForegroundWindow·PasteService·ClipboardBackup·TrayIconHost·FramelessPaletteWindow·JsonWindowPlacementStore·ViteDevServer) | **그대로 이식** — 런처 원시기능, 로직 안 바뀜 |
|
||||
| `App.xaml.cs` 뼈대 (단일인스턴스→핫키→포그라운드 캡처→팔레트 토글→트레이) | **이식** — V1 이 디버그용으로 주석 처리해둔 트레이·blur-to-hide 를 되살림 |
|
||||
| WebView A/B 로딩 (DEBUG=vite, RELEASE=가상호스트 `SetVirtualHostNameToFolderMapping`) | **이식** — 소스 경로만 V1 의 `web/` → V2 의 `2_frontend/` 로 교체 |
|
||||
| `Core.Chat`·`Core.Auth`·네이티브 `ChatView`/ViewModels·브릿지의 chat/auth/session 핸들러·Markdig | **버림** (지금은). V2 React 가 `/api` 직접 호출로 다 함 |
|
||||
| 브릿지 `paste.code`·`window.hide`·`window.resize` | **v1 제외, 다음 단계** — 런처 감칠맛(직전 창에 붙여넣기·Esc숨김·자동높이). React 쪽 `postMessage` 한 줄 필요해서 v1 필수 아님 |
|
||||
|
||||
> **Core 는 나중에 참고할 것.** V1 의 `CodeAssist.Core`(Auth·Chat: AuthClient, DpapiTokenStore, SseChatClient, HttpSessionClient, 토큰 브릿지 등)는 지금은 안 쓰지만 버리는 게 아니라 **레퍼런스로 남긴다.** 나중에 (1) auth 브릿지를 되살려 C#↔JS 로 refresh 토큰 영속·자동 재인증을 붙이거나, (2) RELEASE `/api` 프록시를 네이티브로 처리할 때 이 코드가 출발점이 됨. 구현 시 V1 `Core` 를 열어보고 참고.
|
||||
|
||||
## 3. 프로젝트 구조 (V2)
|
||||
|
||||
```
|
||||
2_frontend/ (그대로, 손 안 댐)
|
||||
3_windowsApp/
|
||||
CodeAssist.App/ WPF 진입점 + PaletteWindow + WebHostView (챗/인증 로직 뺀 WebChatView)
|
||||
CodeAssist.Shell/ V1 그대로 이식 (재활용 핵심)
|
||||
CodeAssist.Tests/ Shell 관련 테스트만 이식 (WindowPlacement, SingleInstance 등)
|
||||
```
|
||||
|
||||
- **`Core` 프로젝트는 이번에 안 만든다** — 네이티브 챗/인증이 사라져 쓸 데 없음. auth 브릿지 되살릴 때 그때 V1 Core 참고해서 추가.
|
||||
- 스택: .NET 8 (`net8.0-windows`) · WPF · WebView2 · (WPF-UI 는 창 스타일용, 옵션)
|
||||
|
||||
## 4. 동작 흐름
|
||||
|
||||
1. 부팅 → 트레이 상주 (단일 인스턴스 — 두 번째 실행은 기존 창 소환 후 종료)
|
||||
2. `Ctrl+Alt+Space` → 직전 창 HWND 캡처 → 팔레트 Show (마지막 위치/크기 복원)
|
||||
3. 창 안엔 **2_frontend React 앱** (DEBUG=vite 핫리로드 / RELEASE=dist 가상호스트)
|
||||
4. `Esc` 또는 포커스 잃음(Deactivated) → Hide (위치·크기 저장)
|
||||
5. 트레이 우클릭 → "종료" (평소 X 는 종료 대신 숨김)
|
||||
|
||||
## 5. 창·단축키 기본값 (V1 준용)
|
||||
|
||||
- 전역 단축키: **`Ctrl+Alt+Space`** (`Alt+Space` 시스템 메뉴 충돌 회피)
|
||||
- 창: 테두리 있는 크기조절 가능한 일반 창, `CenterScreen` 최초 위치, 위치·크기 기억(`JsonWindowPlacementStore`)
|
||||
- (다음 단계) web 이 알려준 컨텐츠 높이로 스르륵 리사이즈 — spotlight 느낌. 브릿지 `window.resize` 되살릴 때.
|
||||
|
||||
## 6. WebView 로딩 (A/B 둘 다 지원, 소스 주소만 분기)
|
||||
|
||||
V1 `WebChatView.OnLoaded` 패턴 그대로:
|
||||
|
||||
- **DEBUG**: `ViteDevServer` 로 `2_frontend` 의 `npm run dev` 를 띄우고 포트 대기 → `http://localhost:<port>` navigate. 핫리로드. `/api` 는 vite 가 백엔드로 프록시(지금 그대로).
|
||||
- **RELEASE**: `2_frontend/dist` 를 출력 폴더로 복사 → `SetVirtualHostNameToFolderMapping("appassets.example", wwwroot, ...)` → `https://appassets.example/index.html` navigate.
|
||||
- `UserDataFolder` 는 temp 로 명시(초기화 실패 방지), 초기화/네비 로그를 temp 파일에 남김 — V1 패턴 유지.
|
||||
|
||||
주의: 2_frontend 는 **BrowserRouter** 라 RELEASE 가상호스트에서 딥링크 새로고침 시 404 가능 → 필요하면 그때 HashRouter 전환이나 fallback 처리. v1(DEBUG 중심)에선 문제 없음.
|
||||
|
||||
## 7. 알려진 미결(다음 단계) — RELEASE `/api` 프록시
|
||||
|
||||
- **DEBUG(vite)**: vite 가 `/api` 프록시 → 지금 인증/쿠키 그대로 동작. **v1 은 여기까지 완전히 굴러감.**
|
||||
- **RELEASE(가상호스트)**: React 가 `/api` 부르면 가상호스트 폴더에서 찾다 404. 닷넷이 `/api` 를 백엔드로 프록시해줘야 함(`WebResourceRequested` 가로채기 등). 근데 이게 **쿠키 origin·Entra 로그인과 엮여서 이번에 제외한 auth 스코프와 겹침.**
|
||||
- **결정**: v1 = **DEBUG 모드로 런처 껍데기 완성·검증**(단축키·트레이·창관리·React 로딩 확인). RELEASE 패키징의 `/api` 프록시 + 쿠키 origin + Entra 로그인은 **auth 스코프 되살릴 때 함께 푸는 다음 단계.** (V1 `Core` 참고)
|
||||
|
||||
## 8. 이번 스코프에서 명시적으로 제외
|
||||
|
||||
- Entra/MSAL 로그인 데스크톱 처리 (Q3 제외됨)
|
||||
- 런처 전용 신규 UI (C안 — 나중에)
|
||||
- 브릿지 paste/hide/resize (다음 단계)
|
||||
- RELEASE 패키징의 `/api` 프록시 (7번, auth 와 함께)
|
||||
|
||||
## 9. 성공 기준 (v1)
|
||||
|
||||
- [ ] `Ctrl+Alt+Space` 로 창이 뜨고/사라진다
|
||||
- [ ] 트레이 상주 + 우클릭 종료, X 는 숨김
|
||||
- [ ] 단일 인스턴스 (두 번째 실행이 기존 창 소환)
|
||||
- [ ] 창 위치·크기가 기억된다
|
||||
- [ ] 창 안에 2_frontend React 앱이 뜬다 (DEBUG=vite 핫리로드, `/api` 정상)
|
||||
- [ ] Shell 이식분 테스트 통과 (WindowPlacement, SingleInstance 등)
|
||||
Reference in New Issue
Block a user