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

16 KiB
Raw Permalink Blame History

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