# 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` 사용은 의도된 선택**이며, 다른 앱의 동일 단축키를 가로채는 점은 감수한다(핫키는 설정 한 곳에서 교체 가능).