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

6.2 KiB

Research: 스니펫 팔레트 (Phase 0)

브레인스토밍·clarify에서 큰 결정은 끝났고(저장=로컬 SQLite, 핫키=Ctrl+1, UI=Raycast, 데이터=003 시드), 여기선 구현 미해결점만 결정한다.


R1. React ↔ SQLite 브릿지: 요청→응답 방식

결정: 기존 postMessage 채널을 reqId 상관관계(correlation) 왕복으로 확장. AddHostObjectToScript(COM 호스트 객체) 안 씀.

  • JS→C#: {type:"snippets.list", reqId} 식으로 요청(각 호출마다 증가하는 reqId).
  • C#→JS: Web.CoreWebView2.PostWebMessageAsJson({type:"snippets.result", reqId, ok, data|error})로 응답.
  • JS: Map<reqId, {resolve,reject}> 유지, chrome.webview.addEventListener("message")에서 매칭 reqId를 resolve.

Rationale:

  • 기존 브릿지가 이미 postMessage(단방향)라 같은 메커니즘 하나로 통일 — 스타일 일관, 학습비용 0.
  • COM 마샬링(hostObjects) 회피: 배열/객체 반환 시 JSON 문자열 마샬링이 번거롭고, 기존 코드와 이질적.
  • SQLite 호출은 로컬·즉각이라 C#에서 동기로 처리 후 바로 응답 → async 복잡도 불필요(UI 스레드에서 안전, 기존 paste도 UI 스레드).

Alternatives 기각:

  • AddHostObjectToScript: 호출부는 깔끔(await hostObjects.snippets.list())하나 COM 세팅·마샬링·기존 스타일 이탈.
  • 별도 로컬 HTTP 서버(C#): 과함(YAGNI). 브릿지로 충분.

R2. C#→JS 방향(navigate 신호)

결정: CoreWebView2.PostWebMessageAsJson로 C#→JS 푸시. JS는 chrome.webview.addEventListener("message", …)로 수신. 이 채널을 (a) 스니펫 핫키 시 {type:"navigate", path:"/snippet"}와 (b) R1의 snippets.result 양쪽에 공용.

  • WebHostViewSendToWeb(object msg) 추가(PostWebMessageAsJson(JsonSerializer.Serialize(msg))).
  • 프론트 bridgeNavigate.ts: navigate 메시지 수신 → 등록된 react-router navigate 콜백 호출. React 최상위에서 mount 시 콜백 등록.

Rationale: WebView2 기본 제공 기능, 의존성 0. 팔레트는 부팅 시 /snap으로 뜨므로(WebHostView StartPath), 스니펫은 이 navigate로 진입.

주의: 여러 addEventListener 등록 가능 → snippetBridge(결과용)와 bridgeNavigate(내비용)가 각자 리스너를 걸고 type으로 필터. 서로 결합 안 함.

R3. 멀티 핫키(C#)

결정: HotKeyServiceid별 다중 등록으로 확장.

  • 현재: 단일 HotKeyId=0x1000, 단일 HotKeyPressed(Action), ProcessMessage(int msg)wParam(=핫키 id)을 무시.
  • 변경:
    • Register(int id, uint mods, uint vk) — 등록 id를 인자로, 내부 HashSet<int>로 추적, Dispose 시 전부 Unregister.
    • event Action<int>? HotKeyPressed — 눌린 id 전달.
    • ProcessMessage(int msg, IntPtr wParam) — wParam.ToInt32()가 핫키 id. App.WndProc가 wParam을 넘기도록 수정.
  • App: id 0x1000=snap 토글(Ctrl+Alt+Space, 기존), 0x1001=스니펫(Ctrl+1). 등록 실패해도 죽지 말고 트레이 안내(기존 패턴).

Ctrl+1 상수: MOD_CONTROL(0x2), VK_1(0x31). 핫키 조합은 App 상수 한 곳에 모아 나중에 바꾸기 쉽게(spec Edge: 전역 Ctrl+1 가로채기 감수).

스니펫 핫키 동작: 눌리면 ShowPalette()(앞창 캡처+표시, 기존) 후 SendToWeb({type:"navigate", path:"/snippet"}). 이미 /snippet에서 보이는 상태면 토글로 숨김(선택, 구현 단순하면 적용).

R4. SQLite 라이브러리·DB 경로

결정:

  • 라이브러리: Microsoft.Data.Sqlite (경량·MS 공식) 를 CodeAssist.Shell.csproj에 추가. (현재 리포에 SQLite 참조 전무.)
  • DB 경로: %LOCALAPPDATA%\CodeAssist\snippets.db (쓰기 가능·안정. WebView2 UserDataFolder(temp)와 별개 — temp는 캐시성).
  • 스키마: 003 그대로 — snippets(name PK, desc, body, category), usage(name PK, count, last_used). (data-model.md 참조.)
  • 저장 방식: 003의 통짜 replace 대신 행 단위 upsert/delete로(개별 CRUD API라 그게 자연스러움). 랭킹·검색은 프론트가 하므로 C# repo는 순수 CRUD + usage upsert만.

Rationale: MS 공식·의존성 가벼움. SQLitePCLRaw 번들 포함이라 네이티브 배포도 무난.

R5. 첫 실행 시드(003 데이터 1회 복사)

결정: SnippetSeed — 앱 시작 시 대상 DB(%LOCALAPPDATA%\CodeAssist\snippets.db)가 없을 때만, 003의 snippets.db가 알려진 경로에 있으면 그 파일을 대상 경로로 복사. 있으면(=이미 시드됨) 아무것도 안 함. 003 파일이 없으면 빈 DB로 시작(에러 아님).

  • 003 경로 확인은 구현 시점 lookup: 003(winkit)의 데이터 디렉토리 규칙(winkit.shell 데이터 폴더 + snippets.db)을 그때 확인해 상수화. 못 찾으면 빈 DB로 폴백(기능 안 막음).
  • 파일 복사 한 번이라 "임포트 기능"급 작업 아님(spec FR-024).

R6. 프리뷰 렌더링

결정: 프리뷰·body 표시는 순수 <pre> 고정폭(공백·줄바꿈 원문 보존). react-markdown 안 씀.

Rationale: body는 마크다운이 아니라 날 코드다. 마크다운 파싱하면 특수문자(*,#,`)가 깨지고 붙여넣을 원문과 화면이 달라짐. <pre>가 요구사항(원문 그대로·고정폭·no-wrap)을 정확히 만족. → docs-lib(react-markdown) 트리거 안 함.

R7. 순수 브라우저(dev) 폴백

결정: 스니펫 기능은 데스크톱(WebView2) 전용. 순수 브라우저에서 isWebView()===falsesnippets.api는 빈 목록/명확한 비활성 상태를 반환(throw 대신 graceful). 붙여넣기·핫키·SQLite 모두 호스트가 필요하므로 순수 브라우저 지원은 비목표.

Rationale: DEBUG 개발도 WebView2 안에서 vite를 물어 돌리므로(WebHostView가 localhost:15173을 WebView2로 네비) 브릿지가 살아있음 → 개발 지장 없음. 순수 크롬 탭 지원은 YAGNI.

미해결/구현 시점 확인 목록

  • 003 snippets.db 실제 경로(R5) — winkit 데이터 디렉토리 규칙 확인 후 상수화.
  • Microsoft.Data.Sqlite 버전 핀 — 구현 시 최신 안정.