Files
CODE_ASSISTANT/specs/001-snippet-palette/spec.md
T
2026-09-16 17:22:14 +09:00

193 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Feature Specification: 스니펫 팔레트 (Snippet Palette)
**Feature Branch**: `001-snippet-palette`
**Created**: 2026-08-05
**Status**: Draft
**Input**: 003(winkit-snippet, 파이썬 PySide6)의 snippet 기능을 021.code-assistant-v2(React SPA + .NET WebView2 데스크톱 런처)에 재개발해 이식. snap과 별도 route(`/snippet`), 동일 아키텍처. 저장은 로컬 SQLite(003 스키마 그대로, C# 호스트가 소유). 전용 전역 핫키 Ctrl+1로 소환. UI/UX는 Raycast 결.
## 2026-09-11 확정: 단계별 스니펫 런처
이번 사용자 승인으로 아래 초기 초안의 항상 열린 목록·자동 프리뷰·Enter 붙여넣기 설명은 이 절로 대체함. 미완료 CRUD·사용기록 저장은 이번 범위에 포함하지 않음.
- 기준 앱은 Tauri(`specs/004-tauri-shell/`), 소환은 `Ctrl+Shift+7`. 클린 블루와 기존 Enter 원문 복사를 유지함.
- 처음 소환하면 검색창만 표시. 공백만 입력한 경우도 결과·분류·프리뷰를 숨김.
- 검색어 입력 시 목록과 분류·하단 힌트만 표시. 첫 행을 자동 선택하지 않음.
- `↓`는 첫 행, `↑`는 마지막 행부터 선택. 이후 화살표·행 클릭으로 선택한 코드만 오른쪽에 표시.
- 프리뷰에는 코드·문법 색상·옅은 줄 번호만 표시. 이름·설명·분류 중복, 장식 점, 코드 번호·언어 헤더, 박스 테두리·그림자는 제거하고 세로 스크롤은 하나만 둠.
- 복사·앱에 붙여넣기는 하단 액션바에 모음. 복사 버튼은 창을 유지하고, 검색창/선택 행 Enter는 기존처럼 복사 후 닫음. 버튼에 포커스한 Enter는 해당 버튼 동작을 실행함. 챗봇 코드 카드는 기존 모양을 유지함.
- 검색어·분류 변경 시 선택을 해제. `Esc`로 검색을 지우면 검색창에 포커스를 돌려주고, 빈 검색에서 `Esc`는 창을 숨김.
- 선택한 행에서 `Enter`로 원문을 복사. 미선택·IME 조합 중에는 복사하지 않음.
- 선택한 코드의 `Ctrl+Enter`는 기존 앱에 붙여넣기 버튼과 같은 경로를 실행함. 데스크톱에서만 사용하고 미선택·IME 조합·편집 중·자동 반복·추가 modifier 조합은 무시함.
- 붙여넣기는 누른 키를 놓은 뒤 대상 창을 확인하고 실행. 대상 없음·창 전환 실패·키 해제 시간 초과 때는 창과 오류 안내를 유지하며, 이미 복사한 원문은 클립보드에 남김.
- 실제 창의 콘텐츠 크기: 검색 `640×84`, 결과 `640×440`, 프리뷰 `840×520`, 편집 `960×600` 논리 픽셀. DPI와 모니터 작업 영역·바깥 테두리를 반영해 화면 안에 맞춤.
- 챗봇 복귀와 종료 때 원래 창 크기·위치를 복원. 최대화 전 일반 크기도 보존하며, 축소 크기를 다음 시작 크기로 저장하지 않음.
- 재소환은 검색어·분류·선택·편집/삭제 확인을 닫고 검색부터 다시 시작함.
---
## 개요
자주 쓰는 코드 조각(스니펫)을 전역 핫키로 언제든 불러, 몇 글자 검색하고 방금까지 쓰던 창에 바로 붙여넣는 기능. 003(파이썬)에 있던 걸 이 앱(React + WebView2) 구조로 다시 만든다. 핵심 동작은 003 그대로, 화면은 Raycast 커맨드 팔레트 느낌으로.
**디자인 방향(Raycast):** 가운데 떠있는 프레임리스 커맨드바 — 맨 위 큰 검색 입력, 그 아래 결과 리스트(행마다 제목 + 부제 + 우측 액세서리), 우측에 선택 항목 프리뷰, 맨 아래 액션 힌트바(왼쪽 컨텍스트 / 오른쪽 `⏎ 붙여넣기` 등). 키보드 우선, 어두운 톤·둥근 모서리.
---
## Clarifications
### Session 2026-08-05
- Q: v1 시작 시점의 데이터 소스(빈 DB / 003 파일 1회 복사 / 003 파일 직접 공유) → A: 첫 실행 시 003의 `snippets.db`를 앱 데이터 폴더로 1회 복사(시드) 후 독립 운영.
---
## User Scenarios & Testing *(mandatory)*
### User Story 1 - 핫키로 검색해서 앞 창에 붙여넣기 (Priority: P1)
다른 앱(에디터·터미널 등)에서 작업하다 `Ctrl+1`을 누르면 스니펫 팔레트가 화면 가운데 뜬다. 검색어를 몇 글자 치면 결과가 실시간으로 좁혀지고, 자주·최근 쓴 것이 위에 온다. `Enter`로 선택하거나 `Alt+1~9`로 번호 매긴 행을 바로 골라 붙여넣으면, 팔레트가 사라지고 **방금 전까지 쓰던 창에 스니펫 내용이 그대로 들어간다.**
**Why this priority**: 이게 이 기능의 존재 이유. 이거 하나만 돌아도 도구로서 가치가 있음(스니펫은 미리 넣어둔 상태 가정). 나머지는 다 이걸 돕는 것.
**Independent Test**: 스니펫 몇 개가 저장된 상태에서, 아무 텍스트 에디터에 포커스 → `Ctrl+1` → 검색어 입력 → `Enter` → 에디터에 스니펫 body가 원문 그대로 삽입되는지로 단독 검증 가능.
**Acceptance Scenarios**:
1. **Given** 에디터에 커서가 있고 스니펫이 여러 개 저장돼 있음, **When** `Ctrl+1`을 누름, **Then** 화면 가운데에 검색 입력에 포커스된 팔레트가 뜨고 결과가 usage 순으로 보인다.
2. **Given** 팔레트가 떠있음, **When** 검색어를 입력, **Then** 공백으로 나눈 키워드가 **전부** `name`+`desc`(대소문자 무시)에 들어있는 스니펫만 남고, usage 순으로 정렬된다.
3. **Given** 결과가 보임, **When** `↑`/`↓`로 이동, **Then** 선택 행이 바뀌고 우측 프리뷰가 그 스니펫의 body를 보여준다.
4. **Given** 원하는 행이 선택됨, **When** `Enter`, **Then** 팔레트가 사라지고 직전 활성 창에 그 스니펫 body가 붙여넣어지며, 그 스니펫의 usage(횟수+최근시각)가 올라간다.
5. **Given** 결과 상위 9개가 `1~9` 번호로 표시됨, **When** `Alt+3`, **Then** 3번 행이 선택 이동 없이 즉시 붙여넣어진다.
6. **Given** 검색어가 있음, **When** `Esc`, **Then** 검색어가 먼저 비워지고, 이미 비어있으면 팔레트가 숨는다.
---
### User Story 2 - 스니펫 만들고 고치고 지우기 (Priority: P2)
팔레트에서 새 스니펫을 만들거나(주로 방금 복사한 코드를 등록), 기존 걸 고치거나 지운다. 새로 만들 때 body는 현재 클립보드 내용으로 미리 채워진다.
**Why this priority**: 스니펫이 있어야 P1이 쓸모 있음. 다만 초기엔 수동/시드로 채워도 되니 P1보단 뒤.
**Independent Test**: 팔레트에서 생성 버튼/키 → 이름·내용 입력 → 저장 → 검색에 새 스니펫이 뜨는지. 편집·삭제도 같은 식으로 단독 검증.
**Acceptance Scenarios**:
1. **Given** 팔레트가 떠있고 클립보드에 코드가 있음, **When** 생성 액션, **Then** 편집 다이얼로그가 열리고 body가 클립보드 내용으로 채워지며 category 기본값은 "코드".
2. **Given** 생성 다이얼로그, **When** 이름 비움 또는 body 비움으로 저장 시도, **Then** 저장이 거부되고 뭐가 문제인지 알려준다.
3. **Given** 생성 다이얼로그, **When** 이미 있는 이름으로 저장 시도, **Then** 중복이라 거부된다.
4. **Given** 결과에서 한 행 선택, **When** `F2` 또는 더블클릭, **Then** 편집 다이얼로그가 열리고 이름은 읽기전용(=키), 나머지는 수정 가능.
5. **Given** 편집 다이얼로그, **When** 삭제 액션 + 확인, **Then** 그 스니펫이 지워지고 목록에서 사라진다.
6. **Given** 저장/수정/삭제 완료, **When** 다이얼로그 닫힘, **Then** 팔레트 목록이 현재 검색어 기준으로 새로고침된다.
---
### User Story 3 - category 칩으로 좁혀보기 (Priority: P3)
결과가 많을 때 category(전체/코드/…) 칩으로 한 분류만 추려본다.
**Why this priority**: 편의 기능. 검색만으로도 대부분 커버됨.
**Independent Test**: 서로 다른 category의 스니펫이 있을 때, 특정 칩 선택 → 그 category 것만 남는지.
**Acceptance Scenarios**:
1. **Given** 여러 category의 스니펫, **When** "코드" 칩 선택, **Then** category가 "코드"인 것만 검색 대상이 된다.
2. **Given** 칩바에 포커스 없이 팔레트가 떠있음, **When** `Ctrl+←`/`Ctrl+→`, **Then** 선택 칩이 좌우로 바뀐다.
3. **Given** "전체" 칩, **When** 선택, **Then** 모든 category가 검색 대상이 된다.
---
### Edge Cases
- **붙여넣기 실패**(직전 창 포커스 복원 실패): 스니펫 내용을 클립보드에 남겨 사용자가 수동으로 붙일 수 있게 한다(기존 붙여넣기 배선의 fallback 준용).
- **검색 결과 0개**: 빈 상태 안내를 보여준다(에러 아님).
- **`Alt+숫자`가 결과 개수보다 큼**: 무시.
- **IME 조합 중 `Enter`**: 조합 확정만 되고 붙여넣기는 일어나지 않는다.
- **전역 `Ctrl+1` 충돌**: `Ctrl+1`은 전역이라 다른 앱의 `Ctrl+1`을 가로챈다. 감수하는 선택. 단 핫키는 한 곳(config 상수)에서 바꿀 수 있어야 한다.
- **로컬 저장 불가/용량 초과**: 저장 실패를 사용자에게 알리고, 최소한 이번 세션 동작은 막지 않는다.
- **스니펫 0개 상태에서 소환**: 빈 목록 + "스니펫을 만들어보라"는 안내를 보여준다.
- **첫 실행 시 003의 `snippets.db`가 없음**: 시드를 건너뛰고 빈 DB로 시작(에러 아님).
---
## Requirements *(mandatory)*
### Functional Requirements
**소환 · 표시**
- **FR-001**: 전역 핫키 `Ctrl+1`로 스니펫 팔레트를 소환할 수 있어야 한다. 이 핫키는 snap 소환(`Ctrl+Alt+Space`)과 별개다.
- **FR-002**: 소환 시, 팔레트가 뜨기 **직전에 활성이던 창**을 붙여넣기 대상으로 기억해야 한다.
- **FR-003**: 팔레트는 화면 가운데 떠서 검색 입력에 자동 포커스되며, snap과 **다른 경로**(`/snippet`)로 열린다.
- **FR-004**: 팔레트는 Raycast식 레이아웃을 따른다 — 상단 검색 입력, 아래 결과 리스트(행: 제목=name, 부제=desc, 우측 액세서리=번호/category 등), 우측 프리뷰 패널, 하단 액션 힌트바.
**검색 · 랭킹**
- **FR-005**: 검색어를 공백으로 나눈 키워드가 **전부(AND)** 스니펫의 `name`+`desc` 합친 문자열(대소문자 무시)에 포함될 때만 결과에 포함한다. `body`는 검색 대상이 아니다. 퍼지/오타 보정 없음.
- **FR-006**: 검색어가 비어있으면 (선택된 category의) 전체 스니펫을 보여준다.
- **FR-007**: 모든 결과는 usage 순 — 사용횟수 내림차순, 같으면 최근 사용시각 내림차순, 그것도 같으면 name 오름차순 — 으로 정렬한다.
- **FR-008**: 상위 9개 결과에 `1~9` 번호를 매겨 표시한다.
**붙여넣기 · usage**
- **FR-009**: `Enter`는 현재 선택 행을, `Alt+1~9`는 해당 번호 행을 (선택 이동 없이) 즉시 붙여넣는다.
- **FR-010**: 붙여넣기는 FR-002에서 기억한 대상 창에 스니펫 `body`**원문 그대로(공백·줄바꿈 보존)** 삽입한다. (기존 WebView2 붙여넣기 배선 재사용.)
- **FR-011**: 붙여넣기 성공 시 해당 스니펫의 사용횟수 +1, 최근 사용시각을 현재로 갱신하고, 팔레트를 숨긴다.
**키 조작**
- **FR-012**: `↑`/`↓`로 선택 이동, 이동 시 우측 프리뷰가 갱신된다.
- **FR-013**: `Esc`는 단계 처리 — 검색어가 있으면 비우고, 없으면 팔레트를 숨긴다.
- **FR-014**: 프리뷰는 선택 스니펫의 `body`를 읽기전용·고정폭으로, 원문 그대로 보여준다.
**관리(CRUD)**
- **FR-015**: 새 스니펫을 만들 수 있어야 하며, 생성 시 body 초기값은 현재 클립보드 내용, category 기본값은 "코드"다.
- **FR-016**: 저장은 name과 body가 모두 비어있지 않을 때만 허용한다. name은 정규화(대문자화, 공백→`_`)되어 **유일**해야 하며 중복은 거부한다.
- **FR-017**: 기존 스니펫을 편집(`F2`/더블클릭)할 수 있고, 이때 name은 읽기전용(=식별 키)이다.
- **FR-018**: 스니펫을 삭제(편집 다이얼로그 안, 확인 후)할 수 있다.
- **FR-019**: 생성/편집/삭제 후 팔레트 목록은 현재 검색어 기준으로 새로고침된다.
**분류**
- **FR-020**: category 칩("전체" + 존재하는 category들)으로 검색 대상을 한 분류로 좁힐 수 있다. `Ctrl+←`/`Ctrl+→`로 칩 이동.
**저장**
- **FR-021**: 스니펫과 usage 정보는 로컬에 영구 저장되어 앱을 재시작해도 유지된다.
- **FR-022**: 저장 접근은 하나의 데이터 인터페이스 뒤에 숨겨, 저장 방식을 바꿔도 화면·검색·랭킹 로직이 영향받지 않아야 한다.
- **FR-023**: 저장 스키마는 003의 것을 그대로 따른다 — 스니펫 테이블(name PK, desc, body, category)과 별도 usage 테이블(name PK, count, last_used).
- **FR-024**: 앱 데이터 폴더에 DB가 아직 없고 003의 `snippets.db`가 존재하면, 첫 실행 시 그 파일을 앱 데이터 폴더로 1회 복사해 시드한다. 없으면 빈 DB로 시작한다. 이후엔 두 파일이 독립이다.
### Key Entities
- **Snippet**: 붙여넣을 코드 조각 한 개.
- `name` — 식별 키(정규화·유일), 결과 행의 제목
- `desc` — 짧은 설명, 결과 행 부제 · 검색 대상
- `body` — 실제 붙여넣어지는 내용(원문 그대로 보존)
- `category` — 분류 라벨(기본 "코드")
- `usageCount` — 붙여넣기 성공 누적 횟수
- `lastUsed` — 마지막 붙여넣기 시각
- (003의 `change_word1/2`는 안 쓰는 유물이라 가져오지 않음)
---
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: 핫키를 누른 뒤 원하는 스니펫을 붙여넣기까지 몇 초 안(핫키 → 검색어 몇 글자 → `Enter`/`Alt+숫자`)에 끝난다.
- **SC-002**: 자주 쓰는 스니펫이 검색·기본 목록 상위에 노출되어, 검색어 없이도 상위 몇 개 안에서 고를 수 있다.
- **SC-003**: 앱을 껐다 켜도 저장된 스니펷과 usage(횟수·최근시각)가 100% 유지된다.
- **SC-004**: 붙여넣어진 내용이 스니펫 body와 글자·공백·줄바꿈까지 완전히 일치한다.
- **SC-005**: 핫키 소환에서 팔레트가 보이기까지 체감상 즉시(수백 ms) 뜬다.
- **SC-006**: 붙여넣기 대상이 항상 "팔레트를 부르기 직전 창"이다(엉뚱한 창에 들어가지 않는다).
---
## Assumptions
- **저장은 v1에서 로컬 SQLite(단일 PC)**이며, C# 데스크톱 호스트가 DB 파일(`snippets.db`, 003과 동일 스키마)을 소유한다. 기기 간 동기화는 안 함. 데이터 인터페이스(FR-022)를 통해 나중에 백엔드로 교체할 여지만 남긴다.
- **첫 실행 시 003의 `snippets.db`를 앱 데이터 폴더로 1회 복사(시드)** 후 두 앱은 독립. 이후 003을 고쳐도 이 앱엔 영향 없음(그 반대도). 이건 파일 복사 한 번이라 별도 "임포트 기능"과 다름 — v1 범위 안.
- **붙여넣기·핫키·앞창 캡처는 기존 데스크톱 런처 배선을 재사용**한다(앞 창에 코드 붙여넣는 기능은 이미 있음). 신규 배선은 (1) 스니펫 전용 핫키 등록, (2) 소환 시 팔레트를 스니펫 경로로 이동시키는 신호, (3) React↔SQLite를 잇는 브릿지 데이터 메서드(요청→응답: 목록/생성/수정/삭제/사용기록)와 C# `SnippetRepository`(003 포팅)다.
- **팔레트 UI는 snap의 기존 커맨드 팔레트(키보드 네비게이션 등) 구조를 재사용**하되, 겉모습을 Raycast 결로 다시 입힌다.
- **003의 기존 `snippets.db` 데이터 임포트는 v1 범위 밖**(나중에 별도 임포트 수단으로).
- **백엔드 `/snippets` 동기화는 v1 범위 밖.**
- **전역 `Ctrl+1` 사용은 의도된 선택**이며, 다른 앱의 동일 단축키를 가로채는 점은 감수한다(핫키는 설정 한 곳에서 교체 가능).