초기 프로젝트 설정: 하네스 시스템 + 설계서 + RE 노하우

- CLAUDE.md: 프로젝트 규칙, 기술 스택, 코딩 규칙, 페이즈 로드맵
- .claude/settings.json: 권한, 강제 훅 (매 프롬프트 설계서 규칙 주입)
- .claude/skills/: implement-phase, review-phase, scaffold, test-commit, debug
- .claude/agents/: electron-architect, voice-pipeline-expert, ui-specialist
- docs/design/00-09: 마스터 아키텍처, 서비스 명세(16개), IPC(113채널),
  DB스키마, UI컴포넌트, 검증리포트, 외부엔진연동, 갭분석,
  VoiceMode패턴, 디자인시스템, 히스토리팝업
- docs/phases/1-7+3.5: 전체 구현 페이즈 문서
- docs/re-findings/: Speakly RE 노하우 5개 문서
This commit is contained in:
Yun Chan 2026-04-05 01:03:03 +09:00
commit e24bb8378c
35 changed files with 11452 additions and 0 deletions

59
docs/phases/phase-1.md Normal file
View file

@ -0,0 +1,59 @@
# Phase 1: 프로젝트 초기화 + Electron 뼈대 + 마이크 캡처
## 목표
프로젝트 기본 구조를 세우고, Electron 앱이 실행되며, 마이크에서 오디오를 캡처할 수 있는 상태까지.
## 태스크
### 1.1 프로젝트 초기화
- `npm init` + package.json 설정
- TypeScript 설정 (tsconfig.json, strict mode)
- Vite + Electron 개발 환경 설정 (electron-vite 또는 vite-plugin-electron)
- ESLint + Prettier 설정
- vitest 테스트 환경 설정
- .gitignore, README.md
### 1.2 Electron 기본 구조
- `src/main/index.ts` — 앱 진입점
- Speakly 패턴: app.whenReady → 서비스 초기화 → createWindow → IPC 등록
- 단일 인스턴스 잠금
- before-quit / will-quit 리소스 정리
- `src/preload/index.ts` — contextBridge 기본 구조
- `src/renderer/main.tsx` — React 앱 진입점
- `src/shared/ipc-channels.ts` — IPC 채널 중앙 정의
- `src/shared/types.ts` — 공통 타입
- `src/shared/errors.ts` — 에러 코드 체계 (Speakly NXError 패턴)
### 1.3 메인 윈도우
- BrowserWindow 생성 (1104x816, hiddenInset titleBar)
- contextIsolation: true, nodeIntegration: false
- 기본 React App 컴포넌트 (빈 대시보드)
### 1.4 시스템 트레이
- Tray 아이콘 + 메뉴 (표시/숨기기, 종료)
- Windows: closeToTray 옵션
### 1.5 마이크 캡처 서비스
- `src/main/services/AudioCaptureService.ts`
- Web Audio API 또는 node-record-lpcm16
- 24kHz mono PCM16 (또는 16kHz for Whisper)
- noiseSuppression: false, echoCancellation: false
- EventEmitter 패턴: 'audio-data', 'audio-level', 'started', 'stopped'
- IPC: `audio:startCapture`, `audio:stopCapture`, `audio:getDevices`
### 1.6 오디오 디바이스 관리
- 마이크 디바이스 목록 조회
- 디바이스 변경 감지
- 선택된 디바이스 저장 (electron-store)
## Speakly RE 참조
- NativeHelper.dll MicrophoneCapture: WASAPI 이벤트 구동, 24kHz mono
- AudioService: EventEmitter 패턴, 구독자 레퍼런스 카운팅
- main/index.js: 22단계 초기화 순서
## 완료 조건
- [ ] `npm run dev` 로 Electron 앱 실행됨
- [ ] 마이크에서 오디오 데이터 캡처 가능
- [ ] 시스템 트레이에 아이콘 표시
- [ ] `npm run typecheck` 통과
- [ ] `npm run test` 통과

46
docs/phases/phase-2.md Normal file
View file

@ -0,0 +1,46 @@
# Phase 2: 로컬 STT 연동 (Whisper) + 핫키
## 목표
마이크 캡처된 오디오를 로컬 Whisper 모델로 전사하고, 글로벌 핫키로 녹음을 제어할 수 있는 상태.
## 태스크
### 2.1 LocalSTTService
- `src/main/services/LocalSTTService.ts`
- faster-whisper Python sidecar 관리 (spawn/kill)
- 또는 whisper.cpp Node addon 직접 호출
- PCM 16kHz mono → Whisper → 텍스트
- Speakly 패턴: 이중 조건 플러시 (모델 로딩 + 오디오 버퍼링 병렬)
- EventEmitter: 'transcription-delta', 'transcription-complete'
### 2.2 VoiceModeService (오케스트레이터)
- `src/main/services/VoiceModeService.ts`
- Speakly 패턴 적용:
- RecognitionState 상태 머신 (IDLE→PREPARING→READY→RECOGNIZING→COMPLETED)
- AudioState 별도 추적
- _isInTerminalState() 체크
- errorEmitted 플래그
- 오디오 무손실 버퍼링 (STT 준비 전 버퍼)
- 모드: dictation (hold-to-talk), hands-free (toggle)
### 2.3 글로벌 핫키
- uiohook-napi로 글로벌 키보드 후킹
- 기본 트리거: Right Alt (Windows)
- Speakly 패턴: pressed/released 이벤트, 더블프레스 감지 (300ms), 최소 700ms
- HotkeyService + HotkeyConfig
### 2.4 IPC 채널 추가
- `voice:startRecording`, `voice:stopRecording`
- `stt:getStatus`, `stt:getModels`
- `hotkey:getDictationShortcut`, `hotkey:setDictationShortcut`
## Speakly RE 참조
- VoiceRecognitionService: 상태 머신, 이중 조건 플러시, 재연결 3계층
- VoiceModeService: 오케스트레이션, 모드별 핫키 처리
- HotkeyConfig: 키코드 맵, 시스템 예약 단축키 블랙리스트
## 완료 조건
- [ ] 핫키로 녹음 시작/종료 가능
- [ ] Whisper로 한국어 음성 전사 작동
- [ ] 상태 머신 정상 전이
- [ ] 오디오 버퍼링/플러시 정상 작동

