Initial Commit

This commit is contained in:
2026-09-16 17:22:14 +09:00
commit 858ee9e9da
335 changed files with 123898 additions and 0 deletions
@@ -0,0 +1,85 @@
# Contract: 스니펫 브릿지 메시지
기준 앱은 Tauri이고 환경별 전달은 `specs/004-tauri-shell/contracts/transport-mapping.md`를 따름. 아래 C# 요청·응답 설명은 참고용 legacy 채널 계약임.
## Tauri 단계별 창 크기 (2026-09-11)
`{type:"window.snippetLayout", stage:"search"|"results"|"preview"|"editor"}``invoke("window_snippet_layout", {stage})`.
- 검색 `640×84`, 결과 `640×440`, 프리뷰 `840×520`, 편집 `960×600` 논리 픽셀. 실제 크기와 DPI·작업 영역 보정은 Rust 소유.
- 호스트가 Tauri일 때만 전송. legacy·브라우저는 화면 단계만 바뀌고 실제 창 크기는 제어하지 않음.
- 다른 route 보고 시 원래 배치를 복원. 종료 직전에 복원한 크기를 저장해 축소 상태가 일반 창 크기를 덮어쓰지 않음.
- 허용되지 않은 stage와 크기 조작 실패는 reject. 기존 transport 오류 토스트로 표시.
## Tauri 앱에 붙여넣기 (2026-09-11)
`{type:"paste.code", text}``invoke("paste_code", {text})`. 스니펫 `Ctrl+Enter`와 기존 코드뷰어 붙여넣기 버튼이 같은 경로를 사용함.
- `Enter`는 기존 원문 복사 후 창 숨김, 복사 버튼은 복사만 하고 창 유지. `Ctrl+Enter`는 선택한 코드에만 적용하고 데스크톱·미편집·비조합 상태에서 한 번만 실행.
- 호스트는 키 해제를 기다린 뒤 직전 앱으로 돌아가고, 실제 입력 직전에 대상·포커스·키 상태를 다시 확인. 대기 중 UI는 멈추지 않고 중복 붙여넣기는 실행하지 않음.
- 성공한 입력 전송 뒤에만 창을 숨김. 실패하면 창을 유지하거나 다시 표시하고 기존 bridge 오류 토스트로 안내.
- 클립보드 기록 뒤에 실패하면 원문은 그대로 남김. 클립보드 자체 기록 실패에는 복사됐다고 안내하지 않음.
- 다른 앱의 편집기가 실제로 내용을 받아들였는지까지 성공 응답이 보장하지는 않음. 별도 입력창에서 end-to-end로 검사하고, 관리자 권한 앱 등 Windows 입력 제한은 오류로 구분.
## 기존 (변경 없음)
| 방향 | 메시지 | 처리 |
|---|---|---|
| JS→C# | `{type:"window.hide"}` | 런처 창 숨김 |
| JS→C# | `{type:"paste.code", text}` | 직전 창에 `text` 붙여넣기 + 창 숨김 |
현재 Enter는 선택한 스니펫 원문을 클립보드에 복사한 뒤 창을 숨김. 기존 `paste.code`는 코드뷰어의 붙여넣기 버튼과 스니펫 Ctrl+Enter에서 사용함. Tauri의 오류·창 표시 동작은 위 절을 따름.
## 신규 — C#→JS 푸시(내비게이션)
| 방향 | 메시지 | 처리 |
|---|---|---|
| C#→JS | `{type:"navigate", path}` | 프론트가 react-router로 `path`로 이동 |
- 스니펫 핫키(Ctrl+1) → C# `ShowPalette()``SendToWeb({type:"navigate", path:"/snippet"})`.
- 프론트 `bridgeNavigate.ts`가 수신 → 등록된 navigate 콜백 호출.
## 신규 — 스니펫 데이터(요청 → 응답, reqId 상관)
각 요청은 프론트가 부여한 `reqId`(문자열, 단조 증가) 포함. C#는 같은 `reqId`로 결과를 돌려준다.
### 요청 (JS→C#)
| 메시지 | 의미 |
|---|---|
| `{type:"snippets.list", reqId}` | 전체 스니펫(usage 병합) 반환 |
| `{type:"snippets.create", reqId, snippet:{name,desc,body,category}}` | 생성(중복 name 거부) |
| `{type:"snippets.update", reqId, snippet:{name,desc,body,category}}` | 수정(name=키, 나머지 갱신) |
| `{type:"snippets.delete", reqId, name}` | 삭제 |
| `{type:"snippets.recordUse", reqId, name}` | usage count+1, last_used=now |
### 응답 (C#→JS)
```jsonc
// 성공
{ "type":"snippets.result", "reqId":"<echo>", "ok":true, "data": <op > }
// 실패
{ "type":"snippets.result", "reqId":"<echo>", "ok":false, "error":"<사람이 읽을 메시지>" }
```
| 요청 | 성공 `data` |
|---|---|
| `snippets.list` | `Snippet[]` (usageCount·lastUsed 병합) |
| `snippets.create` / `update` | 저장된 `Snippet` (또는 갱신 후 전체 목록 — 구현 단순한 쪽) |
| `snippets.delete` | `{name}` |
| `snippets.recordUse` | `{name, usageCount, lastUsed}` |
**에러 계약**: 중복 name 생성/빈 필드 → `ok:false` + 한글 메시지. 프론트는 `ok:false``Error`로 reject → react-query `onError` → sonner 토스트(memos 패턴).
## C# 라우팅
`OnWebMessageReceived``type`으로 분기:
- `window.hide` / `paste.code` → 기존.
- `snippets.*``SnippetBridge`(신규)에 위임: JSON에서 `reqId`·`snippet`·`name` 파싱 → `SnippetRepository` 호출 → `SendToWeb``snippets.result` 응답.
- 파싱은 기존 `type`+`text`만 읽던 걸 확장(요청별 필요한 필드 추출).
## 프론트 리스너
- `snippetBridge.ts`: `Map<reqId,{resolve,reject}>`, `chrome.webview.addEventListener("message")`에서 `snippets.result`만 필터해 매칭 reqId resolve/reject. 각 op은 `postMessage`+promise.
- `bridgeNavigate.ts`: 같은 message 이벤트에서 `navigate`만 필터. (리스너 여러 개 가능 — 서로 독립.)
- 순수 브라우저(`isWebView()===false`): 요청은 즉시 빈 결과/비활성(데스크톱 전용, research R7).