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,118 @@
# Contract: 브릿지 계약 → 환경별 전달 수단 매핑
**Created**: 2026-08-11 | **Updated**: 2026-09-11
## 이 문서의 위치
**메시지 계약의 진실원천은 여기가 아니다.** 원본은 아래 둘이고, 이 문서는 같은 메시지를 Tauri·참고용 legacy·브라우저가 어떻게 나르는지만 대응시킨다.
- `specs/001-snippet-palette/contracts/bridge-messages.md` — window.hide / paste.code / navigate / snippets.*
- `specs/002-capture-to-chat/contracts/bridge-capture.md` — capture.image
메시지의 의미·필드·에러 규칙을 바꿔야 한다면 원본을 먼저 고친다. Tauri 사정만으로 여기서 다른 계약을 만들지 않는다.
---
## 1. JS → 데스크톱 앱 (요청)
| 계약 메시지 | 참고용 legacy (.NET) | 기준 앱 (Tauri) |
|---|---|---|
| `{type:"window.hide"}` | `postMessage``type` 스위치 | `invoke("window_hide")` |
| `{type:"window.drag"}` | `postMessage``type` 스위치 | `invoke("window_drag")` |
| `{type:"window.snippetLayout", stage}` | 전송하지 않음 | `invoke("window_snippet_layout", { stage })` |
| `{type:"paste.code", text}` | `postMessage``type` 스위치 | `invoke("paste_code", { text })` |
| `{type:"route.changed", path}` | `postMessage``type` 스위치 | `invoke("report_route", { path })` |
| `{type:"snippets.list", reqId}` | `postMessage` + reqId 대기 | `invoke("snippets_list")` |
| `{type:"snippets.create", reqId, snippet}` | 〃 | `invoke("snippets_create", { snippet })` |
| `{type:"snippets.update", reqId, snippet}` | 〃 | `invoke("snippets_update", { snippet })` |
| `{type:"snippets.delete", reqId, name}` | 〃 | `invoke("snippets_delete", { name })` |
| `{type:"snippets.recordUse", reqId, name}` | 〃 | `invoke("snippets_record_use", { name })` |
### 커맨드 이름 규칙
계약의 `type`**점 → 밑줄, camelCase → snake_case** 로 바꾼 것. 기계적이라 외울 게 없다.
```
snippets.recordUse → snippets_record_use
route.changed → report_route ← 예외 1건
```
**`route.changed` 만 예외**인 이유: 이건 "무엇이 바뀌었다"는 통보라 `route_changed` 로 하면 커맨드가 아니라 이벤트처럼 읽힌다. 껍데기가 하는 일(보고 받기)을 이름에 담았다.
`window.snippetLayout`의 단계·크기·복원 계약은 스니펫 [원본 계약](../../001-snippet-palette/contracts/bridge-messages.md)의 Tauri 절을 따름.
### reqId는 Tauri에 없다
legacy는 요청·응답을 `reqId`로 짝맞춘다. `invoke`는 Promise를 돌려주므로 Tauri 경로에는 `reqId`가 실리지 않는다.
**그래도 프론트 표면은 같다**`request(type, payload)` 시그니처가 유지되므로 `snippets.api.ts`는 실행 환경을 모른다.
---
## 2. 데스크톱 앱 → JS (푸시)
| 계약 메시지 | 참고용 legacy (.NET) | 기준 앱 (Tauri) |
|---|---|---|
| `{type:"navigate", path}` | `PostWebMessageAsJson` | `emit("bridge", …)` |
| `{type:"paste.target", name, app}` | 〃 | 〃 |
| `{type:"capture.image", dataUrl}` | 〃 | 〃 |
### 채널은 하나, 페이로드는 동일
```jsonc
// .NET 이 보내는 것
{ "type": "navigate", "path": "/snippet" }
// Tauri 가 보내는 것 — 이벤트 이름만 "bridge", 페이로드는 글자 그대로 같음
{ "type": "navigate", "path": "/snippet" }
```
**메시지 종류마다 이벤트 이름을 나누지 않는다.** Tauri 관용으로는 `bridge:navigate`처럼 쪼개는 게 자연스럽지만, 그러면 프론트 `bridgeNavigate.ts``type` 분기를 다시 써야 한다. **한 채널로 몰면 그 분기와 `CustomEvent` dispatch를 그대로 재사용**할 수 있다.
### 필드 이름은 JS 쪽 표기를 따른다
`capture.image``dataUrl`**camelCase 그대로** 나간다. Rust 내부 변수명이 무엇이든 직렬화 결과가 계약과 같아야 한다.
---
## 3. 오류 표현
| 상황 | 참고용 legacy | Tauri | 프론트가 받는 것 |
|---|---|---|---|
| 정상 | `{ok:true, data}` | `invoke` resolve | 값 |
| 실패 (중복 이름 등) | `{ok:false, error:"한글 메시지"}` | `invoke` reject (한글 메시지) | `Error` |
| **아직 안 만듦** | 해당 없음 | `invoke` reject (한글 "아직 안 됨") | `Error` |
세 경우 모두 프론트에서는 `Error`로 도착해 react-query `onError` → sonner 토스트로 흐른다. 실행 환경별 분기가 화면에 생기지 않는다.
### 미구현은 성공을 흉내내지 않는다
Tauri에서 아직 안 만든 기능은 빈 값(`[]`, `null`)으로 답하지 않는다. 구현 중 빠진 기능을 정상 결과와 구분할 수 있어야 한다.
---
## 4. 껍데기 없음 (브라우저)
기존 동작 그대로. 계약상 변화 없음.
| 방향 | 동작 |
|---|---|
| JS → 껍데기 (단방향) | 조용히 no-op |
| JS → 껍데기 (응답 필요) | 즉시 reject — "데스크톱 전용" |
| 껍데기 → JS | 안 옴 |
`npm run dev` 로 브라우저에서 화면만 열어도 안 깨져야 한다 (FR-003).
---
## 5. 검증
이 매핑이 맞는지는 아래로 확인한다. 자동화 대상과 수동 대상이 갈린다.
| 항목 | 방법 |
|---|---|
| 호스트 3종 판별 | 자동 (vitest, mock) |
| 브라우저에서 no-op / reject | 자동 (vitest) |
| legacy adapter 동작 | 자동 (기존 브릿지 테스트 3개) |
| Tauri에서 실제 왕복 | 수동 ([quickstart.md](../quickstart.md)) |
| Tauri와 브라우저가 같은 화면 결과물 사용 | 빌드 설정 + 실제 실행 확인 |