Initial Commit
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# Specification Quality Checklist: Tauri 데스크톱 앱 전환
|
||||
|
||||
**Purpose**: 계획 단계로 넘어가기 전 spec의 완성도·품질 확인
|
||||
**Created**: 2026-08-11
|
||||
**Updated**: 2026-09-09
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] 구현 세부(언어·프레임워크·API)가 목적에 필요한 만큼만 들어감 — 주석 참조
|
||||
- [x] 사용자 가치·목적 중심
|
||||
- [x] 비개발자도 읽을 수 있게 쓰임
|
||||
- [x] 필수 섹션 전부 채움
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] `[NEEDS CLARIFICATION]` 표시 없음
|
||||
- [x] 요구사항이 검증 가능하고 애매하지 않음
|
||||
- [x] 성공 기준이 측정 가능함
|
||||
- [x] 성공 기준이 사용자 결과 중심임
|
||||
- [x] 수용 시나리오 전부 정의됨
|
||||
- [x] 엣지 케이스 식별됨
|
||||
- [x] 범위가 명확히 그어짐 — US1~US5 전부 완료
|
||||
- [x] 의존성·가정 식별됨
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] 기능 요구사항마다 수용 기준이 있음
|
||||
- [x] 유저 스토리가 주요 흐름을 덮음
|
||||
- [x] 성공 기준으로 달성 여부를 잴 수 있음
|
||||
- [x] 미결정 사항 없음
|
||||
|
||||
## Notes
|
||||
|
||||
**기술 이름에 대하여**: 이 기능은 제품의 기준 데스크톱 앱을 Rust + Tauri로 전환하는 작업이라 Tauri와 legacy .NET의 이름을 숨길 수 없다. 구체 API와 코드 조립법은 spec이 아니라 plan에 남긴다.
|
||||
|
||||
**범위 변경**: 2026-09-09 사용자 결정으로 A/B 비교가 끝났다. Tauri가 기준 앱이고 .NET은 참고용 legacy다. 동시 실행·설치물 비교·중간 중단을 빼고 US1~US5 전체 완료를 필수로 바꿨다.
|
||||
|
||||
**스캐폴드**: `4_rust_tauri/`의 생성기 산출물은 이어서 쓰고, 손으로 쓴 Rust 소스는 컴파일하며 다시 확인한다.
|
||||
@@ -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와 브라우저가 같은 화면 결과물 사용 | 빌드 설정 + 실제 실행 확인 |
|
||||
@@ -0,0 +1,116 @@
|
||||
# Phase 1: Data Model — Tauri 기준 데스크톱 앱
|
||||
|
||||
**Created**: 2026-08-11 | **Updated**: 2026-09-09 | **Plan**: [plan.md](./plan.md)
|
||||
|
||||
이 기능은 **새 데이터를 거의 안 만든다.** 기존 스니펫을 보존하고 Tauri의 창·화면 상태만 관리한다.
|
||||
|
||||
---
|
||||
|
||||
## 1. Snippet — 기존 데이터 유지
|
||||
|
||||
**저장소**: `%LocalAppData%\CodeAssist\snippets.db` (SQLite)
|
||||
|
||||
**출처**: `3_windowsApp/CodeAssist.Shell/Storage/SnippetRepository.cs`가 현재 형식의 참고 원본이다. Tauri는 **스키마를 바꾸지 않고** 기존 파일을 직접 쓴다.
|
||||
|
||||
| 필드 | 타입 | 규칙 |
|
||||
|---|---|---|
|
||||
| `name` | string | **키**. 중복 생성 거부 |
|
||||
| `desc` | string | 설명 |
|
||||
| `body` | string | 본문. 붙여넣을 때 **원문 그대로** (FR-013) |
|
||||
| `category` | string | 분류 |
|
||||
| `usageCount` | int | 사용할 때마다 +1 |
|
||||
| `lastUsed` | timestamp | 사용 시각 |
|
||||
|
||||
**검증 규칙**:
|
||||
- `name` 중복 생성 → 거부, 한글 오류 메시지
|
||||
- 빈 필수 필드 → 거부, 한글 오류 메시지
|
||||
- 오류는 화면에 토스트로 뜬다 (FR-022)
|
||||
|
||||
**상태 변화**: 생성 → (수정)\* → (사용 시 `usageCount`+1, `lastUsed` 갱신)\* → 삭제
|
||||
|
||||
**보존 규칙**:
|
||||
- 스키마 변경 금지
|
||||
- SQLite 저널 모드 변경 금지
|
||||
- 기존 파일이 없으면 같은 형식의 빈 저장소를 만든다
|
||||
|
||||
---
|
||||
|
||||
## 2. WindowState — Tauri 앱 상태
|
||||
|
||||
| 필드 | 의미 |
|
||||
|---|---|
|
||||
| 위치 (x, y) | 마지막 창 자리 |
|
||||
| 크기 (w, h) | 마지막 창 크기 |
|
||||
| `pinned` | 항상 위 고정 여부 |
|
||||
|
||||
**저장 위치**: `%LocalAppData%\com.codeassist.app\`. 위치·크기는 window-state 플러그인, `pinned`는 별도 단일 파일로 저장한다.
|
||||
|
||||
**복원 규칙**: 화면 밖 자리로는 복원하지 않는다. 모니터 구성이 바뀌어 저장된 자리가 화면 밖이면 보이는 자리로 되돌린다.
|
||||
|
||||
---
|
||||
|
||||
## 3. PasteTarget — 기존, 휘발성
|
||||
|
||||
창을 부르기 **직전**에 한 번 찍는 스냅샷. 저장 안 함(앱 종료 시 사라짐).
|
||||
|
||||
| 필드 | 의미 |
|
||||
|---|---|
|
||||
| 창 핸들 | 붙여넣을 대상 |
|
||||
| `name` | 창 제목 |
|
||||
| `app` | 프로그램 이름 (예: `Code`, `notepad`) |
|
||||
|
||||
**왜 스냅샷인가**: 나중에 그 창이 닫혀도 "어디에 붙는지" 표시는 남아야 한다(FR-015). 그래서 소환 시점에 제목·프로그램명을 **값으로 복사**해 둔다.
|
||||
|
||||
**생명주기**: 창 소환 시 갱신 → 화면에 배지로 표시 → 다음 소환 때 덮어씀
|
||||
|
||||
---
|
||||
|
||||
## 4. Routes — 기존, 껍데기 메모리
|
||||
|
||||
화면이 이동할 때마다 껍데기에 보고하는 값. 챗봇 단축키 토글 규칙(FR-011)에 쓴다.
|
||||
|
||||
| 필드 | 의미 | 초기값 |
|
||||
|---|---|---|
|
||||
| `current` | 화면이 지금 있는 자리 | `/snap` |
|
||||
| `lastSnap` | 마지막으로 있던 **비-스니펫** 자리 | `/snap` |
|
||||
|
||||
**갱신 규칙**: 보고받은 경로가 `/snippet` 으로 시작하지 **않으면** `lastSnap` 도 같이 갱신. `current` 는 항상 갱신.
|
||||
|
||||
**토글 판정**:
|
||||
```
|
||||
창이 보이는 중 AND current 가 /snippet 이 아님 → 숨긴다
|
||||
그 외 → 띄우고 lastSnap 으로 이동
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. BridgeMessage — 기존 계약
|
||||
|
||||
**진실원천은 여기가 아니다.** 아래 두 문서가 원본:
|
||||
- `specs/001-snippet-palette/contracts/bridge-messages.md`
|
||||
- `specs/002-capture-to-chat/contracts/bridge-capture.md`
|
||||
|
||||
환경별 전달 수단 매핑만 [contracts/transport-mapping.md](./contracts/transport-mapping.md)에 있다.
|
||||
|
||||
**이 기능이 계약에 더하는 제약 1개**:
|
||||
> Tauri에서 아직 안 만든 메시지는 성공 응답을 흉내내지 않는다. 사람이 읽을 수 있는 "아직 안 됨" 오류로 답하고, 화면은 그걸 토스트로 띄운다. (FR-023)
|
||||
|
||||
**왜 이게 데이터 규칙인가**: 빈 목록(`[]`)으로 답하면 "스니펫이 0개"와 "기능이 없음"이 구분되지 않는다. 구현 중 빠진 기능을 성공으로 오판하지 않게 한다.
|
||||
|
||||
---
|
||||
|
||||
## 6. HostKind — 프론트 내부
|
||||
|
||||
프론트가 bridge 사용 시점에 판별하는 값. 저장하지 않는다.
|
||||
|
||||
| 값 | 판별 조건 | 동작 |
|
||||
|---|---|---|
|
||||
| `tauri` | `typeof window.__TAURI_INTERNALS__?.invoke === "function"` | 기준 데스크톱 앱 방식 |
|
||||
| `webview2` | `window.chrome?.webview` 있음 | 참고용 legacy 방식 |
|
||||
| `browser` | 둘 다 없음 | 데스크톱 기능 없음 |
|
||||
|
||||
**판별 순서 고정**: `tauri` → `webview2` → `browser`. Windows Tauri가 WebView2를 사용해도 Tauri invoke 통로를 먼저 고른다.
|
||||
|
||||
**노출 범위**: `isWebView()` 같은 기존 함수의 속만 바뀐다. 화면 코드는 `HostKind`를 몰라야 한다(FR-002, SC-001).
|
||||
|
||||
---
|
||||
@@ -0,0 +1,170 @@
|
||||
# Implementation Plan: Tauri 기준 데스크톱 앱 전환
|
||||
|
||||
**Branch**: `004-tauri-shell` | **Created**: 2026-08-11 | **Updated**: 2026-09-09 | **Spec**: [spec.md](./spec.md)
|
||||
|
||||
**Input**: Feature specification from `/specs/004-tauri-shell/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
프론트(`2_frontend`)에 **호스트 자동 감지 seam** 하나를 두고, 기준 데스크톱 앱인 `4_rust_tauri`(Tauri v2)에 현재 화면·창 제어·붙여넣기·캡쳐·스니펫 저장을 전부 연결한다. 기존 `3_windowsApp`(.NET)은 삭제하지 않지만 참고용 legacy로만 남긴다.
|
||||
|
||||
핵심 설계는 "기존 브릿지 계약을 Tauri 수단으로 그대로 나른다"는 것. Rust→JS 푸시를 **단일 이벤트 채널에 `{type, ...}` 그대로** 실어보내면 프론트 `bridgeNavigate.ts`의 `type` 분기·CustomEvent dispatch 로직을 재사용할 수 있다. JS→Rust는 `invoke`의 Promise를 쓴다.
|
||||
|
||||
**프론트 빌드·dist는 한 벌**만 유지한다. Tauri 앱과 브라우저 개발 화면이 같은 결과물을 쓰고, 참고용 WebView2 adapter도 transport 안에 남긴다.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: TypeScript 5.6 (프론트) / Rust 2021 edition (기준 데스크톱 앱)
|
||||
|
||||
**Primary Dependencies**:
|
||||
- 프론트 추가: `@tauri-apps/api` v2 — 유일한 신규 프론트 의존성
|
||||
- Rust: `tauri` 2 (tray-icon, image-png), `tauri-plugin-global-shortcut`, `tauri-plugin-single-instance`, `tauri-plugin-window-state`, `rusqlite`(bundled), `windows`(Win32 붙여넣기·캡쳐), `serde`
|
||||
- legacy .NET: 변경·새 의존성 없음
|
||||
|
||||
**Storage**:
|
||||
- 스니펫: `%LocalAppData%\CodeAssist\snippets.db` — 기존 사용자 데이터를 그대로 이어서 씀(FR-020)
|
||||
- 창 위치·크기: `tauri-plugin-window-state` 경로(`%LocalAppData%\com.codeassist.app\`)
|
||||
- 핀(항상 위) 상태: 위 폴더의 단일 파일. window-state 플러그인이 always-on-top은 저장 안 함
|
||||
|
||||
**Testing**:
|
||||
- 프론트: `vitest` — transport seam 단위 테스트 + 기존 브릿지 테스트 3개 mock 교체
|
||||
- Rust: `cargo test` — 순수 로직(챗봇 토글 규칙, 스니펫 검증)
|
||||
- 수동: Tauri 창·트레이·표준 단축키·붙여넣기·멀티모니터 캡쳐를 `quickstart.md`에서 실제 실행
|
||||
- 브라우저: 개발 화면과 데스크톱 기능 없는 경로를 실제 브라우저에서 확인
|
||||
|
||||
**Target Platform**: Windows 11. WebView2 런타임 사용
|
||||
|
||||
**Project Type**: Rust + Tauri 데스크톱 앱 + React SPA 프론트. .NET 앱은 참고용 legacy
|
||||
|
||||
**Performance Goals**: 별도 계측 목표 없음. 전역 단축키와 붙여넣기·캡쳐 흐름에서 사용자가 느끼는 멈춤이나 무반응이 없어야 함
|
||||
|
||||
**Constraints**:
|
||||
- Tauri가 표준 단축키 `Ctrl+Shift+7/8/9`를 소유함
|
||||
- legacy 앱과 동시 실행은 지원하지 않음
|
||||
- 프론트 소비자 코드 변경 **0줄** (SC-001)
|
||||
- US1~US5 전부 완료하며 미구현 성공 응답을 남기지 않음
|
||||
- 기본 실행·배포·검증에서 .NET을 요구하지 않음 (SC-008)
|
||||
|
||||
**Scale/Scope**: 프론트 변경은 `lib/bridge/` 4파일 + 관련 테스트로 국한. 데스크톱 기능은 `4_rust_tauri` 안에서 완성
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: 구현 전에 통과했고 설계 갱신 후 다시 확인함.*
|
||||
|
||||
`.specify/memory/constitution.md`는 빈 템플릿이다. CLAUDE.md 규칙을 게이트로 쓴다.
|
||||
|
||||
| 게이트 (출처) | 판정 | 근거 |
|
||||
|---|---|---|
|
||||
| **재사용 우선** (CLAUDE.md 서두) | ✅ | 프론트는 기존 브릿지 계약·분기·소비자 코드를 재사용하고, Rust는 기존 스캐폴드와 검증된 .NET 동작 순서를 참고함 |
|
||||
| **최소 diff / YAGNI** (ponytail) | ✅ | 프론트 신규 파일은 transport 하나. 창 상태·단일 인스턴스·단축키는 공식 플러그인을 씀 |
|
||||
| **API 호출은 `@/lib/api/client`만** (§4) | ✅ 해당없음 | 서버 API를 건드리지 않음 |
|
||||
| **서버 DTO는 `src/types/api`에만** (§4) | ✅ 해당없음 | 브릿지 타입은 서버 DTO가 아님 |
|
||||
| **주석·문서 한글 반말** (§0, §4) | ✅ | feature 문서와 새 주석을 한글로 유지 |
|
||||
| **편집 후 format, 완료 전 lint+test+build** (§4) | ⏳ 실행 시 | tasks와 quickstart에 검증 순서를 둠 |
|
||||
| **docs-lib 선참조** (§3) | ✅ | Tauri v2와 프론트 API 문서 박제 완료 |
|
||||
| **plan 집은 하나** (§1) | ✅ | `specs/004-tauri-shell/`만 사용 |
|
||||
| **버그는 systematic-debugging 먼저** (§1) | ✅ | 구현 중 실패가 나오면 적용 |
|
||||
|
||||
**게이트 결과**: 통과. 신규 프론트 의존성 1개는 Tauri 내부 프로토콜을 직접 흉내내지 않기 위한 정당한 편차다.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/004-tauri-shell/
|
||||
├── plan.md # 이 파일
|
||||
├── research.md # Phase 0 — 기술 결정 12건
|
||||
├── data-model.md # Phase 1 — 엔티티·상태
|
||||
├── quickstart.md # Phase 1 — Tauri 단독 실행·검증 시나리오
|
||||
├── contracts/
|
||||
│ └── transport-mapping.md # Phase 1 — 기존 계약 → Tauri·legacy·브라우저 수단 매핑
|
||||
├── checklists/
|
||||
│ └── requirements.md # spec 품질 체크리스트 (완료)
|
||||
└── tasks.md # Phase 2 — /speckit-tasks 가 만듦 (여기선 안 만듦)
|
||||
```
|
||||
|
||||
**기존 계약 문서는 여기 복사하지 않는다.** 진실원천은 그대로 아래 둘이고, `contracts/transport-mapping.md` 는 그걸 **가리키면서** 껍데기별 수단만 대응시킨다.
|
||||
- `specs/001-snippet-palette/contracts/bridge-messages.md`
|
||||
- `specs/002-capture-to-chat/contracts/bridge-capture.md`
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
2_frontend/ # Tauri와 브라우저가 공유. 빌드 한 벌
|
||||
├── src/lib/bridge/
|
||||
│ ├── transport.ts # [신규] 호스트 감지 + 전송수단 3종
|
||||
│ ├── webviewBridge.ts # [수정] transport 위로 얇게. export 시그니처 불변
|
||||
│ ├── snippetBridge.ts # [축소] 요청 표면만 남김
|
||||
│ ├── bridgeNavigate.ts # [수정] 리스너 부착만 교체, 분기 로직 그대로
|
||||
│ └── *.test.ts # [수정] transport mock 사용
|
||||
├── src/features/** # [무변경] 소비자 8곳 — SC-001
|
||||
└── docs-lib/ # Tauri 프론트 API 박제
|
||||
|
||||
3_windowsApp/ # [무변경] 참고용 legacy
|
||||
└── ...
|
||||
|
||||
4_rust_tauri/ # 기준 데스크톱 앱
|
||||
├── docs-lib/
|
||||
│ ├── README.md # [신규] 카탈로그
|
||||
│ └── tauri-v2.md # [신규] API 박제 — 첫 Rust 코드보다 먼저 (R8)
|
||||
├── package.json # tauri CLI 만
|
||||
└── src-tauri/
|
||||
├── Cargo.toml
|
||||
├── tauri.conf.json # 제품명 CodeAssist, devUrl → 2_frontend:15173, frontendDist → dist
|
||||
├── capabilities/default.json
|
||||
├── icons/ # 생성기 산출물 — 유지
|
||||
└── src/
|
||||
├── main.rs # 진입점
|
||||
├── lib.rs # 조립부 ← .NET App.xaml.cs 대응
|
||||
├── shell/ # 프로젝트 무관 재사용 층 ← .NET CodeAssist.Shell 대응
|
||||
│ ├── mod.rs
|
||||
│ ├── hotkey.rs # US2
|
||||
│ ├── tray.rs # US2
|
||||
│ ├── window.rs # US2
|
||||
│ ├── paste.rs # US3
|
||||
│ └── capture.rs # US4
|
||||
├── bridge/ # 계약 층
|
||||
│ ├── mod.rs # 푸시 타입 + emit
|
||||
│ └── commands.rs # #[tauri::command] 모음
|
||||
└── storage/
|
||||
└── snippets.rs # US5 — rusqlite, .NET 과 같은 DB 파일
|
||||
```
|
||||
|
||||
**Structure Decision**:
|
||||
|
||||
기준 데스크톱 앱은 `4_rust_tauri`다. 그 안에서는 기존 .NET 구현에서 검증된 책임 경계를 참고하되 Tauri 플러그인과 Rust 코드에 맞게 최소로 유지한다.
|
||||
|
||||
- `shell/` = CodeAssist를 모르는 Windows/Tauri 동작 층
|
||||
- `lib.rs` = 어느 단축키가 무엇을 하는지와 화면 이동 규칙을 모으는 조립부
|
||||
- `bridge/` = 기존 메시지 계약을 Tauri `invoke`/`emit`으로 나르는 층
|
||||
|
||||
통합 진입점(`shell::init(config)`)은 만들지 않는다. 세 모듈이 서로 독립이고 콜백도 달라서 감싸면 넘겨주기만 하는 코드가 늘어난다.
|
||||
|
||||
## Constitution Check — 설계 후 재확인 (Phase 1 완료 시점)
|
||||
|
||||
| 게이트 | 판정 | 설계로 확인된 것 |
|
||||
|---|---|---|
|
||||
| **재사용 우선** | ✅ | `bridgeNavigate.ts`의 분기·dispatch와 기존 메시지 계약을 그대로 쓰고, WebView2 요청 짝맞춤은 legacy adapter 안으로 이동함 |
|
||||
| **최소 diff / YAGNI** | ✅ | 프론트 신규 파일 1개. 구현 하나짜리 통합 진입점과 자동 계측 장치를 만들지 않음 |
|
||||
| **docs-lib 선참조** | ✅ | Tauri v2·프론트 invoke/listen 표면과 DPI 동작 확인 완료 |
|
||||
| **주석·문서 한글 반말** | ✅ | 산출물 전부 한글 |
|
||||
| **plan 집은 하나** | ✅ | `specs/004-tauri-shell/`만 사용하고 기존 계약 문서는 링크함 |
|
||||
| **완료 전 lint+test+build** | ⏳ | quickstart와 tasks에 프론트·Rust·Tauri release 검증을 둠 |
|
||||
|
||||
**설계가 드러낸 위험 1건**:
|
||||
|
||||
> `2_frontend`의 브릿지 테스트 3개가 `chrome.webview` mock에 의존한다. transport mock으로 바꾸면서 실제 Tauri 경로가 아니라 mock끼리만 맞는 상태가 생길 수 있다.
|
||||
>
|
||||
> **완화**: 자동 검사 뒤 브라우저와 실제 Tauri 앱을 각각 실행한다. 기존 WebView2 경로는 adapter 단위 테스트로 남기되 legacy 앱 수동 회귀를 완료 게이트로 두지 않는다.
|
||||
|
||||
**게이트 결과**: 통과. 아래 Complexity Tracking의 2건은 근거 있는 편차다.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
> Constitution Check 위반 중 정당화가 필요한 것만
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| 프론트 신규 의존성 `@tauri-apps/api` | `invoke`/`listen`은 Tauri 내부 프로토콜이라 공식 API를 쓰는 편이 작고 안전함 | 직접 구현은 버전마다 깨질 내부 표면을 소유하게 됨 |
|
||||
| `4_rust_tauri`의 미완성 스캐폴드 유지 | 아이콘·`build.rs`·capabilities는 생성기가 만든 유효한 산출물 | 통째 재생성해도 같은 파일만 다시 생김. 손으로 쓴 소스만 컴파일하며 재검증함 |
|
||||
@@ -0,0 +1,144 @@
|
||||
# Quickstart: Tauri 기준 앱 확인하기
|
||||
|
||||
**Date**: 2026-09-09 | **Plan**: [plan.md](./plan.md)
|
||||
|
||||
이 문서는 검증용 실행 안내다. 구현 방법은 `tasks.md`와 코드에 있다.
|
||||
|
||||
---
|
||||
|
||||
## 사전 준비
|
||||
|
||||
- Windows 11 + WebView2 런타임
|
||||
- Node 26 / npm 11
|
||||
- Rust stable MSVC 툴체인
|
||||
- VS C++ 도구 + Windows 11 SDK
|
||||
|
||||
```powershell
|
||||
rustc --version
|
||||
cargo --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## A. 자동 검사
|
||||
|
||||
```powershell
|
||||
cd 2_frontend
|
||||
npm install
|
||||
npm run format
|
||||
npm run lint
|
||||
npm run test
|
||||
npm run build
|
||||
|
||||
cd ..\4_rust_tauri
|
||||
npm install
|
||||
cargo test --manifest-path src-tauri\Cargo.toml
|
||||
```
|
||||
|
||||
**기대**: 전부 통과하고 오류·경고로 숨긴 실패가 없다.
|
||||
|
||||
---
|
||||
|
||||
## B. 브라우저 개발 화면
|
||||
|
||||
```powershell
|
||||
cd 2_frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
`http://localhost:15173`에서 확인:
|
||||
|
||||
| 확인 | 기대 |
|
||||
|---|---|
|
||||
| 챗봇 화면 | 정상 표시 |
|
||||
| 콘솔 오류 | 없음 |
|
||||
| 코드블록의 붙여넣기 버튼 | 안 보임 |
|
||||
| `/snippet` 직접 이동 | 화면은 뜨고 요청은 "데스크톱 전용" 오류 |
|
||||
|
||||
---
|
||||
|
||||
## C. Tauri 앱 기본 동작
|
||||
|
||||
```powershell
|
||||
cd 4_rust_tauri
|
||||
npm run tauri dev
|
||||
```
|
||||
|
||||
| 확인 | 기대 |
|
||||
|---|---|
|
||||
| 창 | 제품명 `CodeAssist`, 제목표시줄 없음 |
|
||||
| 화면 | `2_frontend`의 현재 화면 |
|
||||
| 트레이 | 열기 / 항상 위에 고정 / 종료 |
|
||||
| `Ctrl+Shift+7` | 스니펫 팔레트 |
|
||||
| `Ctrl+Shift+8` | 챗봇 토글 |
|
||||
| `Ctrl+Shift+9` | 캡쳐 시작 |
|
||||
| 두 번 실행 | 새 창 없이 기존 창 소환 |
|
||||
| 창 이동·크기·핀 뒤 재실행 | 마지막 상태 복원 |
|
||||
|
||||
단축키 등록 실패가 있으면 앱은 계속 돌고 실패한 조합을 알려야 한다.
|
||||
|
||||
---
|
||||
|
||||
## D. 붙여넣기
|
||||
|
||||
대상 앱마다 커서를 둔 뒤 `Ctrl+Shift+7` → 스니펫 선택 → Enter:
|
||||
|
||||
| 대상 | 기대 |
|
||||
|---|---|
|
||||
| 메모장 | 선택한 코드가 원문 그대로 붙음 |
|
||||
| VS Code | 선택한 코드가 원문 그대로 붙음 |
|
||||
| 창 전환이 느린 업무용 앱 | 엉뚱한 창이나 일부만 붙는 일이 없음 |
|
||||
|
||||
추가 확인:
|
||||
- 붙여넣기 대상 배지에 직전 앱 이름이 보임
|
||||
- 자동 붙여넣기가 실패해도 클립보드에 코드가 남음
|
||||
|
||||
느린 앱에서 어긋나면 research R10의 80ms 대기 값을 실제 환경에 맞게 조정한다.
|
||||
|
||||
---
|
||||
|
||||
## E. 화면 캡쳐
|
||||
|
||||
| 상황 | 확인 |
|
||||
|---|---|
|
||||
| 배율 100% 모니터 | `Ctrl+Shift+9` → 영역 드래그 → 고른 영역과 결과 일치 |
|
||||
| 배율 150% 모니터 | 같은 흐름에서 결과가 밀리거나 잘리지 않음 |
|
||||
| 두 모니터에 걸친 영역 | 고른 영역과 결과 일치 |
|
||||
| Esc | 첨부 없이 화면 복귀 |
|
||||
| 드래그 없이 클릭 | 조용히 취소 |
|
||||
|
||||
캡쳐한 그림을 원본 화면과 나란히 놓고 경계를 확인한다. 어긋나면 DPI 설정이 아니라 논리↔물리 좌표 변환을 본다.
|
||||
|
||||
---
|
||||
|
||||
## F. 기존 스니펫
|
||||
|
||||
1. `%LocalAppData%\CodeAssist\snippets.db`가 있는 상태에서 Tauri 팔레트를 연다.
|
||||
2. 기존 목록이 보이는지 확인한다.
|
||||
3. 스니펫 하나를 만들거나 수정하고 하나를 사용한다.
|
||||
4. 앱을 다시 켠다.
|
||||
5. 변경 내용·사용 횟수·최근 사용 순서가 유지되는지 확인한다.
|
||||
|
||||
기존 DB가 없으면 빈 목록으로 시작해 새 스니펫을 저장할 수 있어야 한다. 스키마와 저널 모드는 바꾸지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## G. 미구현 처리
|
||||
|
||||
구현 중 아직 연결하지 않은 요청은 빈 값으로 성공한 척하지 않고 한글 "아직 안 됨" 오류를 보여야 한다. 최종 완료 때는 이 오류를 내는 필수 기능이 남아 있으면 안 된다.
|
||||
|
||||
---
|
||||
|
||||
## H. release
|
||||
|
||||
```powershell
|
||||
cd 4_rust_tauri
|
||||
npm run tauri build
|
||||
```
|
||||
|
||||
**기대**:
|
||||
- release 설치물이 만들어짐
|
||||
- 설치물과 실행 창의 제품명이 `CodeAssist`
|
||||
- 빌드·실행·검증에 `3_windowsApp`이 필요하지 않음
|
||||
|
||||
마지막으로 `specs/004-tauri-shell/report.md`에 계획 대비 구현 결과와 검증 증거를 남긴다.
|
||||
@@ -0,0 +1,201 @@
|
||||
# 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 설정이 아니라 논리↔물리 좌표 변환을 본다.
|
||||
@@ -0,0 +1,229 @@
|
||||
# Feature Specification: Tauri 데스크톱 앱 전환
|
||||
|
||||
**Feature Branch**: `004-tauri-shell`
|
||||
|
||||
**Created**: 2026-08-11
|
||||
|
||||
**Status**: Draft
|
||||
|
||||
**Input**: User description: "이 프로젝트를 Rust + Tauri 버전으로 정의하고 수정을 시작한다. Tauri를 기본 데스크톱 앱으로 삼고, 기존 3_windowsApp(.NET WPF + WebView2)은 참고용 legacy로 남긴다."
|
||||
|
||||
## 이 기능이 뭔지 한 줄
|
||||
|
||||
기존 React 화면과 사용자 데이터를 그대로 살리면서, **Rust + Tauri를 CodeAssist의 기준 Windows 데스크톱 앱으로 완성한다.**
|
||||
|
||||
---
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - 프론트 한 벌로 Tauri와 브라우저에서 뜬다 (Priority: P1)
|
||||
|
||||
지금 화면(챗봇·스니펫 팔레트)은 데스크톱 껍데기가 넣어준 통로로 바깥과 얘기한다. 이 통로를 실행 환경에 맞게 고르면 화면 코드를 갈라 만들지 않고도 Tauri 앱과 브라우저 개발 화면을 함께 유지할 수 있다.
|
||||
|
||||
기존 WebView2 통로도 참고용 legacy 앱이 실행될 수 있을 만큼 남기되, 앞으로의 기본 실행·배포 기준은 Tauri다.
|
||||
|
||||
**Why this priority**: 화면과 Tauri 사이 통로가 먼저 열려야 창 제어·붙여넣기·캡쳐·스니펫을 차례로 연결할 수 있다. 화면을 한 벌로 유지하면 이후 기능도 같은 UI와 계약을 그대로 쓴다.
|
||||
|
||||
**Independent Test**: Tauri에서 현재 화면이 뜨고 데스크톱 동작이 Tauri 통로로 전달되는지 확인한다. 같은 화면을 브라우저로 열었을 때 데스크톱 기능만 빠지고 화면은 깨지지 않으면 통과다.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** Tauri 앱을 실행했고, **When** 챗봇이나 스니펫 화면을 열면, **Then** 기존 React 화면이 그대로 뜨고 데스크톱 동작은 Tauri로 전달된다
|
||||
2. **Given** 데스크톱 껍데기 없이 브라우저에서 화면만 열었고, **When** 데스크톱 전용 버튼이 있는 곳에 가면, **Then** 그 버튼은 안 보이거나 눌러도 아무 일 없고 화면은 안 깨진다
|
||||
3. **Given** 참고용 legacy 앱을 실행했고, **When** 기존 기능을 사용하면, **Then** 별도 화면 코드 없이 남겨둔 WebView2 통로를 사용한다
|
||||
4. **Given** 화면 코드를 쓰는 쪽(버튼·페이지 등), **When** 이번 변경 내역을 보면, **Then** 통로를 다루는 파일 바깥은 손댄 데가 없다
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Tauri 앱을 단축키와 트레이로 쓴다 (Priority: P2)
|
||||
|
||||
CodeAssist를 켜두면 트레이에 상주한다. 사용자는 기존에 익숙한 전역 단축키로 챗봇·스니펫·캡쳐를 바로 부르고, 챗봇 단축키를 다시 누르면 창을 숨긴다.
|
||||
|
||||
창을 옮기고 크기를 바꾸거나 항상 위에 고정한 상태는 앱을 다시 켜도 복원된다. 앱을 두 번 실행해도 창은 하나만 유지된다.
|
||||
|
||||
**Why this priority**: CodeAssist의 기본 사용 흐름은 다른 프로그램에서 일하다 단축키로 필요한 도구를 잠깐 부르는 것이다. 이 흐름이 먼저 안정돼야 붙여넣기와 캡쳐를 실제 업무처럼 검증할 수 있다.
|
||||
|
||||
**Independent Test**: Tauri 앱 하나를 켜고 `Ctrl+Shift+7/8/9`를 차례로 눌러 각 기능이 열리는지, 챗봇 토글·트레이·창 상태 복원·단일 실행이 동작하는지 확인한다.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** 앱이 실행 중이고, **When** `Ctrl+Shift+7`을 누르면, **Then** 스니펫 팔레트가 열린다
|
||||
2. **Given** 앱이 실행 중이고, **When** `Ctrl+Shift+8`을 누르면, **Then** 챗봇 창이 열리고 다시 누르면 숨는다
|
||||
3. **Given** 앱이 실행 중이고, **When** `Ctrl+Shift+9`를 누르면, **Then** 화면 캡쳐가 시작된다
|
||||
4. **Given** 창 위치·크기·항상 위 상태를 바꾸고 앱을 다시 켜면, **Then** 마지막 상태가 복원된다
|
||||
5. **Given** 앱이 이미 실행 중이고, **When** 앱을 한 번 더 실행하면, **Then** 새 창을 만들지 않고 기존 창을 불러낸다
|
||||
6. **Given** 단축키가 다른 프로그램에 이미 먹혀서 등록에 실패했을 때, **When** 앱을 켜면, **Then** 앱이 죽지 않고 트레이로 창을 부를 수 있으며 무엇이 실패했는지 알 수 있다
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - 코드가 직전 앱에 붙는다 (Priority: P3)
|
||||
|
||||
작업하다 단축키로 CodeAssist를 불러 코드를 고르면, 창이 닫히면서 **아까 쓰던 그 앱**에 코드가 그대로 들어간다.
|
||||
|
||||
붙이기가 실패해도 코드는 클립보드에 남아서 사용자가 직접 붙여넣을 수 있다.
|
||||
|
||||
**Why this priority**: 스니펫과 챗봇의 코드 결과를 다시 복사하러 다니지 않는 것이 데스크톱 앱의 핵심 가치다. 포커스 전환이 느린 프로그램에서도 틀린 창에 붙지 않아야 한다.
|
||||
|
||||
**Independent Test**: 메모장·코드 에디터·느리게 뜨는 업무용 프로그램을 대상으로 각각 여러 번 붙여넣어 보고 결과와 대상을 확인한다.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** 메모장에 커서를 두고, **When** 단축키로 창을 불러 코드를 고르면, **Then** 창이 닫히고 메모장에 그 코드가 들어간다
|
||||
2. **Given** 창 전환이 느린 프로그램이 대상일 때, **When** 같은 동작을 하면, **Then** 코드가 엉뚱한 곳에 들어가거나 절반만 들어가는 일이 없다
|
||||
3. **Given** 대상 앱이 붙여넣기를 거부하는 상황일 때, **Then** 코드는 클립보드에 남아 있어 사용자가 직접 붙일 수 있다
|
||||
4. **Given** 창을 불러낸 상태에서, **Then** 어느 앱에 붙게 되는지 화면에 표시된다
|
||||
|
||||
---
|
||||
|
||||
### User Story 4 - 화면 일부를 잘라 챗에 붙인다 (Priority: P4)
|
||||
|
||||
단축키를 누르면 화면이 어두워지고, 마우스로 원하는 영역을 끌어서 고르면, 챗 입력창에 그 그림이 첨부된 채로 창이 뜬다. Esc로 취소하면 아무 일도 없다.
|
||||
|
||||
모니터가 여러 대이고 각 모니터 배율이 서로 달라도, **고른 영역과 잘린 그림이 일치**한다.
|
||||
|
||||
**Why this priority**: 화면에 보이는 오류·코드·업무 화면을 바로 대화에 붙이면 설명을 다시 타이핑할 필요가 없다. 좌표가 어긋나면 다른 내용을 보내게 되므로 정확도가 우선이다.
|
||||
|
||||
**Independent Test**: 배율이 서로 다른 모니터 두 대를 놓고, 각 모니터와 두 모니터에 걸친 영역을 여러 번 캡쳐해 고른 영역과 결과가 맞는지 눈으로 대조한다.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** 배율 100% 모니터에서, **When** 영역을 끌어 고르면, **Then** 고른 영역과 똑같은 그림이 챗 입력창에 첨부된다
|
||||
2. **Given** 배율이 다른 두 번째 모니터에서 같은 동작을 하면, **Then** 결과가 어긋나지 않는다
|
||||
3. **Given** 영역 고르는 중에 Esc를 누르면, **Then** 아무것도 첨부되지 않고 화면이 원래대로 돌아간다
|
||||
4. **Given** 영역을 거의 안 끌고 클릭만 하면, **Then** 캡쳐가 안 되고 조용히 취소된다
|
||||
|
||||
---
|
||||
|
||||
### User Story 5 - 기존 스니펫을 그대로 쓴다 (Priority: P5)
|
||||
|
||||
Tauri 앱으로 스니펫 팔레트를 처음 열어도 **기존 CodeAssist에서 쓰던 스니펫 목록이 그대로** 나온다. 새로 만들거나 고친 내용과 자주 쓰는 순서는 앱을 껐다 켜도 유지된다.
|
||||
|
||||
**Why this priority**: 이미 쌓아둔 코드 조각을 잃지 않아야 Tauri 앱을 바로 실제 업무에 쓸 수 있다. 데이터 이전을 따로 요구하지 않는 것이 전환의 핵심 조건이다.
|
||||
|
||||
**Independent Test**: 기존 스니펫 데이터가 있는 상태에서 Tauri 팔레트를 열어 목록을 확인하고, 하나를 수정·사용한 뒤 앱을 다시 켜서 변경과 사용 순서가 유지되는지 확인한다.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** 기존 CodeAssist 스니펫이 저장돼 있고, **When** Tauri 앱에서 팔레트를 열면, **Then** 기존 목록이 그대로 보인다
|
||||
2. **Given** Tauri 앱에서 스니펫을 만들거나 고쳤고, **When** 앱을 다시 켜면, **Then** 변경 내용이 유지된다
|
||||
3. **Given** Tauri 앱에서 스니펫을 사용했고, **When** 목록을 다시 열면, **Then** 사용 횟수와 최근 사용 시각이 반영된 순서로 보인다
|
||||
|
||||
---
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- 전역 단축키를 다른 프로그램이 이미 쓰고 있으면? → 앱은 계속 실행하고, 등록하지 못한 조합을 사용자에게 알린다
|
||||
- 앱을 두 번 실행하면? → 두 번째 창을 새로 띄우지 않고 이미 떠 있는 창을 불러낸다
|
||||
- 창이 화면 밖에 저장돼 있다가 모니터 구성이 바뀌면? → 화면 안 보이는 자리로 복원하지 않는다
|
||||
- 아직 안 만든 기능을 화면이 부르면? → 조용히 성공한 척하지 않고 아직 안 됐다는 걸 사용자가 알 수 있게 한다
|
||||
- 창이 준비되기 전에 껍데기가 화면에 알림을 보내려 하면? → 앱이 죽지 않고, 화면이 준비된 뒤 필요한 현재 상태를 읽을 수 있다
|
||||
- 기존 스니펫 파일이 없으면? → 빈 목록으로 시작하고 새 스니펫을 정상 저장할 수 있다
|
||||
- 참고용 legacy 앱을 Tauri와 동시에 켜면? → 같은 전역 단축키가 충돌할 수 있으며 동시 사용은 지원 범위가 아니다
|
||||
|
||||
---
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
**통로 자동 감지 (US1)**
|
||||
|
||||
- **FR-001**: 화면은 실행 시점에 Tauri / 참고용 legacy / 껍데기 없음(브라우저) 중 어느 환경인지 스스로 판별해야 한다
|
||||
- **FR-002**: 판별 결과에 따라 바깥과 얘기하는 방식을 자동으로 고르되, 화면을 쓰는 쪽 코드는 실행 환경을 몰라도 되어야 한다
|
||||
- **FR-003**: 껍데기가 없으면 데스크톱 전용 동작은 조용히 넘어가고 화면은 정상 동작해야 한다
|
||||
- **FR-004**: Tauri 앱과 브라우저 개발 화면은 같은 화면 결과물 하나를 써야 한다
|
||||
- **FR-005**: Tauri가 화면에 보내는 알림(이동·붙여넣기 대상·캡쳐 결과)은 기존 계약 문서의 모양을 유지해야 한다
|
||||
|
||||
**데스크톱 앱 동작 (US2)**
|
||||
|
||||
- **FR-006**: Tauri 앱은 `Ctrl+Shift+7`=스니펫, `Ctrl+Shift+8`=챗봇, `Ctrl+Shift+9`=캡쳐 전역 단축키를 제공해야 한다
|
||||
- **FR-007**: 창 위치·크기·항상 위 고정 상태를 저장하고 다음 실행에 복원해야 한다
|
||||
- **FR-008**: 앱은 트레이에 상주하고, 열기 / 항상 위에 고정 / 종료를 제공해야 한다
|
||||
- **FR-009**: 앱 인스턴스가 하나만 뜨도록 막아야 한다
|
||||
- **FR-010**: 단축키 등록에 실패해도 앱은 계속 동작해야 하며, 무엇이 실패했는지 사용자가 알 수 있어야 한다
|
||||
- **FR-011**: 챗봇 단축키는 토글로 동작해야 한다 — 보고 있으면 숨기고, 아니면 마지막 챗봇 자리로 데려온다
|
||||
- **FR-012**: 창은 제목표시줄 없이 뜨고, 화면 안의 지정된 자리를 끌어서 창을 옮길 수 있어야 한다
|
||||
|
||||
**붙여넣기 (US3)**
|
||||
|
||||
- **FR-013**: 창을 부르기 직전에 쓰던 앱을 기억하고, 코드 선택 시 그 앱에 넣어야 한다
|
||||
- **FR-014**: 붙여넣은 내용은 클립보드에 남겨 실패 시 사용자가 직접 붙일 수 있어야 한다
|
||||
- **FR-015**: 어느 앱에 붙게 되는지 화면에 표시해야 한다
|
||||
|
||||
**화면 캡쳐 (US4)**
|
||||
|
||||
- **FR-016**: 단축키로 영역 선택 화면을 띄우고, 끌어서 고른 영역을 그림으로 만들어야 한다
|
||||
- **FR-017**: 모니터가 여러 대이고 배율이 서로 달라도 고른 영역과 결과가 일치해야 한다
|
||||
- **FR-018**: 취소(Esc·너무 작은 영역)는 아무 일도 일어나지 않아야 한다
|
||||
- **FR-019**: 캡쳐한 그림은 새 대화 입력창에 첨부로 들어가야 한다
|
||||
|
||||
**스니펫 (US5)**
|
||||
|
||||
- **FR-020**: 기존 CodeAssist 스니펫 데이터를 별도 이전 작업 없이 그대로 읽고 써야 한다
|
||||
- **FR-021**: 목록·생성·수정·삭제·사용기록이 기존 사용자 흐름과 동일하게 동작해야 한다
|
||||
- **FR-022**: 저장 실패·중복 등의 오류는 사용자가 읽을 수 있는 한글 메시지로 화면에 떠야 한다
|
||||
|
||||
**완성 기준 (전 구간 공통)**
|
||||
|
||||
- **FR-023**: 아직 안 만든 기능은 **조용히 성공한 척하지 않고**, 아직 안 됐다는 걸 사용자가 알 수 있게 해야 한다
|
||||
- **FR-024**: US1~US5를 모두 끝내 평소 업무 흐름 중간에 미구현으로 막히는 곳이 없어야 한다
|
||||
- **FR-025**: 기본 실행·배포·사용 문서는 Tauri 앱을 기준으로 해야 한다
|
||||
- **FR-026**: 기존 .NET 앱은 참고용 legacy로 남기되 새 기능·기본 배포·완료 검증 대상에서 제외해야 한다
|
||||
- **FR-027**: 설치물과 실행 창에는 실험판 표기 없이 제품명 `CodeAssist`를 보여줘야 한다
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **브릿지 메시지**: 화면과 Tauri가 주고받는 약속. 방향(화면→껍데기 / 껍데기→화면), 종류, 딸린 값. 기존 계약 문서가 원본
|
||||
- **창 상태**: 창의 위치·크기·항상 위 고정 여부. 다음 실행에 복원할 값
|
||||
- **스니펫**: 이름(고유)·설명·본문·분류 + 사용 횟수·마지막 사용 시각. 기존 CodeAssist 데이터를 이어서 사용
|
||||
- **붙여넣기 대상**: 창을 부르기 직전 쓰던 앱의 제목과 프로그램 이름. 소환 시점에 한 번 찍어 보관
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: 화면 결과물 한 벌이 Tauri 앱과 브라우저에서 모두 뜨며, 통로를 다루는 파일 바깥의 화면 코드 변경은 0줄이다
|
||||
- **SC-002**: 프론트 자동 검사와 데스크톱 순수 로직 검사가 전부 통과한다
|
||||
- **SC-003**: `Ctrl+Shift+7/8/9` 세 단축키가 각각 스니펫·챗봇·캡쳐만 실행하고, 반복 사용 중 무반응이나 오작동이 없다
|
||||
- **SC-004**: 사용자가 Tauri 앱으로 단축키 소환 → 코드 선택 → 직전 앱 붙여넣기 흐름을 처음부터 끝까지 막힘없이 마친다
|
||||
- **SC-005**: 사용자가 캡쳐 흐름을 끝까지 마치고, 배율이 다른 실제 모니터 각각과 두 모니터에 걸친 영역에서 고른 영역과 결과가 일치한다
|
||||
- **SC-006**: 기존 스니펫의 목록·수정·사용기록이 Tauri 앱 재실행 뒤에도 유지된다
|
||||
- **SC-007**: Tauri release 설치물이 만들어지고, 설치물과 실행 창에 제품명 `CodeAssist`가 표시된다
|
||||
- **SC-008**: 기본 실행·배포·검증 명령에서 기존 .NET 앱을 요구하지 않는다
|
||||
|
||||
## Assumptions
|
||||
|
||||
- **Tauri가 기준 앱이다** — 앞으로 기본 실행·배포·새 데스크톱 기능은 Tauri를 대상으로 한다
|
||||
- **기존 .NET 앱은 참고용 legacy다** — 코드는 남기지만 새 기능·배포·회귀 보장 대상은 아니다
|
||||
- **기존 계약 문서가 원본이다** — 브릿지 메시지 약속은 `specs/001-snippet-palette/contracts/bridge-messages.md`와 `specs/002-capture-to-chat/contracts/bridge-capture.md`를 이어서 쓴다
|
||||
- **표준 단축키는 Tauri가 쓴다** — `Ctrl+Shift+7/8/9`를 Tauri에 배정하며 legacy 앱과 동시 실행은 지원하지 않는다
|
||||
- **기존 스니펫 데이터는 보존한다** — `%LocalAppData%\CodeAssist\snippets.db`를 그대로 이어서 쓴다
|
||||
- **Rust 개발 환경은 준비됐다** — rustc/cargo와 Windows 빌드 도구 설치·링크 확인이 끝난 상태다
|
||||
- **미완성 스캐폴드는 이어서 쓴다** — 생성기 산출물은 유지하고, 손으로 쓴 Rust 소스는 컴파일하며 다시 확인한다
|
||||
- **이 PC 한 대, 사용자 한 명이 우선 대상이다** — 여러 기기·여러 사용자 대상 검증은 범위 밖이다
|
||||
- **자동 계측 장치는 만들지 않는다** — 실제 동작 검증에 필요한 최소 코드만 만든다
|
||||
- **US1~US5는 모두 필수다** — 비교 결과에 따라 중간에 멈추는 경로는 없다
|
||||
|
||||
## Dependencies
|
||||
|
||||
- 기존 브릿지 계약 문서 2개
|
||||
- 화면(`2_frontend`)의 통로 파일들
|
||||
- 기존 CodeAssist 스니펫 데이터 파일
|
||||
- 설치가 끝난 Rust·Windows 빌드 환경
|
||||
|
||||
## 정해진 것
|
||||
|
||||
### D1: Tauri를 기준 데스크톱 앱으로 삼는다 (2026-09-09)
|
||||
|
||||
- 기본 실행·배포·새 기능 대상은 `4_rust_tauri`다
|
||||
- 제품명에서 실험판 표기를 뺀다
|
||||
- Tauri가 기존 표준 단축키 `Ctrl+Shift+7/8/9`를 사용한다
|
||||
- `3_windowsApp`은 삭제하지 않고 참고용 legacy로 남긴다
|
||||
- legacy 앱과 Tauri 앱의 동시 실행은 지원하지 않는다
|
||||
|
||||
### D2: 기능을 끝까지 옮긴다 (2026-09-09)
|
||||
|
||||
- US1~US5를 모두 완료한다
|
||||
- A/B 비교·설치물 비교·판정 기록은 범위에서 뺀다
|
||||
- “비교하다 결론이 나면 중간에 멈춤” 규칙을 없앤다
|
||||
- 아직 안 만든 기능은 구현 중에만 분명한 오류로 알리고, 완료 시점에는 남아 있으면 안 된다
|
||||
@@ -0,0 +1,33 @@
|
||||
# Stage 1: 프론트 transport 전환
|
||||
|
||||
## 한 일
|
||||
|
||||
- `transport.ts` 한 곳에서 Tauri → WebView2 → 브라우저 순서로 실행 호스트를 고르게 함
|
||||
- Tauri `invoke`/`listen`, legacy `postMessage`/`message`, 브라우저 no-op/reject를 같은 API로 묶음
|
||||
- `route.changed` 예외와 `snippets.recordUse` 같은 camelCase command 이름 변환을 반영함
|
||||
- `webviewBridge.ts`·`snippetBridge.ts`·`bridgeNavigate.ts`의 기존 export는 유지하고 내부 통로만 교체함
|
||||
- React StrictMode 재초기화와 테스트별 새 WebView2 인스턴스에서 listener가 중복되거나 빠지지 않게 함
|
||||
- 전체 lint를 막던 기존 파일 업로드 접근성 문제와 skill-mapping의 `any` 타입을 최소 수정함
|
||||
|
||||
## 검증 결과
|
||||
|
||||
- transport·navigate·snippet API 대상 검사: 4파일 20개 통과
|
||||
- listener 재초기화 회귀 검사: 3파일 11개 통과
|
||||
- `npm run format`: 통과
|
||||
- `npm run lint`: 오류 0개, 기존 경고 9개
|
||||
- `npm run test`: 43파일 244개 통과
|
||||
- `npm run build`: TypeScript와 Vite production build 통과
|
||||
- 브라우저 실제 확인:
|
||||
- `/snap` 챗봇 화면과 `/snippet` 화면 렌더 확인
|
||||
- `isWebView()`가 `false`라 데스크톱 전용 붙여넣기 버튼 조건이 꺼짐
|
||||
- `snippets.list` 요청이 `desktop-only: 데스크톱 전용 기능임`으로 reject됨
|
||||
- page error와 unhandled rejection 없음
|
||||
- Tauri 실제 확인:
|
||||
- `CodeAssist` native 창과 React 로그인 화면 렌더 확인
|
||||
- CDP로 붙은 실제 WebView에서 host가 `tauri`임을 확인하고 `transport.request("window.hide")`로 native 창이 숨는 것까지 확인
|
||||
- native hotkey가 보낸 `bridge` 이벤트를 실제 listen해 `{ type: "navigate", path: "/snippet" }` 수신 확인
|
||||
|
||||
## 다음
|
||||
|
||||
- Phase 4 T026부터 표준 단축키·트레이·창 상태·단일 인스턴스를 Tauri 기준으로 확정함
|
||||
- 현재 Rust 쪽의 비교용 `Ctrl+Alt+7/8/9`와 오래된 v0 문구를 `Ctrl+Shift+7/8/9` 기준으로 바로 걷어냄
|
||||
@@ -0,0 +1,27 @@
|
||||
# Stage 2: Tauri shell 기준 확정
|
||||
|
||||
## 한 일
|
||||
|
||||
- 표준 전역 단축키를 `Ctrl+Shift+7/8/9`로 바꿈
|
||||
- 챗봇 토글과 마지막 챗봇 route 갱신 규칙을 순수 로직으로 분리함
|
||||
- window-state는 `SIZE | POSITION`만 저장하게 제한함
|
||||
- 단일 인스턴스, 트레이, 핀 파일 저장·복원, 창 show/hide/focus/drag 배선을 다시 확인함
|
||||
- `window_hide`·`window_drag`·`report_route`를 실제 shell 함수에 연결함
|
||||
- 아직 다음 단계인 붙여넣기·스니펫 command는 빈 성공값 대신 `아직 안 됨`으로 reject하게 정리함
|
||||
|
||||
## 검증 결과
|
||||
|
||||
- `cargo fmt --check`·`cargo clippy -- -D warnings`: 통과
|
||||
- `cargo test`: 챗봇 토글·마지막 route 규칙 2개 통과
|
||||
- `CodeAssist` 프레임리스 native 창과 React 화면 렌더 확인
|
||||
- 헤더를 끌어 창 위치가 `(260, 110)`에서 `(380, 192)`로 이동하는 걸 확인
|
||||
- `Ctrl+Shift+7`: `/snippet` 이동 신호 확인
|
||||
- `Ctrl+Shift+8`: 보이는 챗봇 창 숨김과 숨은 창 재소환 확인
|
||||
- `Ctrl+Shift+9`: 다른 route 동작 없이 capture 분기만 실행함. 실제 캡처는 US4에서 연결함
|
||||
- 실행 파일을 다시 실행했을 때 새 창 대신 기존 창이 바로 소환되고 두 번째 프로세스는 종료됨
|
||||
- 창을 `(100, 120)`, `820×620`으로 바꾸고 핀 파일을 `true`로 둔 뒤 재실행해 위치·크기 복원과 Win32 `topmost=true`를 확인함. 검증 뒤 원래 `(420, 200)`, `976×689`, 핀 `false`로 돌려둠
|
||||
- 트레이 아이콘 생성과 열기/핀/종료 메뉴 배선은 실제 빌드에서 초기화됨. 메뉴 항목 클릭 자체는 자동 UI 트리로 구분되지 않아 코드·실행 초기화까지만 확인함
|
||||
|
||||
## 다음
|
||||
|
||||
- Phase 5 T041부터 기존 `%LocalAppData%\CodeAssist\snippets.db`를 Rust에서 그대로 열어 스니펫 CRUD와 붙여넣기를 완성함
|
||||
@@ -0,0 +1,18 @@
|
||||
# Stage 4: Tauri 화면 캡쳐 구현
|
||||
|
||||
## 한 일
|
||||
|
||||
- `Ctrl+Shift+9` 스텁을 Win32 네이티브 캡쳐 오버레이에 연결함.
|
||||
- 전체 가상 화면을 반투명·항상위 창으로 덮고, 드래그 영역을 `BitBlt`로 캡쳐해 PNG data URL로 만듦.
|
||||
- 캡쳐 뒤 `/snap/new`로 이동하고 기존 `capture.image` bridge 계약으로 Composer에 첨부함.
|
||||
- Esc·포커스 이탈·4px 미만 선택은 조용히 취소하고, 중복 핫키는 무시함.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
- `cargo test --locked`: 11 passed, 1 ignored.
|
||||
- 역방향 드래그·음수 좌표 모니터·작은 선택 취소 단위 테스트 통과.
|
||||
- 실제 픽셀 일치, 100%/150% 혼합 배율, 모니터 사이 드래그는 T054 수동 확인이 남음.
|
||||
|
||||
## 다음
|
||||
|
||||
- quickstart D로 실기기 캡쳐·Esc·작은 클릭·혼합 DPI를 확인함.
|
||||
@@ -0,0 +1,21 @@
|
||||
# 단계 5: 스니펫 저장
|
||||
|
||||
## 한 일
|
||||
|
||||
- 기존 `%LocalAppData%\CodeAssist\snippets.db` 스키마 그대로 생성·수정·삭제·사용기록 저장을 연결함.
|
||||
- 이름 정규화, 중복 이름, 빈 필수값, 없는 스니펫 수정 오류를 legacy와 같은 한글 메시지로 맞춤.
|
||||
- 삭제는 스니펫과 usage를 transaction 하나로 함께 지움.
|
||||
- DB가 없으면 기존 snippets/usage 스키마로 새로 만들고 폴더도 준비함.
|
||||
|
||||
## 검증
|
||||
|
||||
- RED: 저장 함수가 없는 상태에서 신규 Rust 테스트가 컴파일 오류 12개로 실패함.
|
||||
- GREEN: 임시 메모리·파일 DB에서 생성→재오픈 조회→수정→사용횟수 2회 증가→삭제와 usage 정리 통과.
|
||||
- Rust: fmt, clippy `-D warnings`, 전체 검사 9개 통과·로컬 실DB 읽기 검사 1개 제외.
|
||||
- 프론트: 50파일 273개 검사, lint 오류 0개·기존 경고 9개, build 통과.
|
||||
- 실행 중 Tauri 창의 스니펫 route 진입은 확인함. 자동조작 도구가 WebView 입력 focus를 못 잡아 실제 UI 생성·수정·삭제 한 바퀴는 확인하지 못함.
|
||||
- 생성 성공 시 입력창을 바로 닫지 않고 성공 toast를 1초 보여준 뒤 검색어·분류·선택을 비우고 기본 검색 화면으로 돌아가게 함. 프론트 전체 50파일 274개 검사와 build 통과.
|
||||
|
||||
## 다음
|
||||
|
||||
- T062 실제 UI에서 테스트 항목 하나를 생성→수정→삭제하고 재실행 뒤 상태를 확인함.
|
||||
@@ -0,0 +1,31 @@
|
||||
# main 기준 Tauri 작업 통합
|
||||
|
||||
## 통합한 작업
|
||||
|
||||
- main의 클린 블루·`/snap` 시작·스니펫 SQLite 읽기·Enter 복사와 canonical의 공통 transport·직전 앱 붙여넣기를 함께 보존.
|
||||
- `rusqlite`와 `windows` 의존성을 모두 유지하고 Rust command에 두 구현을 연결.
|
||||
- Tauri 제품명은 `CodeAssist`, identifier는 canonical의 `com.codeassist.app` 사용. 기존 `com.codeassist.tauri`의 창 위치·핀 설정을 자동 이관하는 작업은 포함하지 않음. 스니펫 DB 경로는 기존 `%LocalAppData%\CodeAssist\snippets.db` 유지.
|
||||
- UI의 호스트 판별을 `transport.hostKind()`로 통일하고 main의 오류 토스트를 공통 transport로 이관.
|
||||
- 이전 양쪽 이력과 작업로그를 보존. Tauri 기준 명세를 따르며 A/B 실험용 절차는 제거.
|
||||
|
||||
## 검증
|
||||
|
||||
- 프론트 전체 45파일·255개 테스트 통과. `npm run lint` 오류 0개·기존 경고 9개, `npm run build` 통과.
|
||||
- `cargo fmt --check`, `cargo clippy --locked -- -D warnings` 통과. `cargo test --locked` 4개 통과·로컬 DB 검사 1개는 기본 제외.
|
||||
- 제외된 로컬 DB 읽기 검사도 별도로 실행해 기존 스니펫 137개 조회 통과. 읽기 전용으로 열어 원본 데이터는 수정하지 않음.
|
||||
- 실제 대상 앱에 붙여넣기·전역 단축키·트레이 조작은 이번 통합에서 재검증하지 않음.
|
||||
|
||||
## 남은 작업
|
||||
|
||||
- Phase 5의 실제 대상 앱 붙여넣기 검증, Phase 6 캡처, Phase 7 스니펫 쓰기 등은 미완료 상태를 유지.
|
||||
- 통합 시점의 Raycast UI는 초안이었음. 이후 승인된 단계별 스니펫 개선 결과는 아래와 같음.
|
||||
|
||||
## 후속: 단계별 스니펫 런처 (2026-09-11)
|
||||
|
||||
- 검색창만 시작 → 검색 결과 → 직접 선택 시 코드 프리뷰. Enter 원문 복사와 기존 DB 읽기 유지.
|
||||
- 실제 Tauri 콘텐츠 크기 640×84 → 640×440 → 960×600. DPI·작업 영역과 Windows 바깥 테두리를 반영.
|
||||
- 이전 배치를 메모리에 보관하고 챗봇 복귀·종료 시 복원. 최대화 전 일반 배치도 유지.
|
||||
- 축소 상태에서 native close 후 작은 크기가 저장되는 문제를 재현. 복원 직후 window-state 캐시에 실측값을 저장하도록 고쳐 재시작 시 960×680 확인.
|
||||
- 실제 앱에서 SELECT 검색 결과 7개·화살표 선택·프리뷰·Esc 축소·빈 결과·편집/삭제 확인 취소·재소환·우하단 경계 확인. 원본 DB는 변경하지 않음.
|
||||
- 프론트 46파일·259검사, lint 오류 0·기존 경고 9, build 통과. Rust 6검사 통과·로컬 DB 전용 1검사 기본 제외, fmt와 clippy `-D warnings` 통과.
|
||||
- 생성·수정·삭제·사용기록의 Rust 미구현은 그대로 남김. 이번 개선 완료와 전체 스니펫 기능 완료는 구분함.
|
||||
@@ -0,0 +1,297 @@
|
||||
# Tasks: Tauri 기준 데스크톱 앱 전환
|
||||
|
||||
**Input**: `specs/004-tauri-shell/` 의 설계 문서 (spec.md, plan.md, research.md, data-model.md, contracts/, quickstart.md)
|
||||
|
||||
**Tests**: 포함한다. CLAUDE.md §1 이 TDD(RED→GREEN)를 파이프라인에 박아뒀고, research R12 가 "자동으로 될 것만 자동으로" 기준으로 대상을 이미 갈라놨다.
|
||||
|
||||
**Organization**: 유저스토리 우선순위대로 쌓는다. US1~US5는 전부 필수이며 중간 중단 경로는 없다.
|
||||
|
||||
## Format: `[ID] [P?] [Story] 설명`
|
||||
|
||||
- **[P]**: 병렬 가능 (파일 서로 안 겹치고 선행 task 에 안 물림)
|
||||
- **[Story]**: 어느 유저스토리 소속인지 (US1~US5)
|
||||
- 파일 경로는 repo 루트 기준
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Setup (환경·선행 문서)
|
||||
|
||||
**목적**: 첫 Rust 코드보다 먼저 있어야 하는 것들. US1 은 T004 만 있으면 시작 가능.
|
||||
|
||||
- [x] T001 rust 툴체인 설치·확인 — `rustup` + MSVC 타깃(`x86_64-pc-windows-msvc`) + VS Build Tools/Windows SDK. `cargo --version`·`rustc --version`·`cl.exe` 존재가 통과 조건 (research R1, quickstart §사전준비)
|
||||
→ **2026-08-15 완료.** rustc/cargo 1.97.1, 기본 툴체인 `stable-x86_64-pc-windows-msvc`. 별도 Build Tools 를 새로 받지 않고 **이미 있던 VS 2026 Community 에 컴포넌트만 추가**(`VC.Tools.x86.x64` + `Windows11SDK.26100`) — MSVC 14.51.36231, SDK 10.0.26100.0. `rustc` 로 실제 링크까지 통과 확인
|
||||
- [x] T002 [P] `4_rust_tauri/docs-lib/README.md` 신규 — 카탈로그 (CLAUDE.md §3 양식)
|
||||
- [x] T003 [P] `4_rust_tauri/docs-lib/tauri-v2.md` 신규 — 실제 쓸 API 표면 박제: 플러그인 4종 init(`global-shortcut`/`single-instance`/`window-state`/`opener`), `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 권한 이름. **R11 의 미확정(Tauri v2 에서 PerMonitorV2 DPI 인지를 어디서 켜는가)도 여기서 확인해 적는다** (research R3)
|
||||
→ **2026-08-15 완료.** 공식 가이드 예제를 안 믿고 `cargo fetch` 로 받은 **실제 crate 소스**에서 뽑음(가이드 안에 2인자/3인자 예제가 둘 다 있어서 근거로 못 씀). 버전 박제: tauri 2.11.5 / global-shortcut 2.3.2 / single-instance 2.4.3 / window-state 2.4.1 / tao 0.35.3. 원문은 `tauri-v2-llms-full.md`(2.4MB) 로 같이 받아둠
|
||||
- [x] T004 [P] `2_frontend/docs-lib/tauri-api.md` 신규 — 프론트가 쓸 `invoke`/`listen` 표면만 박제 (+ `2_frontend/docs-lib/README.md` 카탈로그에 한 줄 추가)
|
||||
→ **2026-08-15 완료.** `node_modules/@tauri-apps/api/` 의 실제 `.d.ts`·`.js` 에서 뽑음 (2.11.1)
|
||||
- [x] T005 `2_frontend/package.json` 에 `@tauri-apps/api` v2 추가 후 `npm install` — 이 기능의 유일한 신규 프론트 의존성 (plan Complexity Tracking 1번)
|
||||
→ **2026-08-15 완료.** `^2.11.1`
|
||||
|
||||
**Checkpoint**: T003 없이 Rust 코드를 쓰지 않는다. T005 없이 US1 을 시작하지 않는다.
|
||||
|
||||
### 📌 Phase 1 이 뒤 단계에 남긴 것 (docs-lib 만들며 새로 확인된 사실)
|
||||
|
||||
문서 만들다 **계획을 바꿔야 하는 것 3건**이 나왔다. 해당 task 본문에도 반영해뒀다.
|
||||
|
||||
| 무엇 | 어디에 영향 | 요약 |
|
||||
|---|---|---|
|
||||
| **DPI 인지는 이미 켜져 있다** | T049 | `tao` 가 이벤트 루프 만들 때 `SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2)` 를 무조건 부름. R11 의 "매니페스트 넣기"는 **할 일이 없어졌다** — 확인만 하고 넘어감 |
|
||||
| **호스트 판별식을 바꿔야 한다** | T012·T015 | `"__TAURI_INTERNALS__" in window` 는 틀린다. 공식 `clearMocks()` 가 **속성만 지우고 빈 객체를 남겨서** 정리 후에도 계속 true → 브라우저 모드 테스트가 tauri 로 오판됨. `typeof window.__TAURI_INTERNALS__?.invoke === "function"` 으로 잰다 |
|
||||
| **window-state 기본 플래그가 함정** | T030 | `StateFlags` 기본값이 `all()` 이라 `VISIBLE`·`DECORATIONS` 까지 저장·복원한다. 트레이 상주 + 프레임리스 앱엔 안 맞음 → `SIZE \| POSITION` 만 켠다 |
|
||||
|
||||
**공식 테스트 mock 이 있다는 것도 알아냈다** — `@tauri-apps/api/mocks` 의 `mockIPC`/`clearMocks`, `shouldMockEvents: true` 면 `listen`/`emit` 까지 mock 됨. T012~T014 는 이걸 쓴다 (직접 흉내낼 필요 없음).
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Tauri 빌드 베이스라인)
|
||||
|
||||
**목적**: `4_rust_tauri`가 컴파일되고 현재 `2_frontend` 화면을 띄우는 기준 앱 베이스라인 만들기.
|
||||
|
||||
**범위 주의**: 여기서 제품명·identifier·빌드 경로를 기준 앱 값으로 확정한다. 손으로 쓴 Rust와 설정은 컴파일 검증 0줄이므로 전부 다시 본다.
|
||||
|
||||
**⚠️ 전제**: 손으로 쓴 `src-tauri/src/**/*.rs`·`Cargo.toml`·`tauri.conf.json` 은 **컴파일 검증 0줄**이다. "있으니 맞겠지"로 넘기지 말고 전부 다시 본다 (research R2).
|
||||
|
||||
- [x] T006 `4_rust_tauri/src-tauri/src/main.rs`의 lib 이름이 `codeassist_tauri_lib::run()`으로 `[lib] name`과 일치하는지 확인
|
||||
- [x] T007 `4_rust_tauri/src-tauri/Cargo.toml` 재검증 — Tauri 2의 `tray-icon`·`image-png`, global-shortcut·single-instance·window-state, serde·serde_json만 포함. `rusqlite`·`windows`는 필요한 스토리까지 미룸
|
||||
- [x] T008 `4_rust_tauri/src-tauri/tauri.conf.json` — `productName`/창 제목=`CodeAssist`, `identifier`=`com.codeassist.app`, devUrl·frontendDist·프레임리스 설정 확인
|
||||
- [x] T009 `4_rust_tauri/src-tauri/capabilities/default.json` — 프론트가 우리 command만 invoke하므로 `core:default`면 listen/emit까지 충분함을 docs-lib 기준으로 확인
|
||||
- [x] T010 `4_rust_tauri/package.json` — Tauri CLI 스크립트만 있고 프론트 dev/build는 `tauri.conf.json`이 `2_frontend`에서 실행함을 확인
|
||||
- [x] T011 `cd 4_rust_tauri && npm run tauri dev` 첫 통과 — Rust dev 빌드 완료, 제목 `CodeAssist` 창에서 `2_frontend` 로그인 화면 렌더 확인
|
||||
|
||||
**Checkpoint**: 여기까지 되면 실제 Tauri bridge와 데스크톱 기능을 연결할 수 있다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - 프론트 한 벌로 Tauri와 브라우저에서 뜬다 (Priority: P1) 🎯 MVP
|
||||
|
||||
**Goal**: `2_frontend`가 실행 시점에 Tauri·legacy·브라우저를 판별해 맞는 통로를 고른다. 화면 소비자는 바꾸지 않는다.
|
||||
|
||||
**Independent Test**: Tauri 앱에서 실제 invoke/listen 왕복이 되고, 브라우저에서는 화면이 깨지지 않으며, legacy adapter 자동 검사가 통과하면 완료.
|
||||
|
||||
**위험**: 기존 브릿지 테스트의 WebView2 mock을 transport mock으로 바꾸면서 실제 Tauri 경로가 아니라 mock끼리만 맞을 수 있다. 자동 검사 뒤 실제 Tauri 앱을 반드시 실행한다.
|
||||
|
||||
### Tests for User Story 1 ⚠️ 먼저 쓰고 RED 확인
|
||||
|
||||
> transport는 사용할 때 호스트를 판별한다. 테스트 전용 reset API는 만들지 않는다. Windows Tauri도 WebView2를 쓸 수 있으므로 판별 순서는 `tauri` → `webview2` → `browser`다.
|
||||
|
||||
- [x] T012 [P] [US1] `2_frontend/src/lib/bridge/transport.test.ts` 신규 — 호스트 3종 판별(`tauri`→`webview2`→`browser`), 브라우저 단방향 no-op·응답 요청 즉시 reject를 먼저 작성하고 RED 확인. Tauri는 `@tauri-apps/api/mocks`의 `mockIPC`/`clearMocks` 사용
|
||||
- [x] T013 [P] [US1] `2_frontend/src/lib/bridge/bridgeNavigate.test.ts`의 `chrome.webview` mock을 transport 경유 mock으로 교체. 기존 분기·CustomEvent 기대값은 유지
|
||||
- [x] T014 [P] [US1] `2_frontend/src/lib/bridge/snippetBridge.test.ts`와 `2_frontend/src/features/snippets/api/snippets.api.test.ts`의 mock을 transport 경유로 교체. 소비자 소스는 무변경
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [x] T015 [US1] `2_frontend/src/lib/bridge/transport.ts` 신규 — `HostKind` 판별 + 전송수단 3종(tauri: `invoke`+`listen`, webview2: `postMessage`+`addEventListener`, browser: no-op·reject). Tauri 판별식은 `typeof window.__TAURI_INTERNALS__?.invoke === "function"`이며 호출 시점에 판별
|
||||
- [x] T016 [US1] `2_frontend/src/lib/bridge/webviewBridge.ts` 를 transport 위로 얇게 — `isWebView`/`hideWindow`/`pasteToApp`/`reportRoute`/`startWindowDrag` **export 시그니처 불변**, 속만 transport 호출로 교체 (`isWebView` 는 "데스크톱 껍데기 안인가"로 의미 확장)
|
||||
- [x] T017 [US1] `2_frontend/src/lib/bridge/snippetBridge.ts` 축소 — `reqSeq`/`pending` Map/`ensureListener` 를 **지우지 말고 `transport.ts` 의 webview2 구현 안으로 옮긴다**(기존 껍데기 경로엔 그대로 필요). 여기엔 `request(type, payload)` 만 남김 (research R6)
|
||||
- [x] T018 [US1] `2_frontend/src/lib/bridge/bridgeNavigate.ts` — 리스너 부착만 transport 로 교체. `paste.target`/`capture.image`/`navigate` 분기와 `CustomEvent` dispatch **본문은 한 줄도 안 바꾼다** (research R5 — 이게 diff 를 가장 크게 줄이는 한 수).
|
||||
**transport 가 흡수해야 하는 차이 2개** (`docs-lib/tauri-api.md` §2): ① Tauri 는 페이로드가 `Event<T>` 로 한 겹 싸여 오므로 **`e.payload` 를 벗겨서** 넘겨야 분기 본문이 그대로 산다 ② `listen` 은 `Promise<UnlistenFn>` 이라 **async** — `initBridgeNavigate()` 를 async 로 바꾸면 호출부가 바뀌어 SC-001 위반이므로 transport 안에서 Promise 를 삼키고 밖은 동기 유지. 붙기 전 도착분은 기존 `lastPasteTarget` 스냅샷·`pendingCaptureImage` read-once 가 이미 막아줌
|
||||
- [x] T019 [US1] `transport.ts` 에 타입명→커맨드명 매핑 반영 — `snippets.recordUse`→`snippets_record_use` 기계 변환 + **예외 1건** `route.changed`→`report_route` (contracts/transport-mapping.md §1)
|
||||
- [x] T020 [US1] 미구현 응답 규칙 — tauri 경로에서 `invoke` reject 는 한글 메시지 그대로 `Error` 로 올라가 react-query `onError`→sonner 로 흐르는지 확인. 빈 값(`[]`,`null`)으로 성공을 흉내내지 않는다 (FR-023, transport-mapping §3)
|
||||
|
||||
### 검증 for User Story 1
|
||||
|
||||
- [x] T021 [US1] 소비자 diff 0줄 확인 — `2_frontend/src/lib/bridge/`와 관련 테스트 바깥의 화면 소비자 소스에 변경이 없는지 확인
|
||||
- [x] T022 [US1] 자동 검사 — `cd 2_frontend && npm run format && npm run lint && npm run test && npm run build` 전부 통과
|
||||
- [x] T023 [US1] 브라우저 — `npm run dev` 후 챗봇 화면, 콘솔 오류 0, 데스크톱 전용 붙여넣기 버튼 숨김, `/snippet`의 "데스크톱 전용" 오류 확인
|
||||
- [x] T024 [US1] 실제 Tauri 앱 — `npm run tauri dev`에서 화면→Rust `invoke`와 Rust→화면 `bridge` 이벤트를 각각 한 번 이상 실제 동작으로 확인
|
||||
- [x] T025 [US1] `specs/004-tauri-shell/stage-1.md` 기록 — 한 일 / 검증 결과 / 다음
|
||||
|
||||
**Checkpoint**: Tauri와 브라우저가 같은 화면 한 벌을 쓰고 실제 bridge 왕복이 된다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - Tauri 앱을 단축키와 트레이로 쓴다 (Priority: P2)
|
||||
|
||||
**Goal**: Tauri 앱이 표준 단축키·트레이·창 상태 복원·단일 인스턴스를 제공한다.
|
||||
|
||||
**Independent Test**: Tauri 앱 하나에서 `Ctrl+Shift+7/8/9`, 챗봇 토글, 트레이 메뉴, 창 상태 복원, 두 번 실행을 확인한다.
|
||||
|
||||
**의존**: Phase 2 + Phase 3. 챗봇 토글이 `route.changed` 보고에 물린다.
|
||||
|
||||
### Tests for User Story 2
|
||||
|
||||
- [x] T026 [P] [US2] `4_rust_tauri/src-tauri/src/lib.rs` 의 챗봇 토글 규칙을 순수 함수로 뽑고 `cargo test` 작성 — "창 보이는 중 AND current 가 `/snippet` 아님 → 숨김, 그 외 → 띄우고 `lastSnap` 으로" + `lastSnap` 갱신 규칙(`/snippet` 으로 시작 안 하면 같이 갱신) (data-model §4). 창·트레이·핫키는 자동 테스트 대상 아님 (research R12)
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [x] T027 [P] [US2] `4_rust_tauri/src-tauri/src/shell/window.rs` 재검증 — show/hide/set_focus/set_always_on_top/start_dragging/is_visible. CodeAssist 를 하나도 모르는 층으로 유지 (plan Structure Decision)
|
||||
- [x] T028 [P] [US2] `4_rust_tauri/src-tauri/src/shell/hotkey.rs` 재검증 — `Ctrl+Shift+7/8/9` 등록. 등록 실패해도 앱을 죽이지 않고 어느 조합이 실패했는지 알림
|
||||
- [x] T029 [P] [US2] `4_rust_tauri/src-tauri/src/shell/tray.rs` 재검증 — 트레이 상주 + 열기/항상 위에 고정/종료
|
||||
- [x] T030 [US2] `4_rust_tauri/src-tauri/src/lib.rs`에 window-state·single-instance 배선 — `.with_state_flags(StateFlags::SIZE | StateFlags::POSITION)`만 사용. 저장 경로는 `%LocalAppData%\com.codeassist.app\`
|
||||
- [x] T031 [US2] 핀(항상 위) 상태만 별도 저장/복원 — `app_local_data_dir()` 아래 단일 파일. window-state 플러그인이 always-on-top 은 안 봄 (research R8)
|
||||
- [x] T032 [US2] `4_rust_tauri/src-tauri/src/lib.rs` 조립 — 어느 핫키가 무엇을 하는지(7=스니펫, 8=챗봇 토글, 9=캡쳐)와 `Routes{current,lastSnap}` 상태를 **여기에만** 모은다. `shell::init(config)` 통합 진입점은 **만들지 않는다** (plan Structure Decision — 구현 하나짜리 추상화 금지)
|
||||
- [x] T033 [US2] `4_rust_tauri/src-tauri/src/bridge/commands.rs` — `window_hide`/`window_drag`/`report_route` 구현 (transport-mapping §1)
|
||||
- [x] T034 [US2] `4_rust_tauri/src-tauri/src/bridge/mod.rs` — 푸시를 **단일 이벤트 `"bridge"` 에 `{type, ...}` 그대로** 실어 `app.emit`. 우선 `navigate` 부터 (research R5, transport-mapping §2)
|
||||
- [x] T035 [US2] 아직 안 만든 커맨드는 한글 "아직 안 됨" 으로 **reject** — 빈 값으로 성공 흉내 금지 (FR-023). 이 시점엔 `paste_code`·`snippets_*` 가 대상
|
||||
- [x] T036 [US2] 창은 제목표시줄 없이 뜨고 헤더 끌기로 이동 — 프론트의 `window.drag` 가 `start_dragging` 까지 도달하는지 (FR-012)
|
||||
|
||||
### 검증 for User Story 2
|
||||
|
||||
- [x] T037 [US2] quickstart C 기본 앱 — `CodeAssist` 프레임리스 창, 현재 챗봇 화면, 트레이, 두 번 실행 시 기존 창 소환 확인
|
||||
- [x] T038 [US2] quickstart C 표준 단축키 — `Ctrl+Shift+7`=스니펫, `Ctrl+Shift+8`=챗봇 토글, `Ctrl+Shift+9`=캡쳐만 반응하는지 확인
|
||||
- [x] T039 [US2] quickstart C 창 상태 — 창 위치·크기·핀을 바꾸고 재실행해 복원되는지 확인
|
||||
- [x] T040 [US2] `specs/004-tauri-shell/stage-2.md` 기록 — 한 일 / 검증 결과 / 다음
|
||||
|
||||
**Checkpoint**: CodeAssist를 Tauri 기준 앱으로 상시 켜두고 단축키로 부를 수 있다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - 코드가 직전 앱에 붙는다 (Priority: P3)
|
||||
|
||||
**Goal**: 단축키로 창 불러 코드 고르면 **아까 쓰던 그 앱**에 들어간다.
|
||||
|
||||
**Independent Test**: 메모장·VS Code·창전환 느린 앱 각각에 여러 번 붙여넣어 성공률 세기.
|
||||
|
||||
- [ ] T041 [US3] `4_rust_tauri/src-tauri/Cargo.toml` 에 `windows` crate 추가 (Win32 직접 호출 — 플러그인 안 씀, research R10)
|
||||
- [ ] T042 [US3] `4_rust_tauri/src-tauri/src/shell/paste.rs` 신규/재검증 — **순서를 그대로 지킨다**: ① 클립보드 적재 → ② **우리 창 숨기기 전에** `SetForegroundWindow` → ③ 짧게 대기 → ④ `Ctrl+V` 주입 → ⑤ 클립보드 **원복 안 함**. 참조 원본은 `3_windowsApp/CodeAssist.Shell/Platform/PasteService.cs` 의 "함정 박제" 주석 (research R10)
|
||||
- [ ] T043 [US3] 대기시간을 **조정 가능한 값**으로 둔다 — .NET 의 80ms 로 시작. 물리 타이밍이라 환경 따라 다름 (research R10 "미해결로 남기는 것")
|
||||
- [ ] T044 [US3] 붙여넣기 대상 스냅샷 — 창 소환 **직전**에 창 제목·프로세스명을 **값으로 복사**해 보관(창이 나중에 닫혀도 표시는 남아야 함, data-model §3)
|
||||
- [ ] T045 [US3] `bridge/mod.rs` 에 `{type:"paste.target", name, app}` 푸시 추가 → 프론트 배지에 직전 앱 이름 표시 (FR-015)
|
||||
- [ ] T046 [US3] `bridge/commands.rs` 에 `paste_code` 구현 — 실패해도 **코드는 클립보드에 남긴다**(사용자가 직접 `Ctrl+V`, FR-014). T035 의 "아직 안 됨" reject 제거
|
||||
- [ ] T047 [US3] quickstart C 수동 검증 — 메모장/VS Code/창전환 느린 업무용 앱 3종. **엉뚱한 곳에 붙거나 절반만 붙지 않아야 함**. 배지에 앱 이름 보이는지, 실패 시 클립보드에 남는지. 느린 앱에서 어긋나면 T043 값을 올려본다
|
||||
- [ ] T048 [US3] `specs/004-tauri-shell/stage-3.md` 기록
|
||||
|
||||
**Checkpoint**: SC-004 도달 — 평소 흐름(소환→코드 고름→직전 앱에 붙음)을 처음부터 끝까지 해볼 수 있다.
|
||||
|
||||
**2026-09-11 통합 상태**: T041~T046 관련 구현은 main에 보존했고 Rust 컴파일·단위 검사를 통과함. T047의 실제 대상 앱 검증과 단계 인수는 아직 미완료. 스니펫 DB 읽기 구현도 main에서 가져왔지만 전체 CRUD 완료를 뜻하지 않음. 상세: `stage-main-integration.md`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: User Story 4 - 화면 일부를 잘라 챗에 붙인다 (Priority: P4)
|
||||
|
||||
**Goal**: 단축키 → 영역 드래그 → 챗 입력창에 그림 첨부. **멀티모니터·다른 배율에서도 고른 영역과 결과가 일치.**
|
||||
|
||||
**Independent Test**: 배율 다른 모니터 두 대에서 각각·걸친 영역을 여러 번 캡쳐해 눈으로 대조.
|
||||
|
||||
- [x] T049 [US4] **PerMonitorV2 DPI 인지 — 확인만 하고 넘어간다.** ~~켜기~~ **할 일 없음이 T003 에서 확인됐다**: `tao` 0.35.3 이 이벤트 루프 생성 시(`event_loop.rs:190`) `become_dpi_aware()` → `SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2)` 를 **무조건** 부른다. .NET 판이 `app.manifest` 를 따로 넣어야 했던 것과 다름 (`4_rust_tauri/docs-lib/tauri-v2.md` §8).
|
||||
**그래서 T054 에서 좌표가 어긋나면 원인이 DPI 설정이 아니다** — 논리↔물리 좌표 변환(T051) 쪽을 본다. R11 의 미확정 항목은 이걸로 닫힘
|
||||
- [x] T050 [US4] `4_rust_tauri/src-tauri/src/shell/capture.rs` 신규/재검증 — 전체 가상 화면을 덮는 **투명·항상위 오버레이** 창 + 드래그 영역 선택
|
||||
- [x] T051 [US4] 네이티브 오버레이의 물리 픽셀 좌표로 그 영역 잘라내기 — `windows` crate 의 `BitBlt` 직접 호출(.NET 판과 같은 API). Tauri의 PerMonitorV2 프로세스에서 Win32 가 주는 client/가상화면 좌표는 이미 물리 픽셀이라 별도 배율 곱을 하지 않음
|
||||
- [x] T052 [US4] 취소 경로 — Esc, 포커스 이탈, 그리고 드래그 거의 없이 클릭만 하면 **조용히 취소**하고 화면 원복 (FR-018)
|
||||
- [x] T053 [US4] `bridge/mod.rs` 의 기존 `{type:"capture.image", dataUrl}` 계약을 실제 캡쳐 완료 경로에 연결 — 프론트의 read-once 소비 경로(`consumePendingCaptureImage`)에 그대로 물림
|
||||
- [ ] T054 [US4] quickstart D 수동 검증 — 배율 100%/150% 모니터·두 모니터에 걸친 영역·Esc·클릭만. 캡쳐물을 원본과 나란히 놓고 눈으로 대조(어긋나면 보통 일정 비율로 밀림) (SC-005)
|
||||
- [x] T055 [US4] `specs/004-tauri-shell/stage-4.md` 기록
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: User Story 5 - 기존 스니펫을 그대로 쓴다 (Priority: P5)
|
||||
|
||||
**Goal**: Tauri가 기존 CodeAssist 스니펫 DB를 그대로 읽고 쓴다.
|
||||
|
||||
**Independent Test**: 기존 데이터가 Tauri 팔레트에 보이고, 생성·수정·사용기록이 재실행 뒤에도 유지되면 통과.
|
||||
|
||||
**데이터 주의**: `%LocalAppData%\CodeAssist\snippets.db`의 스키마와 저널 모드를 바꾸지 않는다.
|
||||
|
||||
- [x] T056 [US5] `4_rust_tauri/src-tauri/Cargo.toml`에 `rusqlite`(bundled feature) 추가
|
||||
- [x] T057 [US5] 기존 `4_rust_tauri/src-tauri/src/bridge/snippets.rs` 확장 — `%LocalAppData%\CodeAssist\snippets.db`를 열고 없으면 기존 형식으로 생성. 참고 원본은 legacy `SnippetRepository.cs`
|
||||
- [x] T058 [P] [US5] `cargo test` — 이름 중복과 빈 필수 필드 거부 테스트를 먼저 작성하고 RED 확인
|
||||
- [x] T059 [US5] `bridge/commands.rs`에 `snippets_list`/`snippets_create`/`snippets_update`/`snippets_delete`/`snippets_record_use` 구현
|
||||
- [x] T060 [US5] 오류를 한글 메시지로 reject — 중복 이름·빈 필드·저장 실패가 프론트 `Error`→sonner로 흐르는지 확인
|
||||
- [x] T061 [US5] `usageCount` +1 / `lastUsed` 갱신 — 재실행 뒤 자주 쓰는 순서에 반영
|
||||
- [ ] T062 [US5] quickstart F — 기존 목록 표시 / 생성·수정 / 사용 횟수·순서 / 재실행 후 유지 / 빈 DB 첫 저장 확인
|
||||
- [x] T063 [US5] `specs/004-tauri-shell/stage-5.md` 기록
|
||||
|
||||
**Checkpoint**: 기존 데이터를 잃지 않고 Tauri 앱을 평소 업무에 쓸 수 있다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Polish & 마무리 (기준 앱 전환)
|
||||
|
||||
**목적**: 미구현 경로를 없애고 제품 정의·문서·release 결과물을 Tauri 기준으로 맞춘다.
|
||||
|
||||
- [ ] T064 quickstart G 확인 — US1~US5 필수 요청 중 "아직 안 됨" reject가 남아 있지 않은지 실제 흐름으로 확인
|
||||
- [ ] T065 [P] 제품 정의 갱신 — `CLAUDE.md`, `4_rust_tauri/README.md`, `4_rust_tauri/src-tauri/Cargo.toml`, `tauri.conf.json`에서 Tauri를 기준 앱, .NET을 참고용 legacy, 제품명을 `CodeAssist`로 통일
|
||||
- [ ] T066 최종 검증 — `2_frontend`에서 format+lint+test+build, `4_rust_tauri`에서 cargo test + `npm run tauri build`. 기본 검증에 `3_windowsApp`을 요구하지 않음
|
||||
- [ ] T067 `/code-review` + `superpowers:verification-before-completion`
|
||||
- [ ] T068 `specs/004-tauri-shell/report.md` 작성 — 계획 대비 구현 결과, 자동 검사, Tauri 실제 실행, 붙여넣기·캡쳐·기존 데이터 검증 증거
|
||||
- [ ] T069 사용자 최종 승인 → merge
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase 의존
|
||||
|
||||
```
|
||||
Phase 1 (Setup)
|
||||
└── Phase 2 (Tauri 빌드 베이스라인)
|
||||
└── Phase 3 (US1 transport)
|
||||
└── Phase 4 (US2 창·단축키)
|
||||
├── Phase 5 (US3 붙여넣기)
|
||||
├── Phase 6 (US4 캡쳐)
|
||||
└── Phase 7 (US5 스니펫)
|
||||
└── Phase 8 (기준 앱 전환 마무리)
|
||||
```
|
||||
|
||||
- **Phase 1**: 완료
|
||||
- **Phase 2**: 모든 구현의 실행 베이스라인
|
||||
- **Phase 3**: Phase 2 필요. 실제 Tauri bridge 왕복까지 확인
|
||||
- **Phase 4**: Phase 3 필요. route 보고 위에 창 토글을 연결
|
||||
- **Phase 5~7**: Phase 4 필요. 세 기능 모두 완료
|
||||
- **Phase 8**: US1~US5 전부 끝난 뒤
|
||||
|
||||
### 스토리 간 의존
|
||||
|
||||
- **US1**: Tauri 빌드 베이스라인 필요
|
||||
- **US2**: US1 필요
|
||||
- **US3·US4·US5**: US2 필요. 셋은 서로 파일 일부가 겹치므로 task 순서대로 합침
|
||||
|
||||
### 병렬 기회
|
||||
|
||||
- **Phase 2**: T009·T010은 서로 다른 설정 파일
|
||||
- **Phase 3 테스트**: T012·T013·T014는 테스트 파일이 겹치지 않음
|
||||
- **Phase 4**: T027·T028·T029는 `shell/` 아래 독립 파일
|
||||
- **Phase 8**: T065 문서·메타데이터 정리와 T064 실제 흐름 확인은 독립
|
||||
|
||||
### 스토리 안에서
|
||||
|
||||
- 테스트 먼저 쓰고 **RED 확인** 후 구현 (CLAUDE.md §1, superpowers:test-driven-development)
|
||||
- 검증 task 는 그 스토리 구현이 다 끝난 뒤
|
||||
- 스토리 하나 끝날 때마다 `stage-N.md` — **승인 대기 없이 계속** (CLAUDE.md §1 7단계)
|
||||
|
||||
---
|
||||
|
||||
## Parallel Example: Phase 1 / Phase 4
|
||||
|
||||
```bash
|
||||
# Phase 1 — 문서 3개 동시
|
||||
Task: "4_rust_tauri/docs-lib/README.md 카탈로그 작성"
|
||||
Task: "4_rust_tauri/docs-lib/tauri-v2.md — 쓸 API 표면 박제 (+ R11 DPI 설정 위치 확인)"
|
||||
Task: "2_frontend/docs-lib/tauri-api.md — invoke/listen 표면 박제"
|
||||
|
||||
# Phase 4 — shell 3모듈 동시 (서로 독립·콜백도 다름)
|
||||
Task: "shell/window.rs 재검증 — show/hide/focus/always-on-top/start_dragging"
|
||||
Task: "shell/hotkey.rs 재검증 — Ctrl+Shift+7/8/9 + 등록 실패 알림"
|
||||
Task: "shell/tray.rs 재검증 — 열기/항상위/종료"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### 첫 실행 가능 상태
|
||||
|
||||
1. Phase 2 — Tauri 창에 현재 프론트가 뜸
|
||||
2. Phase 3 — 화면과 Tauri bridge가 실제 왕복
|
||||
3. Phase 4 — 표준 단축키·트레이·창 상태가 동작
|
||||
|
||||
### 기능 완성 순서
|
||||
|
||||
1. US3 — 직전 앱 붙여넣기
|
||||
2. US4 — 멀티모니터 화면 캡쳐
|
||||
3. US5 — 기존 스니펫 데이터 읽기·쓰기
|
||||
4. Phase 8 — 제품 정의·release·완료 보고
|
||||
|
||||
US1~US5는 전부 필수다. 비교 결과에 따른 중간 중단은 없다.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- `3_windowsApp`은 참고용 legacy라 새 기능·기본 배포·완료 검증 대상이 아니다
|
||||
- Tauri v2 API는 T003의 docs-lib을 보고 작성
|
||||
- 기존 메시지 계약을 Tauri 사정으로 몰래 바꾸지 않음
|
||||
- 자동 계측 장치는 만들지 않음
|
||||
- 편집 후 `npm run format`, 완료 선언 전 lint+test+build (CLAUDE.md §4)
|
||||
- 작업 스텝마다 `python z-my-docs/work-log/gen_worklog.py add "<한 일>"` (CLAUDE.md §5)
|
||||
Reference in New Issue
Block a user