16 KiB
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:
- Given 에디터에 커서가 있고 스니펫이 여러 개 저장돼 있음, When
Ctrl+1을 누름, Then 화면 가운데에 검색 입력에 포커스된 팔레트가 뜨고 결과가 usage 순으로 보인다. - Given 팔레트가 떠있음, When 검색어를 입력, Then 공백으로 나눈 키워드가 전부
name+desc(대소문자 무시)에 들어있는 스니펫만 남고, usage 순으로 정렬된다. - Given 결과가 보임, When
↑/↓로 이동, Then 선택 행이 바뀌고 우측 프리뷰가 그 스니펫의 body를 보여준다. - Given 원하는 행이 선택됨, When
Enter, Then 팔레트가 사라지고 직전 활성 창에 그 스니펫 body가 붙여넣어지며, 그 스니펫의 usage(횟수+최근시각)가 올라간다. - Given 결과 상위 9개가
1~9번호로 표시됨, WhenAlt+3, Then 3번 행이 선택 이동 없이 즉시 붙여넣어진다. - Given 검색어가 있음, When
Esc, Then 검색어가 먼저 비워지고, 이미 비어있으면 팔레트가 숨는다.
User Story 2 - 스니펫 만들고 고치고 지우기 (Priority: P2)
팔레트에서 새 스니펫을 만들거나(주로 방금 복사한 코드를 등록), 기존 걸 고치거나 지운다. 새로 만들 때 body는 현재 클립보드 내용으로 미리 채워진다.
Why this priority: 스니펫이 있어야 P1이 쓸모 있음. 다만 초기엔 수동/시드로 채워도 되니 P1보단 뒤.
Independent Test: 팔레트에서 생성 버튼/키 → 이름·내용 입력 → 저장 → 검색에 새 스니펫이 뜨는지. 편집·삭제도 같은 식으로 단독 검증.
Acceptance Scenarios:
- Given 팔레트가 떠있고 클립보드에 코드가 있음, When 생성 액션, Then 편집 다이얼로그가 열리고 body가 클립보드 내용으로 채워지며 category 기본값은 "코드".
- Given 생성 다이얼로그, When 이름 비움 또는 body 비움으로 저장 시도, Then 저장이 거부되고 뭐가 문제인지 알려준다.
- Given 생성 다이얼로그, When 이미 있는 이름으로 저장 시도, Then 중복이라 거부된다.
- Given 결과에서 한 행 선택, When
F2또는 더블클릭, Then 편집 다이얼로그가 열리고 이름은 읽기전용(=키), 나머지는 수정 가능. - Given 편집 다이얼로그, When 삭제 액션 + 확인, Then 그 스니펫이 지워지고 목록에서 사라진다.
- Given 저장/수정/삭제 완료, When 다이얼로그 닫힘, Then 팔레트 목록이 현재 검색어 기준으로 새로고침된다.
User Story 3 - category 칩으로 좁혀보기 (Priority: P3)
결과가 많을 때 category(전체/코드/…) 칩으로 한 분류만 추려본다.
Why this priority: 편의 기능. 검색만으로도 대부분 커버됨.
Independent Test: 서로 다른 category의 스니펫이 있을 때, 특정 칩 선택 → 그 category 것만 남는지.
Acceptance Scenarios:
- Given 여러 category의 스니펫, When "코드" 칩 선택, Then category가 "코드"인 것만 검색 대상이 된다.
- Given 칩바에 포커스 없이 팔레트가 떠있음, When
Ctrl+←/Ctrl+→, Then 선택 칩이 좌우로 바뀐다. - 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사용은 의도된 선택이며, 다른 앱의 동일 단축키를 가로채는 점은 감수한다(핫키는 설정 한 곳에서 교체 가능).