Files
2026-09-16 17:22:14 +09:00

8.4 KiB

Implementation Plan: 스니펫 팔레트 (Snippet Palette)

Branch: 001-snippet-palette | Date: 2026-08-05 | Spec: spec.md

Input: Feature specification from specs/001-snippet-palette/spec.md

2026-09-11 실행 범위

현재 구현 계획은 spec.md의 단계별 런처 확정을 따름. 아래 .NET 초기 이식 계획은 미완료 저장 기능의 배경이며, 이번 작업은 Tauri 기준으로 진행함.

  1. SnippetPalettePage.tsx: 빈 검색 결과 숨김, 명시적인 선택만 프리뷰 표시, 검색·분류·재소환 시 선택 해제. 기존 검색·랭킹·복사 함수와 편집기를 재사용.
  2. SnippetRow.tsxPaletteShell.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(/snapSessionListPage: 자동포커스 검색 + ↑↓/Enter nav + IME 안전 + 단계 Esc), 앞 창에 붙여넣는 배선(pasteToAppPasteService), 전역 핫키·트레이·단일실행·앞창 캡처. 그래서 신규는 딱 세 곳만 새로 짠다: (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)

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)

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 타입). 별도 정당화 표 불필요.