# Phase 0: Research — Tauri 기준 데스크톱 앱 **Created**: 2026-08-11 | **Updated**: 2026-09-09 | **Plan**: [plan.md](./plan.md) 기술 결정 12건. 각 항목은 **결정 / 왜 / 버린 대안** 형식. --- ## R1. 개발 환경: Rust + MSVC 준비 완료 **현황 (2026-08-15 확인)**: `rustup` + `stable-x86_64-pc-windows-msvc`, VS 2026 Community의 C++ 도구와 Windows 11 SDK 설치가 끝났다. `rustc`로 실제 링크까지 통과했다. **결정**: 현재 MSVC 툴체인을 그대로 쓴다. 별도 Build Tools나 GNU 툴체인을 추가하지 않는다. **왜**: Tauri와 Win32 API를 이 PC의 실제 Windows 환경에서 빌드·검증할 수 있다. 이미 준비된 도구 외 설치는 필요 없다. **영향 범위**: 모든 Rust 작업을 바로 시작할 수 있다. --- ## R2. 이미 있는 미완성 스캐폴드를 어떻게 할 것인가 **현황**: `4_rust_tauri/` 에 `create-tauri-app` 으로 뽑은 뼈대 + 손으로 쓴 소스가 섞여 있다. `Cargo.toml` 의 lib 이름은 `codeassist_tauri_lib` 인데 `main.rs` 는 생성기 기본값 `_rust_tauri_lib::run()` 을 부른다 → **현재 빌드 불가**. **결정**: **이어 쓴다.** 단 자산을 두 등급으로 나눠 다르게 취급한다. | 등급 | 대상 | 취급 | |---|---|---| | 신뢰 | `icons/`(13개), `build.rs`, `capabilities/`, `.gitignore` | 그대로 유지. 생성기 산출물이라 검증됨 | | **재검증 대상** | 손으로 쓴 `src/**/*.rs`, `Cargo.toml`, `tauri.conf.json` | **컴파일 검증 0줄.** 첫 빌드에서 전부 다시 본다 | **왜**: 아이콘 13종(.ico/.icns/.png 다중 해상도)은 손으로 못 만들고, 지우고 재생성해도 **같은 생성기를 다시 돌리는 것**이라 얻는 게 없다. 반대로 손으로 쓴 Rust 는 한 줄도 컴파일 안 해봤으므로 "있으니 맞겠지"로 넘기면 안 된다. **버린 대안**: 통째로 지우고 재생성 — 아이콘을 다시 뽑아야 하고 결과물이 같음. 순수 낭비. **첫 할 일**: `main.rs` 의 lib 이름 한 줄 정합. --- ## R3. Tauri v2 API 를 기억으로 쓰지 않는다 (docs-lib) **결정**: 첫 Rust 코드를 쓰기 **전에** `4_rust_tauri/docs-lib/tauri-v2.md` 를 만들고, 실제로 쓸 API 표면을 거기 박제한 뒤 그걸 보고 짠다. `2_frontend/docs-lib/tauri-api.md` 도 같이(프론트 `invoke`/`listen` 용). **왜**: CLAUDE.md §3 의 명시 규칙이기도 하지만, 이 건에서 특히 위험하다. Tauri v2 는 **2.x 안에서도 시그니처가 바뀐 자리**가 있다 — 특히: - `global_shortcut().on_shortcut()` 핸들러 인자 개수 (2개 → 3개) - 트레이 `show_menu_on_left_click` 의 이름·존재 여부 - `Emitter`/`Manager` trait 를 어디서 import 하는지 이걸 기억으로 쓰면 첫 빌드가 에러 벽에 부딪히고, 그때 "내가 틀린 건지 버전이 다른 건지"를 구분 못 해 시간을 크게 잃는다. **박제할 표면**: 플러그인 4종 init, `TrayIconBuilder`, `Menu`/`CheckMenuItem`, `WebviewWindow`(show/hide/set_focus/set_always_on_top/start_dragging/is_visible), `#[tauri::command]` + `State`, `app.emit`, `app.path().app_local_data_dir()`, capabilities 권한 이름. **버린 대안**: 일단 짜고 컴파일 에러로 배우기 — 에러 메시지가 매크로 안에서 나면 원인이 안 보인다. 특히 `generate_handler!` 안 에러는 해독이 어렵다. --- ## R4. 프론트 호스트 감지: 사용 시점 자동 판별, 빌드는 한 벌 **결정**: bridge를 사용할 때 작동하는 통로를 판별한다. 별도 빌드 플래그나 테스트 전용 reset API는 만들지 않는다. | 호스트 | 판별 | 전송 수단 | |---|---|---| | Tauri | `typeof window.__TAURI_INTERNALS__?.invoke === "function"` | `invoke()` + `listen()` | | 참고용 legacy | `window.chrome?.webview` 있음 | `postMessage` + `addEventListener("message")` | | 없음(브라우저) | 둘 다 없음 | 단방향 no-op / 응답 필요는 즉시 reject | **판별 순서**: `tauri` → `webview2` → `browser`. Windows의 Tauri도 WebView2 위에서 돌 수 있으므로 Tauri의 실제 invoke 통로를 먼저 확인한다. **왜 사용 시점인가**: 호스트 확인은 속성 두 개를 보는 작은 작업이고, 테스트가 런타임에 mock을 심어도 별도 재판별 훅 없이 실제 흐름을 검증할 수 있다. 앱에서는 호스트가 바뀌지 않으므로 결과도 결정적이다. **버린 대안**: - Vite `define` / 환경변수 — 브라우저 개발 화면과 Tauri가 서로 다른 번들을 갖게 됨 - 모듈 로드 때 상수로 고정 + reset API — 테스트만 위한 공개 표면이 생김 - 공식 `isTauri()` — 별도 `window.isTauri` 플래그를 보므로 실제 invoke 가능 여부보다 약함 --- ## R5. Rust→JS 푸시: 단일 채널에 `{type, ...}` 그대로 **결정**: 이벤트 이름은 `"bridge"` 하나. 페이로드는 기존 계약과 같은 모양을 유지한다. ``` app.emit("bridge", { "type": "navigate", "path": "/snippet" }) ``` **왜**: `bridgeNavigate.ts` 안의 `navigate` / `paste.target` / `capture.image` 분기와 `CustomEvent` dispatch를 그대로 재사용할 수 있다. 바뀌는 건 리스너를 붙이는 transport뿐이다. **버린 대안**: 메시지 종류마다 이벤트 이름 분리 — Tauri 관용에는 가깝지만 기존 분기 로직을 다시 쓰게 되어 코드가 늘어난다. --- ## R6. JS→Rust: `invoke` 가 reqId 인프라를 대체한다 **결정**: `snippetBridge.ts` 의 reqId 채번·`pending` Map·리스너 필터를 **삭제**하고, `request(type, payload)` 시그니처만 남겨 `invoke(커맨드명, payload)` 로 넘긴다. **왜**: 요청↔응답 짝맞춤은 `invoke` 가 Promise 로 이미 해준다. 그 위에 또 얹으면 같은 일을 두 번 한다. `snippets.api.ts` 는 `request("snippets.list")` 를 그대로 부르므로 **호출부 무변경**. **타입명 → 커맨드명** 매핑 표는 `contracts/transport-mapping.md`. **주의**: 기존 껍데기 경로에서는 reqId 인프라가 **그대로 필요하다**. 그래서 이 코드는 지우는 게 아니라 `transport.ts` 의 webview2 구현 안으로 **옮긴다**. --- ## R7. 단축키: Tauri가 표준 조합을 소유한다 **결정**: Tauri 앱은 `Ctrl+Shift+7/8/9`를 쓴다. 각각 스니펫·챗봇·캡쳐다. **왜**: Tauri가 기준 앱이므로 사용자가 익숙한 조합을 그대로 이어받는다. 참고용 legacy 앱과 동시 실행은 지원하지 않는다. **같이 결정**: 등록 실패는 앱을 죽이지 않고 어떤 조합이 실패했는지 알린다(FR-010). --- ## R8. 상태와 기존 데이터 경로 **결정**: | 데이터 | 경로 | 처리 | |---|---|---| | 창 위치·크기·핀 | `%LocalAppData%\com.codeassist.app\` | 제품용 identifier에 저장 | | 스니펫 DB | `%LocalAppData%\CodeAssist\snippets.db` | 기존 파일을 직접 열어 이어서 사용 | **왜**: 창 상태는 Tauri 제품 설정으로 관리하고, 사용자가 쌓은 스니펫은 별도 이전 없이 보존해야 한다(FR-020). **스니펫 규칙**: 기존 스키마와 저널 모드를 바꾸지 않는다. legacy 코드는 참고 원본으로 남아 있으므로 데이터 형식을 함부로 갈라놓지 않는다. **버린 대안**: 새 DB로 복사 — 데이터가 두 벌로 갈리고 이후 변경이 나뉜다. --- ## R9. 창 상태 저장은 플러그인에 맡긴다 **결정**: `tauri-plugin-window-state` 를 쓰고, 화면 밖 복원 방지도 플러그인 기본 동작에 맡긴다. 다만 **핀(항상 위) 상태만** 직접 저장한다(플러그인이 always-on-top 은 안 봄). **왜**: .NET 판의 `JsonWindowPlacementStore`(46줄) + `WindowPlacement.IsVisibleWithin`(가상 화면 겹침 판정)이 통째로 사라진다. 직접 짤 이유가 없다. **버린 대안**: 직접 구현해서 .NET 과 동형 유지 — 동형일 필요가 없다. 사용자에게 보이는 결과(껐다 켜면 그 자리)만 같으면 됨. --- ## R10. 붙여넣기: .NET 이 박제한 함정을 그대로 옮긴다 **참조 원본**: `3_windowsApp/CodeAssist.Shell/Platform/PasteService.cs` — 주석에 "**함정 박제**"라고 적혀 있는 자리. **결정**: 순서를 **그대로** 지킨다. 1. 클립보드에 코드 적재 2. **우리 창을 숨기기 전에** 직전 창 활성화 (`SetForegroundWindow`) 3. 짧게 대기 (창 전환 느린 대상 대비) 4. `Ctrl+V` 주입 5. 클립보드 원복 **안 함** — 붙인 내용을 남긴다(사용자 요청 사항) **왜 순서가 중요한가**: Windows 의 포그라운드 락 때문에 `SetForegroundWindow` 는 **우리 창이 떠 있을 때만** 먹힌다. 창을 먼저 숨기면 실패한다. 이건 .NET 판에서 이미 한 번 당한 자리라, 새로 설계하지 말고 베낀다. **대기 시간**: .NET 은 80ms. **그대로 시작하되 조정 가능한 값으로 둔다** — 물리적 타이밍이라 환경 따라 다를 수 있고, 최소 코드로는 안 보이는 종류의 값이다. **실패 시**: 붙이기는 포기하되 **코드는 클립보드에 남긴다** → 사용자가 직접 `Ctrl+V` (FR-014). **Rust 수단**: `windows` crate 로 `SetForegroundWindow` / `keybd_event`(또는 `SendInput`) 직접 호출. 플러그인 없음. --- ## R11. 캡쳐: DPI 인지와 좌표 변환 **참조 원본**: legacy 앱의 캡쳐 구현과 `4_rust_tauri/docs-lib/tauri-v2.md` §8. **결정**: Tauri 런타임이 이미 켜는 PerMonitorV2 DPI 인지를 그대로 쓰고, 오버레이는 전체 가상 화면을 덮는 투명·항상위 창으로 만든다. 드래그로 고른 논리 좌표를 물리 픽셀로 변환한 뒤 해당 영역을 자른다. **왜**: 배율이 다른 모니터에서 논리 좌표를 그대로 픽셀로 쓰면 고른 영역과 결과가 어긋난다. 별도 DPI 매니페스트는 필요 없다. **Rust 수단**: `windows` crate로 `BitBlt`를 직접 호출한다. 기존 Windows 동작을 가장 적은 의존성으로 옮길 수 있다. --- ## R12. 테스트 전략: 자동으로 될 것만 자동으로 **결정**: | 대상 | 방식 | 근거 | |---|---|---| | 프론트 transport 선택·브라우저 동작 | `vitest` 자동 | 순수 분기이며 호스트 3종을 mock으로 태울 수 있음 | | 기존 브릿지 테스트 3개 | `vitest` — transport mock으로 교체 | 화면 소비자 동작을 유지하는 회귀 검사 | | 챗봇 토글 규칙, 스니펫 검증 | `cargo test` 자동 | 순수 로직 | | 창·트레이·핫키 | 실제 Tauri 앱 수동 확인 | OS 자원 테스트 하네스가 본체보다 커짐 | | 붙여넣기·캡쳐 | 실제 Windows 환경 수동 확인 | 포커스·모니터 배율에 달림 | **왜 실제 Tauri 확인을 붙이나**: transport mock만 맞고 실제 invoke/listen 권한이나 이벤트 연결이 깨진 상태를 자동 검사만으로는 잡을 수 없다. 브라우저 확인 뒤 Tauri 앱을 직접 실행한다. --- ## 미해결로 남기는 것 | 항목 | 왜 지금 안 정하나 | |---|---| | 붙여넣기 대기 시간 최종값 (R10) | 물리 타이밍. 80ms로 시작하고 실기기에서 조정 | ### R11 DPI 인지 — 해소 (2026-08-15) docs-lib(`4_rust_tauri/docs-lib/tauri-v2.md` §8)에서 확인했다. `tao` 0.35.3이 이벤트 루프를 만들 때 `SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)`를 호출하므로 우리가 별도 설정할 것은 없다. **귀결**: US4에서 좌표가 어긋나면 DPI 설정이 아니라 논리↔물리 좌표 변환을 본다.