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,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와 브라우저가 같은 화면 결과물 사용 | 빌드 설정 + 실제 실행 확인 |
+116
View File
@@ -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).
---
+170
View File
@@ -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는 생성기가 만든 유효한 산출물 | 통째 재생성해도 같은 파일만 다시 생김. 손으로 쓴 소스만 컴파일하며 재검증함 |
+144
View File
@@ -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`에 계획 대비 구현 결과와 검증 증거를 남긴다.
+201
View File
@@ -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 설정이 아니라 논리↔물리 좌표 변환을 본다.
+229
View File
@@ -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 비교·설치물 비교·판정 기록은 범위에서 뺀다
- “비교하다 결론이 나면 중간에 멈춤” 규칙을 없앤다
- 아직 안 만든 기능은 구현 중에만 분명한 오류로 알리고, 완료 시점에는 남아 있으면 안 된다
+33
View File
@@ -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` 기준으로 바로 걷어냄
+27
View File
@@ -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와 붙여넣기를 완성함
+18
View File
@@ -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를 확인함.
+21
View File
@@ -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 미구현은 그대로 남김. 이번 개선 완료와 전체 스니펫 기능 완료는 구분함.
+297
View File
@@ -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)