# Implementation Plan: 스니펫 팔레트 (Snippet Palette) **Branch**: `001-snippet-palette` | **Date**: 2026-08-05 | **Spec**: [spec.md](./spec.md) **Input**: Feature specification from `specs/001-snippet-palette/spec.md` ## 2026-09-11 실행 범위 현재 구현 계획은 `spec.md`의 단계별 런처 확정을 따름. 아래 .NET 초기 이식 계획은 미완료 저장 기능의 배경이며, 이번 작업은 Tauri 기준으로 진행함. 1. `SnippetPalettePage.tsx`: 빈 검색 결과 숨김, 명시적인 선택만 프리뷰 표시, 검색·분류·재소환 시 선택 해제. 기존 검색·랭킹·복사 함수와 편집기를 재사용. 2. `SnippetRow.tsx`와 `PaletteShell.tsx`: 테두리 없는 행과 선택 강조, 복사 전용 런처에는 대상 앱 배지를 표시하지 않음. 3. `window.snippetLayout`을 기존 transport로 전달. Rust `commands.rs`가 단계별 크기를 선택하고 `shell/window.rs`가 이전 배치 보관·작업 영역 보정·복원을 담당. 4. `report_route`에서 다른 화면 복원. 창이 닫힐 때 복원한 실측값을 window-state 캐시에 명시적으로 저장해 종료 이벤트 순서에 의존하지 않음. 5. 검색 전환·원문 복사·IME·Esc 포커스·삭제 확인 닫힘은 Vitest, 경계 계산은 Rust 검사. 실제 Tauri에서 크기 전환·최대화 복원·축소 상태 종료 후 재시작 확인. ## Summary 003(파이썬 winkit-snippet)의 스니펫 검색·붙여넣기 기능을 이 앱(React SPA + .NET WebView2 런처)에 재개발 이식한다. 전역 핫키 `Ctrl+1`로 팔레트를 소환해, 검색해서 방금 쓰던 창에 스니펫을 붙여넣는다. UI는 Raycast 결. 저장은 C# 호스트가 소유한 로컬 SQLite(003 스키마 그대로), React는 WebView2 브릿지로 접근. **기술 접근(재사용 우선):** 이 앱은 이미 팔레트에 필요한 걸 대부분 갖고 있다 — 커맨드 팔레트 UI(`/snap`의 `SessionListPage`: 자동포커스 검색 + ↑↓/Enter nav + IME 안전 + 단계 Esc), 앞 창에 붙여넣는 배선(`pasteToApp`→`PasteService`), 전역 핫키·트레이·단일실행·앞창 캡처. 그래서 신규는 딱 세 곳만 새로 짠다: (1) 두 번째 전역 핫키, (2) C#→JS `navigate` 신호, (3) React↔SQLite 요청-응답 브릿지 + C# `SnippetRepository`. 나머지는 memos(feature 모듈 카논)·snap(팔레트)의 패턴 복제·리스킨. ## Technical Context **Language/Version**: TypeScript 5 / React 18 (Vite) · C# / .NET (WPF, WebView2) **Primary Dependencies**: @tanstack/react-query, shadcn/ui, sonner (프론트) · Microsoft.Web.WebView2, **Microsoft.Data.Sqlite(신규 추가)** (C#) **Storage**: 로컬 SQLite 파일 `snippets.db`(C# 호스트 소유, 003과 동일 스키마). 브라우저 아님. **Testing**: vitest(프론트: 검색/랭킹 순수함수 + 브릿지 mock) · xUnit(C#: `SnippetRepository` CRUD/usage against temp db) — TDD(RED→GREEN)는 superpowers 뒷단에서. **Target Platform**: Windows 데스크톱(WebView2 런처). 순수 브라우저(dev)는 브릿지 no-op → 스니펫 기능 비활성(데스크톱 전용). **Project Type**: 기존 웹앱(2_frontend) + 데스크톱 호스트(3_windowsApp)에 feature 추가. **Performance Goals**: 핫키→팔레트 표시 체감 즉시(수백 ms). 검색은 수백~수천 스니펫에서 substring 필터로 즉각(퍼지 없음). **Constraints**: 붙여넣기 대상은 항상 "소환 직전 창"(기존 캡처 로직 재사용). 붙여넣기 순서 계약(포커스→클립보드→포커스→Ctrl+V)은 기존 `PasteService`가 이미 지킴. **Scale/Scope**: 개인 단일 PC. 스니펫 수백~수천 규모. 화면 1개(팔레트) + 편집 다이얼로그. ## Constitution Check *GATE: Phase 0 전 통과, Phase 1 후 재확인.* `.specify/memory/constitution.md`는 빈 템플릿 → 형식 게이트 없음. **실질 캐논은 `CLAUDE.md`**(재사용 우선·feature 모듈 컨벤션·react-query·TDD·docs-lib 선참조). 이에 비춘 점검: - ✅ **재사용 우선**: 팔레트 UI(snap)·붙여넣기(pasteToApp)·핫키/트레이/캡처 전부 기존 것 재사용. 신규는 최소 3곳. - ✅ **feature 모듈 컨벤션**: `features/snippets/{api,hooks,components,pages}` + react-query(KEY 상수·invalidate·sonner 한글 토스트·ApiError 분기)로 memos 패턴 준용. - ⚠️ **의도된 편차 1 — 데이터 접근이 `@/lib/api/client`(apiGet/apiPost) 경유 아님**: CLAUDE.md 4번은 "axios/HTTP는 apiGet/apiPost만"인데, 스니펫 저장은 **HTTP 백엔드가 아니라 로컬 SQLite(WebView2 브릿지)**다. 그래서 `snippets.api.ts`는 브릿지 백엔드로 구현. react-query로 감싸는 건 유지. → 백엔드가 아니라 편차가 정당(사용자가 로컬 SQLite로 결정). tasks/impl에서 명시. - ⚠️ **의도된 편차 2 — 타입 위치**: 서버 DTO는 `types/api.ts`가 규칙이나, `Snippet`은 서버 DTO가 아니라 로컬 엔티티 → snap이 `features/snap/contract/types.ts` 두는 것처럼 `features/snippets/types.ts`에 둠. 정당. Gate: **PASS** (편차 2건은 문서화된 정당 편차, 복잡도 증가 아님). ## Project Structure ### Documentation (this feature) ```text specs/001-snippet-palette/ ├── plan.md # 이 파일 ├── research.md # Phase 0 — 미해결점 결정(브릿지 왕복·멀티핫키·SQLite·시드·프리뷰) ├── data-model.md # Phase 1 — Snippet/Usage 엔티티 + DB 스키마 + TS 타입 + 랭킹규칙 ├── contracts/ # Phase 1 — 브릿지 메시지 계약 + 프론트 데이터 인터페이스 │ ├── bridge-messages.md │ └── snippet-data-interface.md ├── quickstart.md # Phase 1 — end-to-end 검증 시나리오 └── checklists/requirements.md ``` ### Source Code (repository root) ```text 2_frontend/src/ ├── features/snippets/ # ★ 신규 feature (memos 모양 + snap 팔레트 재사용) │ ├── types.ts # Snippet 로컬 타입(서버 DTO 아님) │ ├── api/snippets.api.ts # 데이터 인터페이스(브릿지 백엔드). list/create/update/delete/recordUse │ ├── core/search.ts # 순수 검색(키워드 AND, name+desc, 대문자) — 단위테스트 │ ├── core/ranking.ts # usage 정렬 (-count,-lastUsed,name) — 단위테스트 │ ├── hooks/useSnippets.ts # react-query (SNIPPETS_KEY·invalidate·토스트) │ ├── components/ # SnippetRow·PreviewPane·CategoryChips·EditDialog (Raycast 리스킨) │ └── pages/SnippetPalettePage.tsx # snap SessionListPage 복제 + Raycast 룩 + Enter→pasteToApp ├── lib/bridge/ │ ├── webviewBridge.ts # (기존) + C#→JS 리스너 확장 │ ├── snippetBridge.ts # ★ 신규: 요청-응답(reqId) 래퍼 — snippets.api 가 사용 │ └── bridgeNavigate.ts # ★ 신규: C#→JS navigate 수신 → react-router 이동 ├── config/routes.ts # + PATHS.SNIPPET └── routes.tsx # + SnippetPalettePage 라우트(SnapLayout 그룹) 3_windowsApp/ ├── CodeAssist.Shell/ │ ├── Storage/SnippetRepository.cs # ★ 신규: 003 repository.py+usage.py 포팅(Microsoft.Data.Sqlite) │ ├── Storage/SnippetSeed.cs # ★ 신규: 첫 실행 시 003 snippets.db 1회 복사 │ ├── Platform/HotKeyService.cs # 확장: 멀티 핫키(id별 등록/디스패치) │ └── CodeAssist.Shell.csproj # + Microsoft.Data.Sqlite ├── CodeAssist.App/ │ ├── App.xaml.cs # 확장: Ctrl+1 두 번째 핫키 + WndProc wParam 전달 │ └── Views/WebHostView.xaml.cs # 확장: snippets.* 브릿지 핸들러 + navigate 송신(SendToWeb) └── CodeAssist.Tests/ # + SnippetRepository 테스트 ``` **Structure Decision**: 기존 2폴더 구조(2_frontend feature 모듈 + 3_windowsApp WPF/Shell) 유지. 신규 코드는 위 ★ 표시. 새 프로젝트/레이어 없음. ## Complexity Tracking > Constitution 위반 없음. 편차 2건은 §Constitution Check에 정당화(로컬 SQLite라 HTTP 클라이언트 미경유 / 로컬 엔티티라 feature-local 타입). 별도 정당화 표 불필요.