# 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와 브라우저가 같은 화면 결과물 사용 | 빌드 설정 + 실제 실행 확인 |