54
docs/phases/phase-3.5.md Normal file
View file

@ -0,0 +1,54 @@
# Phase 3.5: 커서 위치 히스토리 팝업
## 목표
핫키(Ctrl+Shift+V)로 커서 근처에 최근 전사 히스토리 팝업을 띄우고, Arrow 키로 선택하여 즉시 붙여넣기.
Speakly에 없는 D3RO-VOICE 고유 편의 기능.
## 전제 조건
Phase 3 완료 (TextInsertService, HistoryService, HotkeyService, WindowManager 모두 동작)
## 태스크
### 3.5.1 HistoryPopupWindow (Vanilla JS)
- 별도 HTML 엔트리포인트 (`src/renderer/popups/history-popup/`)
- 08-design-system.md 준수: 다크 카드(#242427), 앰버 악센트(#f25b29), 22px radius
- 최근 10건 표시, 텍스트 50자 truncate, 상대 시간 표시
- 선택 항목: 좌측 앰버 바 + hover 배경
- 하단 키보드 힌트 (↑↓ SELECT / ⏎ PASTE / ESC ×)
- 등장/퇴장 애니메이션 (0.15s ease-out / 0.1s ease-in)
### 3.5.2 위치 계산
- mouse.getPosition()으로 커서 좌표
- 기본: 커서 위에 표시, 화면 밖이면 아래로
- 멀티모니터: screen.getDisplayNearestPoint()
### 3.5.3 글로벌 키 인터셉트
- HotkeyService에 historyPopupMode 추가
- 팝업 열림 중: Arrow↑↓, Enter, Escape, 1-9 키 소비
- focusable: false로 활성 앱 포커스 유지
### 3.5.4 IPC 채널 추가
- `history:showPopup`, `history:hidePopup`
- `history:showItems` (send), `history:selectItem` (send)
- `history:itemSelected` (on), `history:popupDismissed` (on)
### 3.5.5 설정 연동
- ConfigService에 historyPopup 섹션 추가 (hotkey, maxItems, autoCloseMs)
- Settings UI에 핫키 설정 추가
## Speakly RE 참조
- ResultPopupWindow: Vanilla JS 팝업 패턴, 프리로딩, 2-phase 리사이즈
- AskGensparkWindow: 키보드 인터랙션 (Enter/Escape)
- HotkeyService: 키 인터셉트 모드 (keyRecordingMode 패턴 응용)
## 설계 문서 참조
- `docs/design/09-history-popup.md` — 전체 설계
- `docs/design/08-design-system.md` — 디자인 시스템
## 완료 조건
- [ ] Ctrl+Shift+V로 커서 위에 팝업 표시
- [ ] Arrow↑↓로 항목 선택, Enter로 붙여넣기
- [ ] 숫자 1-9로 직접 선택+삽입
- [ ] Escape로 닫기
- [ ] 활성 앱 포커스 유지 (focusable: false)
- [ ] 디자인 시스템 준수 (앰버 악센트, 다크 카드)

49
docs/phases/phase-3.md Normal file
View file

@ -0,0 +1,49 @@
# Phase 3: 텍스트 삽입 + 기본 UI
## 목표
전사된 텍스트를 활성 앱에 삽입하고, 녹음 상태/결과를 표시하는 UI 구축.
## 태스크
### 3.1 TextInsertService
- `src/main/services/TextInsertService.ts`
- Speakly ClipboardPaste 패턴:
1. 클립보드 전체 백업
2. 텍스트를 클립보드에 설정
3. @nut-tree/nut-js로 Ctrl+V 시뮬레이션
4. 클립보드 복원
- 삽입 검증 (옵션)
### 3.2 RecordingTipWindow (Vanilla JS 팝업)
- 녹음 상태 표시 (opening → recording → thinking → result/error)
- 9개 웨이브 바 애니메이션 (Speakly 패턴)
- 2-phase 리사이즈
- 프리로딩 방식
### 3.3 ResultPopupWindow (Vanilla JS 팝업)
- 전사 결과 텍스트 표시
- 복사 버튼, 닫기 버튼
- 자동 숨김 + 마우스 호버 시 유지
### 3.4 Dashboard (React)
- 통계: 총 시간, 단어 수, 세션 수
- 최근 세션 목록
- Speakly 패턴: MUI Drawer(240px) + Content Area
### 3.5 Settings (React Modal)
- 언어, 테마(light/dark/auto), 핫키 설정
- 마이크 선택 + 테스트
- Ollama 서버 URL 설정
## Speakly RE 참조
- ClipboardPaste 의사코드: save→set→SendInput(Ctrl+V)→restore
- RecordingTip: wave-bar cos 분포, thinking 점근 수렴
- ResultPopup: requestAnimationFrame 높이 측정, mouseenter/leave
- App.js: MUI 테마, Drawer 네비게이션, 라우팅 패턴
- Settings: HotkeyRecordModal, 마이크 선택 UI
## 완료 조건
- [ ] 전사된 텍스트가 활성 앱(메모장 등)에 삽입됨
- [ ] RecordingTip 웨이브 애니메이션 작동
- [ ] Dashboard에 통계 표시
- [ ] Settings에서 핫키/마이크 변경 가능

115
docs/phases/phase-4.md Normal file
View file

@ -0,0 +1,115 @@
# Phase 4: Ollama LLM 연동 (텍스트 다듬기, 번역)
## 목표
Ollama REST API를 통해 로컬 LLM과 연동하여, 전사된 텍스트를 다듬기(polishing)하거나 번역(translate)할 수 있는 상태. 스트리밍 응답을 UI에 실시간 표시하고, 커스텀 명령어의 기본 구조를 갖춘다.
## 태스크
### 4.1 LocalLLMService 구현
- `src/main/services/LocalLLMService.ts`
- Ollama REST API 연동 (`/api/generate`, `/api/chat`, `/api/tags`)
- 싱글톤 + EventEmitter 패턴
- 상태 머신: `Unavailable → Available → Generating → Available` (서비스 명세 LLMState 참조)
- Ollama 가용성 폴링 (5초 간격, `OLLAMA_CONFIG.pollIntervalMs`)
- `availability-changed` 이벤트로 UI에 상태 전파
- ConfigService에서 `llm.serverUrl`, `llm.defaultModel` 읽기
### 4.2 비스트리밍 생성 (`generate`)
- `POST /api/generate` 호출 (stream: false)
- GenerateOptions 지원: model, temperature, maxTokens, topP, topK, systemPrompt
- GenerateResult 반환: text, model, promptTokens, completionTokens, totalDuration
- 타임아웃 처리 (AbortController, 기본 120초)
### 4.3 스트리밍 생성 (`stream`)
- `POST /api/generate` (stream: true) → NDJSON 파싱
- fetch + ReadableStream으로 청크 처리
- `token` 이벤트를 통해 점진적 토큰 전달
- AbortController로 중간 취소 지원
- 버퍼 관리: 불완전한 JSON 라인 처리
### 4.4 텍스트 다듬기 모드 (Polishing)
- PostProcessCommand `{ type: 'polish', style?: 'formal' | 'casual' }` 처리
- 시스템 프롬프트 설계:
```
[formal] 다음 음성 전사 텍스트를 자연스럽고 격식 있는 문어체로 다듬어주세요.
원래 의미를 유지하면서 문법 오류를 수정하고, 불필요한 반복이나 필러를 제거하세요.
다듬어진 텍스트만 출력하세요. 설명이나 부가 문구를 붙이지 마세요.
[casual] 다음 음성 전사 텍스트를 자연스러운 구어체로 다듬어주세요.
원래 의미와 톤을 유지하면서 문법 오류만 수정하세요.
다듬어진 텍스트만 출력하세요.
```
- VoiceModeService의 Processing 상태에서 호출
### 4.5 번역 모드 (Translate)
- PostProcessCommand `{ type: 'translate', targetLanguage: string }` 처리
- 시스템 프롬프트 설계:
```
다음 텍스트를 {targetLanguage}로 번역해주세요.
자연스럽고 정확한 번역만 출력하세요. 원문이나 설명을 붙이지 마세요.
```
- 감지 언어 → 대상 언어 자동 전환 (한국어 감지 시 영어로, 영어 감지 시 한국어로)
- ConfigService에서 기본 대상 언어 설정 지원
### 4.6 커스텀 명령어 기본 구조
- PostProcessCommand `{ type: 'custom', prompt: string }` 처리
- 사용자가 직접 시스템 프롬프트를 지정하여 LLM에 전달
- CustomInstruction 인터페이스 초기 정의:
```typescript
interface CustomInstruction {
id: string;
name: string;
prompt: string;
isBuiltin: boolean;
createdAt: number;
}
```
- Phase 6에서 완전 구현 (CRUD, 프리셋, 핫키 바인딩)
### 4.7 스트리밍 응답 UI 표시
- RecordingTipWindow에 `processing` 상태 추가
- Thinking 프로그레스 바 (점근 수렴 패턴: `min(95, (1 - 1/(1+1.5*t)) * 100)%`)
- 스트리밍 토큰이 도착하면 텍스트 실시간 표시
- ResultPopupWindow에 스트리밍 텍스트 영역 추가
- 토큰 도착 시 점진적 텍스트 추가
- 완료 시 복사 버튼 활성화
- IPC 채널:
- `llm:streamToken` (main→renderer, 스트리밍 토큰)
- `llm:streamComplete` (main→renderer, 생성 완료)
- `llm:streamError` (main→renderer, 에러)
### 4.8 VoiceModeService LLM 통합
- RecognitionState.Processing 상태에서 LLM 호출
- 전사 완료 → postProcessCommand에 따라 분기:
- `none`: 바로 텍스트 삽입
- `polish` / `translate` / `custom`: LLM 스트리밍 호출 → 완료 후 텍스트 삽입
- `processing-update` 이벤트를 통해 UI에 진행 상황 전달
- LLM 미가용 시 postProcess를 `none`으로 폴백 + 사용자 알림
### 4.9 에러 처리
- Ollama 서버 미실행: `LLM_CONNECTION_FAILED` 에러, UI에 "Ollama가 실행 중이 아닙니다" 표시
- 모델 미설치: `LLM_MODEL_NOT_FOUND` 에러, UI에 모델 설치 안내 표시
- 생성 실패/타임아웃: `LLM_GENERATION_FAILED`, 원본 텍스트로 폴백 옵션 제공
- 네트워크 에러: 재시도 없이 즉시 에러 표시 (로컬이므로 재시도 불필요)
### 4.10 IPC 채널 추가
- `llm:getStatus` — LLM 가용 상태 조회
- `llm:getModels` — 설치된 Ollama 모델 목록
- `llm:generate` — 비스트리밍 텍스트 생성
- `llm:stream` — 스트리밍 텍스트 생성 시작
- `llm:cancelStream` — 스트리밍 취소
- `llm:checkConnection` — Ollama 연결 테스트
## Speakly RE 참조
- GensparkService: 클라우드 LLM 호출 패턴 (D3RO는 로컬 Ollama로 대체)
- CustomInstructionService: 명령어 구조, 프롬프트 템플릿
- VoiceModeService: Processing 상태 전이, 후처리 파이프라인
- RecordingTip: thinking 상태 프로그레스 바, 점근 수렴 패턴
## 완료 조건
- [ ] Ollama 가용성 자동 감지 및 UI 표시
- [ ] 전사된 텍스트를 다듬기(polish) 모드로 LLM 처리 가능
- [ ] 전사된 텍스트를 번역(translate) 모드로 LLM 처리 가능
- [ ] 커스텀 프롬프트로 자유 LLM 처리 가능
- [ ] 스트리밍 토큰이 UI에 실시간 표시됨
- [ ] Ollama 미실행/모델 미설치 시 적절한 에러 메시지 표시
- [ ] LLM 미가용 시 원본 텍스트 삽입으로 폴백

148
docs/phases/phase-5.md Normal file
View file

@ -0,0 +1,148 @@
# Phase 5: TTS + 히스토리/사전 DB
## 목표
TTS 엔진을 연동하여 텍스트를 음성으로 재생하고, 히스토리와 사전 DB를 완전 구현하여 사용자 데이터를 체계적으로 관리한다. Dashboard 통계 UI를 완성한다.
## 태스크
### 5.1 LocalTTSService 구현
- `src/main/services/LocalTTSService.ts`
- Piper-TTS 또는 Kokoro sidecar 프로세스 관리 (spawn/kill)
- 상태 머신: `Idle → Loading → Speaking → Idle` (서비스 명세 TTSState 참조)
- 싱글톤 + EventEmitter 패턴
- Sidecar 통신 프로토콜:
```
Main Process piper-tts sidecar
│── stdin: text ────────►│
│◄── stdout: WAV data ───│ (또는 PCM 스트리밍)
```
- 음성 모델 디렉토리 관리 (`userData/tts-models/`)
- TTSVoice 목록 조회: id, name, language, gender, sampleRate, downloaded
### 5.2 TTS 오디오 재생
- Node.js 측에서 PCM/WAV 데이터를 renderer로 전달
- renderer에서 Web Audio API로 재생
- IPC 채널:
- `tts:speak` — 텍스트 합성 및 재생 시작
- `tts:stop` — 재생 중지
- `tts:getVoices` — 사용 가능한 음성 목록
- `tts:audioChunk` (main→renderer) — 오디오 청크 스트리밍
- `tts:finished` (main→renderer) — 재생 완료
- TTSOptions: speed (0.5~2.0), format (pcm/wav)
### 5.3 TTS 재생 UI
- ResultPopupWindow에 재생/중지 버튼 추가
- 재생 아이콘 (▶) → 클릭 시 TTS 재생, 아이콘 중지(■)로 변경
- 재생 중 상태 표시 (파형 또는 진행 바)
- History UI 각 항목에 재생 버튼 추가
- ConfigService의 `tts.enabled` 설정에 따라 버튼 표시/숨김
### 5.4 HistoryService 완전 구현
- `src/main/services/HistoryService.ts`
- better-sqlite3 + drizzle-orm (DB 스키마: `03-db-and-ui.md` 참조)
- CRUD 메서드:
- `create(input: CreateHistoryInput): Promise<History>` — nanoid로 ID 생성
- `getById(id: string): Promise<History | null>`
- `getList(filter: HistoryFilter): Promise<{ entries: History[]; total: number }>` — 페이지네이션
- `update(id: string, data: Partial<History>): Promise<void>`
- `softDelete(id: string): Promise<void>` — deleted 플래그 설정
- `hardDelete(id: string): Promise<void>` — 물리 삭제
- `bulkDelete(ids: string[]): Promise<void>`
- 검색: originalText, polishedText에 대한 LIKE 검색, 날짜 범위, 모드, 상태 필터
- 통계 집계:
- `getStats(): Promise<HistoryStats>` — 총 항목, 총 녹음 시간, 언어별 통계 등
- stats 테이블 싱글턴 업데이트 (세션 완료 시 자동 갱신)
- 연속 사용 일수(streakDays) 계산
- 보존 정책:
- `cleanupOldEntries()`: retentionDays(30일) 초과 + softDelete된 항목 물리 삭제
- 앱 시작 시 + 24시간 주기로 실행
- maxEntries 초과 시 오래된 항목부터 softDelete
### 5.5 DictionaryService 완전 구현
- `src/main/services/DictionaryService.ts`
- better-sqlite3 + drizzle-orm (DB 스키마: `03-db-and-ui.md` 참조)
- CRUD 메서드:
- `add(word: string, pronunciation?: string, category?: string): Promise<Dictionary>`
- `update(id: string, data: Partial<Dictionary>): Promise<void>`
- `delete(id: string): Promise<void>`
- `getAll(filter?: DictionaryFilter): Promise<Dictionary[]>`
- `search(query: string): Promise<Dictionary[]>` — word 검색
- `incrementUsage(id: string): Promise<void>` — usageCount 증가 + lastUsedAt 갱신
- 카테고리 관리: `user`, `auto`, `technical`
- STT 연동: Whisper initialPrompt에 사전 단어 목록 주입
- 전사 시작 전 DictionaryService에서 상위 N개 단어 조회
- initialPrompt 형태: `"단어1, 단어2, 단어3"` (Whisper 컨텍스트 힌트)
- 사용 횟수 자동 갱신: 전사 결과에 사전 단어가 포함되면 incrementUsage 호출
### 5.6 History UI (React)
- `src/renderer/components/History.tsx`
- 목록 표시:
- MUI DataGrid 또는 커스텀 리스트
- 원본 텍스트, 다듬어진 텍스트, 모드, 상태, 날짜, 녹음 시간 표시
- 무한 스크롤 또는 페이지네이션 (기본 50개씩)
- 검색: 텍스트 검색 입력 + 필터 (날짜 범위, 모드, 언어)
- 항목별 액션:
- 복사 (원본/다듬은 텍스트)
- 재시도 (같은 텍스트를 다시 LLM 처리)
- TTS 재생 (Phase 5.3 연동)
- 삭제 (softDelete + 확인 다이얼로그)
- IPC 채널:
- `history:getList` — 목록 조회
- `history:getById` — 상세 조회
- `history:delete` — 삭제
- `history:bulkDelete` — 일괄 삭제
- `history:search` — 검색
- `history:getStats` — 통계
### 5.7 Dictionary UI (React)
- `src/renderer/components/Dictionary.tsx`
- 단어 목록: word, pronunciation, category, usageCount 표시
- 추가: 단어 + 발음 힌트 + 카테고리 입력 다이얼로그
- 편집: 인라인 편집 또는 모달
- 삭제: 확인 후 삭제
- 가져오기/내보내기:
- JSON 파일로 내보내기 (`[{ word, pronunciation, category }]`)
- JSON 파일에서 가져오기 (중복 word+category 시 스킵 또는 덮어쓰기 옵션)
- Electron dialog.showOpenDialog / dialog.showSaveDialog 사용
- IPC 채널:
- `dictionary:getAll` — 전체 목록
- `dictionary:add` — 추가
- `dictionary:update` — 수정
- `dictionary:delete` — 삭제
- `dictionary:import` — 파일에서 가져오기
- `dictionary:export` — 파일로 내보내기
### 5.8 Dashboard 통계 UI 완성
- `src/renderer/components/Dashboard.tsx`
- 통계 카드:
- 총 녹음 시간 (시:분:초 형식)
- 총 단어 수
- 총 세션 수
- 연속 사용 일수 (streakDays)
- 최근 7일 / 30일 활동 그래프 (간단한 바 차트, MUI 또는 커스텀 SVG)
- 최근 세션 목록 (5~10개, History UI로 이동 링크)
- 언어별/모드별 사용 비율 (파이 차트 또는 비율 바)
- stats 테이블에서 데이터 조회 + history 테이블에서 최근 데이터 집계
### 5.9 Drawer 네비게이션 업데이트
- Speakly 패턴: MUI Drawer (240px, permanent)
- 네비게이션 항목 추가: Dashboard, History, Dictionary
- 아이콘 + 텍스트 라벨
- 현재 라우트 하이라이트
## Speakly RE 참조
- HistoryService: SQLite 스키마 (history 테이블), CRUD 패턴, 검색 쿼리
- DictionaryService: 단어 사전 관리, 카테고리 분류
- RecordStatsService: 싱글턴 통계 테이블, 누적 집계 패턴
- App.js: MUI Drawer 네비게이션, 라우팅 패턴 (useState 기반)
- ResultPopup: 결과 표시 + 액션 버튼 패턴
## 완료 조건
- [ ] TTS로 텍스트 음성 재생 가능
- [ ] 재생/중지 버튼 UI 작동
- [ ] 히스토리 CRUD + 검색 + 페이지네이션 작동
- [ ] 히스토리 보존 정책 (30일 초과 자동 정리) 작동
- [ ] 사전 CRUD + 가져오기/내보내기 작동
- [ ] 사전 단어가 STT initialPrompt에 주입됨
- [ ] Dashboard 통계 카드 및 최근 세션 표시
- [ ] Drawer 네비게이션으로 각 화면 이동 가능

183
docs/phases/phase-6.md Normal file
View file

@ -0,0 +1,183 @@
# Phase 6: 커스텀 명령어 + 설정 UI 고도화
## 목표
사용자 정의 LLM 명령어 시스템을 완전 구현하고, 설정 UI를 고도화하여 STT/LLM/TTS 모델 선택, 온보딩, 다국어(i18n)를 지원한다.
## 태스크
### 6.1 CustomInstructionService 완전 구현
- `src/main/services/CustomInstructionService.ts`
- electron-store에 명령어 목록 저장 (DB 아닌 설정 파일)
- CustomInstruction 타입:
```typescript
interface CustomInstruction {
id: string; // nanoid
name: string; // 표시 이름 (예: "번역 (한→영)")
description: string; // 설명
prompt: string; // 시스템 프롬프트 템플릿
icon: string; // MUI 아이콘 이름 또는 이모지
isBuiltin: boolean; // 프리셋 여부 (삭제 불가)
hotkeyId: string | null; // 바인딩된 핫키 ID (null이면 미설정)
order: number; // 표시 순서
createdAt: number;
updatedAt: number;
}
```
- CRUD 메서드:
- `getAll(): CustomInstruction[]`
- `getById(id: string): CustomInstruction | null`
- `create(input: CreateCustomInstructionInput): CustomInstruction`
- `update(id: string, data: Partial<CustomInstruction>): void`
- `delete(id: string): void` — isBuiltin은 삭제 불가
- `reorder(ids: string[]): void` — 순서 변경
- `resetBuiltins(): void` — 프리셋을 기본값으로 초기화
### 6.2 프리셋 명령어 5개
- 앱 최초 실행 시 자동 생성 (isBuiltin: true)
1. **번역** (`translate`)
```
다음 텍스트를 {{targetLanguage}}로 번역해주세요.
자연스럽고 정확한 번역만 출력하세요.
```
2. **요약** (`summarize`)
```
다음 텍스트의 핵심 내용을 3줄 이내로 요약해주세요.
요약문만 출력하세요.
```
3. **전문 리라이트** (`formal-rewrite`)
```
다음 텍스트를 격식 있는 비즈니스 문체로 다시 작성해주세요.
원래 의미를 유지하면서 전문적인 톤으로 변환하세요.
다시 작성된 텍스트만 출력하세요.
```
4. **코드 설명** (`explain-code`)
```
다음 코드를 한국어로 설명해주세요.
각 부분이 무엇을 하는지 간결하게 설명하세요.
```
5. **자유 프롬프트** (`free-prompt`)
```
{{userPrompt}}
```
- 사용자가 매번 프롬프트를 직접 입력하는 특수 모드
- UI에서 프롬프트 입력 필드 표시
### 6.3 사용자 정의 명령어 CRUD UI
- `src/renderer/components/CustomCommands.tsx`
- 명령어 목록: 이름, 설명, 프리셋 여부 표시
- 추가: 이름, 설명, 프롬프트 템플릿 입력 다이얼로그
- 프롬프트 템플릿에 `{{text}}` 변수 자동 삽입 안내
- 프롬프트 미리보기 (예시 텍스트로 치환 결과 표시)
- 편집: 프리셋은 프롬프트만 수정 가능, 사용자 정의는 전체 수정 가능
- 삭제: 프리셋은 삭제 불가 (리셋만 가능), 사용자 정의는 확인 후 삭제
- 드래그 앤 드롭으로 순서 변경
- IPC 채널:
- `customInstruction:getAll`
- `customInstruction:create`
- `customInstruction:update`
- `customInstruction:delete`
- `customInstruction:reorder`
### 6.4 핫키 바인딩 (명령어별)
- 각 커스텀 명령어에 개별 핫키 설정 가능
- HotkeyService에 동적 핫키 등록/해제 연동
- 핫키 녹화 UI (Speakly HotkeyRecordModal 참조):
- 모달에서 키 조합 누르면 감지 → 표시 → 저장
- 시스템 예약 키 블랙리스트 (Ctrl+C, Ctrl+V, Alt+F4 등)
- 중복 핫키 충돌 감지 + 경고
- 명령어별 핫키 → VoiceModeService에 postProcess 자동 설정
- 핫키 감지 시: 해당 명령어의 prompt로 postProcess 설정 후 녹음 시작
### 6.5 설정 UI 고도화
- `src/renderer/components/Settings.tsx` — MUI Modal 기반
- 섹션별 탭 구성:
**일반 탭:**
- 테마 선택 (light/dark/system)
- 시작 시 최소화
- 닫기 시 트레이로 최소화
**음성 입력(STT) 탭:**
- STT 모델 선택 드롭다운 (base, small, medium, large-v3)
- 모델별 크기, 정확도 설명 표시
- 미다운로드 모델 표시 + 다운로드 버튼 (선택)
- 기본 언어 선택 (auto, ko, en, ja, zh 등)
- VAD 필터 토글
- 초기 프롬프트 입력
- 마이크 디바이스 선택 + 테스트 (Phase 3에서 이관)
**LLM 탭:**
- Ollama 서버 URL 입력 + 연결 테스트 버튼
- 기본 LLM 모델 선택 드롭다운 (Ollama에서 설치된 모델 목록 조회)
- 기본 온도, 최대 토큰 설정
- 기본 후처리 명령 선택
**TTS 탭:**
- TTS 활성화/비활성화 토글
- 기본 음성 선택 (설치된 음성 목록)
- 말하기 속도 슬라이더 (0.5x ~ 2.0x)
- 미리듣기 버튼
**핫키 탭:**
- Dictation 모드 핫키 설정
- Hands-free 모드 핫키 설정
- 커스텀 명령어별 핫키 설정 (6.4 연동)
- 핫키 녹화 모달
### 6.6 온보딩 플로우 (첫 실행 안내)
- 앱 최초 실행 감지: ConfigService에 `onboarding.completed` 플래그
- 4단계 온보딩 위저드:
1. **환영**: 앱 소개, 주요 기능 설명
2. **마이크 설정**: 디바이스 선택 + 테스트 녹음
3. **핫키 설정**: 기본 핫키 안내 + 커스텀 설정
4. **Ollama 설정**: Ollama 설치 안내 + 연결 테스트, 모델 선택
- 건너뛰기 가능, 설정에서 다시 실행 가능
- Speakly FlowOnboarding 참조: 단계별 UI, 진행 표시기
### 6.7 i18n (한국어/영어)
- `src/shared/i18n/` 디렉토리
- 리소스 파일 구조:
```
i18n/
├── ko.json # 한국어 (기본)
└── en.json # 영어
```
- 간단한 i18n 유틸리티 (라이브러리 없이 직접 구현):
```typescript
type I18nKey = string;
function t(key: I18nKey, params?: Record<string, string>): string;
function setLocale(locale: 'ko' | 'en'): void;
function getLocale(): string;
```
- React 컨텍스트: `I18nProvider` + `useI18n()`
- ConfigService `ui.language` 연동
- 번역 대상: UI 라벨, 에러 메시지, 프리셋 명령어 이름/설명, 온보딩 텍스트
- Speakly I18nService 참조: 키-값 리소스 파일, 런타임 언어 전환
### 6.8 IPC 채널 추가
- `customInstruction:getAll`, `customInstruction:create`, `customInstruction:update`, `customInstruction:delete`, `customInstruction:reorder`
- `config:getSection`, `config:setSection`, `config:resetSection`
- `i18n:getLocale`, `i18n:setLocale`, `i18n:getTranslations`
- `onboarding:getStatus`, `onboarding:complete`
## Speakly RE 참조
- CustomInstructionConfigService: 명령어 CRUD, 프리셋 관리, electron-store 저장
- HotkeyConfig: 키코드 맵, 시스템 예약 단축키 블랙리스트, HotkeyRecordModal
- FlowOnboarding: 단계별 온보딩 UI, 진행 표시, 건너뛰기
- I18nService: 키-값 리소스 파일, 런타임 언어 전환, React 컨텍스트 연동
- Settings: 섹션별 탭 UI, 디바이스 선택, 모델 선택
## 완료 조건
- [ ] 프리셋 명령어 5개 기본 제공
- [ ] 사용자 정의 명령어 추가/편집/삭제/순서변경 가능
- [ ] 명령어별 핫키 바인딩 및 핫키로 녹음+명령어 실행 가능
- [ ] 설정에서 STT 모델, LLM 모델, TTS 음성 선택 가능
- [ ] Ollama 연결 테스트 작동
- [ ] 온보딩 위저드가 첫 실행 시 표시됨
- [ ] 한국어/영어 UI 전환 가능
- [ ] 모든 UI 텍스트가 i18n 리소스에서 로드됨

191
docs/phases/phase-7.md Normal file
View file

@ -0,0 +1,191 @@
# Phase 7: 테스트 + 빌드 + 배포
## 목표
테스트 커버리지를 확보하고, 프로덕션 빌드/패키징 파이프라인을 구축하여 Windows 설치 파일을 생성한다. CI/CD를 설정하여 자동 빌드 및 배포를 가능하게 한다.
## 태스크
### 7.1 테스트 전략 수립
- 3계층 테스트 피라미드:
- **단위 테스트** (vitest): 서비스 로직, 유틸리티, 상태 머신
- **통합 테스트** (vitest): IPC 핸들러, DB 쿼리, 서비스 간 상호작용
- **E2E 테스트** (Playwright + electron): 전체 사용자 시나리오
- 커버리지 목표: 단위 70%+, 통합 50%+, E2E 핵심 플로우
### 7.2 단위 테스트 — 최우선 대상
- **VoiceModeService 상태 머신**:
- 모든 RecognitionState 전이 경로 테스트
- 모든 AudioState 전이 경로 테스트
- 잘못된 전이 시도 시 에러 처리
- 이중 조건 플러시 (sttReady + audioBuffer) 시나리오
- 타이밍 상수 검증 (minAudioDurationMs, doublePressMs)
- 세션 취소/타임아웃 시나리오
- **LocalSTTService**:
- sidecar 프로토콜 파싱 (JSON stdout)
- 이중 조건 플러시 로직
- 모델 로딩 상태 전이
- 에러 전파 (sidecar crash, transcription fail)
- **LocalLLMService**:
- Ollama API 응답 파싱
- NDJSON 스트리밍 파싱 (불완전 라인 처리 포함)
- 가용성 폴링 로직
- 에러 코드별 처리 (연결 실패, 모델 미설치)
- **HotkeyService**:
- 키코드 매칭 + 수정자 키 조합
- 더블프레스 감지 (300ms 윈도우)
- holdMode vs toggleMode 동작
- **TextInsertService**:
- 클립보드 백업/복원 흐름
- 삽입 실패 시 클립보드 복원 보장
- **HistoryService / DictionaryService**:
- CRUD 메서드 (in-memory SQLite 사용)
- 검색/필터 쿼리
- 보존 정책 (cleanup)
- 통계 집계 정확성
- **CustomInstructionService**:
- CRUD + 프리셋 보호 (삭제 불가)
- 순서 변경
- 프롬프트 템플릿 변수 치환
### 7.3 통합 테스트
- **IPC 핸들러 테스트**:
- 각 IPC 채널이 올바른 서비스 메서드를 호출하는지 검증
- 인자 검증 + 에러 응답 형식
- preload bridge ↔ main handler 매핑 정합성
- **DB 쿼리 통합 테스트**:
- drizzle-orm 쿼리가 실제 SQLite에서 올바르게 실행되는지 검증
- 마이그레이션 적용 후 스키마 일관성
- 동시 접근 시나리오 (WAL 모드)
- **서비스 간 상호작용**:
- VoiceModeService → AudioCaptureService → LocalSTTService → LocalLLMService → TextInsertService 파이프라인
- ConfigService 변경 → 서비스 반영 (설정 핫리로드)
### 7.4 E2E 테스트 (Playwright)
- `@playwright/test` + `electron` fixture 사용
- 핵심 시나리오:
1. 앱 실행 → 메인 윈도우 표시
2. Dashboard 통계 로드
3. Settings 열기 → 설정 변경 → 저장
4. History 목록 표시 → 검색 → 삭제
5. Dictionary 추가 → 편집 → 삭제
6. 온보딩 플로우 완주 (첫 실행 시뮬레이션)
- 외부 의존성 모킹:
- Ollama API: MSW(Mock Service Worker) 또는 로컬 HTTP 서버
- faster-whisper sidecar: 모킹된 stdout 응답
- piper-tts sidecar: 모킹된 WAV 출력
### 7.5 테스트 유틸리티
- `tests/helpers/`:
- `createTestDb()` — in-memory SQLite + 스키마 적용
- `mockElectronStore()` — electron-store 모킹
- `mockSidecar()` — sidecar stdout/stdin 모킹
- `createMockAudioBuffer()` — 테스트용 PCM16 버퍼 생성
- `waitForState()` — 상태 머신 전이 대기 유틸
- vitest 설정:
- `vitest.config.ts`: 경로 별칭, 환경변수, 타임아웃
- `vitest.workspace.ts`: unit / integration / e2e 워크스페이스 분리
### 7.6 electron-vite 빌드 설정
- `electron.vite.config.ts`:
- main: TypeScript → CJS, externals (better-sqlite3, uiohook-napi, @nut-tree/nut-js)
- preload: TypeScript → CJS, contextIsolation 호환
- renderer: React + MUI → 번들 (코드 스플리팅, 트리 셰이킹)
- 환경 분리: `MAIN_VITE_*`, `RENDERER_VITE_*`
- native 모듈 빌드: electron-rebuild 또는 prebuild-install
- 팝업 HTML 복사: recording-tip.html, result-popup.html → output
### 7.7 electron-builder 패키징
- `electron-builder.yml` 설정:
```yaml
appId: com.d3ro.voice
productName: D3RO Voice
directories:
output: release
win:
target:
- target: nsis
arch: [x64]
icon: resources/icon.ico
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
createDesktopShortcut: true
createStartMenuShortcut: true
extraResources:
- from: sidecar/
to: sidecar/
filter: ["**/*"]
```
- sidecar 번들링:
- faster-whisper Python 환경 (embedded Python 또는 PyInstaller 빌드)
- piper-tts 바이너리 + 음성 모델
- Whisper 모델 파일 (선택: 초기 번들 또는 첫 실행 시 다운로드)
- native 모듈 리빌드: better-sqlite3, uiohook-napi, @nut-tree/nut-js
- asar 설정: native 모듈은 asar 외부 (`asarUnpack`)
### 7.8 자동 업데이트 (electron-updater, 선택)
- `electron-updater` 라이브러리 통합
- GitHub Releases를 업데이트 서버로 사용
- 업데이트 체크: 앱 시작 시 + 24시간 주기
- 업데이트 플로우:
1. `autoUpdater.checkForUpdates()`
2. 업데이트 발견 시 알림 (강제 아님)
3. 사용자 승인 시 백그라운드 다운로드
4. 다운로드 완료 → 재시작 안내
- 버전 관리: semver, `package.json` version 필드
### 7.9 CI/CD (GitHub Actions)
- `.github/workflows/ci.yml`:
```yaml
on: [push, pull_request]
jobs:
lint-and-typecheck:
- npm run lint
- npm run typecheck
unit-test:
- npm run test:unit
integration-test:
- npm run test:integration
build:
- npm run build
- 빌드 산출물 아티팩트 업로드
```
- `.github/workflows/release.yml`:
```yaml
on:
push:
tags: ['v*']
jobs:
build-and-release:
- npm run build
- electron-builder --win --x64
- GitHub Release 생성 + 설치파일 업로드
```
- Windows 러너 사용 (`runs-on: windows-latest`)
- 캐시: node_modules, electron 바이너리
### 7.10 코드 서명 (선택)
- Windows: Authenticode 서명 (EV 코드 서명 인증서)
- electron-builder 설정:
```yaml
win:
sign: ./scripts/sign.js
signingHashAlgorithms: [sha256]
```
- CI에서 서명: GitHub Secrets에 인증서 저장
- 서명 없이도 실행 가능 (SmartScreen 경고 표시)
## Speakly RE 참조
- Speakly 패키징: electron-builder, NSIS 인스톨러, Squirrel.Windows 기반 업데이트
- Speakly 빌드: NativeHelper.dll → extraResources, asar 외부 배치
- Speakly CI: 자동 빌드/배포 파이프라인 (D3RO는 GitHub Actions로 대체)
## 완료 조건
- [ ] `npm run test:unit` — VoiceModeService, HistoryService 등 핵심 서비스 테스트 통과
- [ ] `npm run test:integration` — IPC 핸들러, DB 쿼리 테스트 통과
- [ ] `npm run test:e2e` — 핵심 사용자 시나리오 테스트 통과
- [ ] `npm run build` — 프로덕션 빌드 성공
- [ ] electron-builder로 Windows NSIS 인스톨러 생성
- [ ] 인스톨러로 설치 → 실행 → 기본 기능 작동 확인
- [ ] GitHub Actions CI 파이프라인 작동 (lint, typecheck, test, build)
- [ ] (선택) 자동 업데이트 체크 작동