# D3RO-VOICE 프로젝트 현황
> 마지막 갱신: 2026-04-13 09:30 (Phase V2-6 Admin CRM 고도화)
> 규칙 13: 작업 완료 즉시 이 파일 갱신 의무
## Phase V2-6: Admin CRM 고도화 (2026-04-13 09:30) ✅
### DB 마이그레이션 (`20260413000004_admin_enhancement.sql`)
- `audit_log` 테이블: admin_id, action, target, before/after JSON diff, memo
- `subscriptions.admin_note` 컬럼 추가
- `profiles.role` CHECK 확장: user/admin/super_admin
- RLS: admin+super_admin 조회, super_admin 쓰기
- SQL RPC 3종: admin_usage_by_feature, admin_top_users, admin_dau
### Edge Functions 4개 (admin-users, admin-subscriptions, admin-payments, admin-audit-log)
- admin-users: 유저 목록/상세 + super_admin role 변경
- admin-subscriptions: 구독 CRUD (생성/수정/소프트삭제) + 메모 필수 + 감사로그 자동 기록
- admin-payments: DB + Payple API 병행 결제 이력 조회
- admin-audit-log: 감사로그 목록/상세 (필터: target_type, admin_id, 날짜)
- 공유 유틸: `_shared/admin-auth.ts` (권한 검증), `_shared/audit.ts` (감사 기록)
### Swagger UI (`apps/admin-swagger/`)
- 독립 정적 페이지 + OpenAPI 3.0 spec
- Bearer token 입력으로 admin 인증
### Admin CRUD 웹페이지 (apps/admin)
- 구독 상세/수정/삭제: `/subscriptions/[id]` + SubscriptionForm + MemoDialog
- 구독 생성 (VIP 부여): `/subscriptions/new`
- 유저 상세: role 변경 버튼 (super_admin만) + RoleChangeDialog
- 감사로그: `/audit-log` 목록 + `/audit-log/[id]` 상세 diff (AuditDiffViewer)
- 결제 이력: PaymentHistory 컴포넌트 (DB + Payple 병행)
- 사이드바: Audit Log 메뉴 + API Docs 외부 링크
### recharts 차트 3종
- FeatureUsageChart: feature별 일간 StackedBar
- DauChart: DAU 추이 Line
- TopUsersChart: 유저별 사용량 Top 20 HorizontalBar
- Usage 페이지: 차트 + 기간 필터(7/14/30일) + 테이블
### 권한 체계
- super_admin: 구독 CRUD, role 변경, 구독 삭제 (위험 작업)
- admin: 전체 조회 전용
- admin-guard.ts: requireAdmin(), requireSuperAdmin(), isSuperAdmin()
---
## Admin CRM 독립 프로젝트 (2026-04-12 22:00) ✅
- `apps/admin` Next.js 독립 앱 (포트 3001) — apps/web에서 완전 분리
- OAuth 로그인 + app_metadata 기반 admin 가드
- RLS: 재귀 참조 수정 → `auth.jwt() -> app_metadata` 기반
- middleware: Supabase 세션 갱신
- 4개 페이지: Overview / Users / Subscriptions / Usage
## 마스터플랜 4개 작업 (2026-04-12 21:00) ✅
### 작업 1: LemonSqueezy 코드 정리 (36031ac)
- LicenseService.ts ~150줄 삭제 (API 상수/인터페이스/메서드 4개)
- activate/deactivate 로컬 키 검증 전용으로 단순화
- site/ taxNote: LemonSqueezy → Payple (10 locale)
### 작업 2: Payple 정기 갱신 크론 (d3c2a43)
- `payple-renew` Edge Function: 만료 구독 자동 재결제 + 3회 실패 시 다운그레이드
- migration: `subscriptions.renewal_failures` 컬럼 추가
- GitHub Actions: 매일 01:00 UTC 스케줄
- payple-webhook: 결제 완료 시 renewal_failures=0 리셋
### 작업 3: 관리자 CRM 웹페이지 (667c092)
- migration: `profiles.role` 컬럼 + admin RLS 정책 3개
- `/admin` 대시보드: 총 유저, 유료 구독자, API 호출, 만료 예정
- `/admin/users`: 검색/필터/페이지네이션 + 유저 상세
- `/admin/subscriptions`: 상태 필터
- `/admin/usage`: feature별 집계 (7/14/30일)
- Sidebar에 Admin 네비게이션 추가
### 작업 4: SSE 스트리밍 Premium (7467bd6)
- `sse-parser.ts`: Anthropic SSE 파서 (content_block_delta.text 추출)
- `CloudSyncService.invokeFunctionStream()`: raw fetch + ReadableStream
- `PremiumLLMService.chatStream()`: stream=true, 토큰 단위 yield + fallback
---
## 라이선스 UI 빅뱅 리라이트 (2026-04-12 20:00) ✅
LemonSqueezy 키 입력 방식 → Supabase 구독 + Payple 결제 기반 UI로 전면 교체
### 변경 내역
- `LicenseTab.tsx` / `LicenseModal.tsx`: 키 입력 제거, 클라우드 인증 + 업그레이드 버튼
- `useLicenseState.ts` 훅: 공용 라이선스+클라우드 상태 관리 (중복 제거)
- `PREMIUM_MODEL_LIMITS`: 3곳 중복 → `@d3ro/core/constants` 단일 소스
- i18n: LemonSqueezy 전용 키 18개 삭제, 구독 관련 키 13개 추가 (12 locale)
- `DashboardPage`: `dashboard.model*` → `license.model*` i18n 키 통일
### 알려진 이슈
- LicenseService.ts의 activate/deactivate 메서드 + IPC 핸들러: 코드 잔존 (UI 미사용)
- 다음 사이클에서 LemonSqueezy API 코드 완전 제거 예정
---
## Phase 3.2-B 배포/확인/버그수정 (2026-04-12 19:30) ✅
### 배포 완료
- DB migration `20260412000002_payple_billing.sql` → Supabase 리모트 적용
- Payple secrets (테스트 키) → Supabase Secrets 설정
- Edge Functions 3개 (`payple-checkout`, `payple-webhook`, `payple-manage`) 배포 ACTIVE
- Electron `OPEN_BILLING`: 개발 환경 `localhost:3000` / 프로덕션 `d3ro.dev` 분기 추가
### 대시보드 시각 확인 완료
- Premium LLM LED (CLAUDE) 초록색 표시 ✓
- 티어 카드 (FREE) 앰버 LED ✓
- 사용량 바 + Premium 쿼터 (Haiku 268/주간) ✓
- i18n 키 14개 전부 존재 확인 ✓
### 세션 만료 버그 수정
- `CloudSyncService.ts`: stored token 없을 때 `auth-changed { user: null }` emit 추가
- 렌더러 CloudSyncSection이 초기 상태를 올바르게 수신하게 됨
---
## Phase 3.2-B Payple 결제 연동 + 대시보드 Premium 상태 (코드 구현, 2026-04-12) ✅
### A. Payple 결제 연동
Stripe 기반 결제를 Payple(한국 PG)로 전면 교체. 웹(apps/web) 결제 페이지가 핵심.
**신규 파일:**
- `server/supabase/functions/_shared/payple.ts` — Payple API 래퍼 (auth, billing, cancel, deleteBillingKey)
- `server/supabase/functions/payple-checkout/index.ts` — 빌링키로 첫 결제 + 구독 활성화
- `server/supabase/functions/payple-webhook/index.ts` — 결제완료/취소 이벤트 처리
- `server/supabase/functions/payple-manage/index.ts` — 구독 취소 (빌링키 해지)
- `server/supabase/migrations/20260412000002_payple_billing.sql` — payment_provider, payple_payer_id, payple_pay_oid 컬럼
- `apps/web/src/components/billing/payple-checkout-button.tsx` — Payple JS SDK 결제창 호출
- `apps/web/src/components/billing/payple-manage-button.tsx` — 구독 관리/취소 다이얼로그
**수정 파일:**
- `apps/web/src/app/(app)/billing/page.tsx` — 티어 통일(team→pro_plus) + Payple 컴포넌트로 교체
- `apps/desktop/src/renderer/components/LicenseModal.tsx` — 업그레이드 → shell.openExternal(웹 결제)
- `packages/core/src/ipc-channels.ts` — LICENSE.OPEN_BILLING 추가
- `apps/desktop/src/preload/index.ts` — license.openBilling() API
- `apps/desktop/src/main/ipc/license-handlers.ts` — OPEN_BILLING 핸들러
- `server/supabase/config.toml` — payple-* verify_jwt=false
### B. 대시보드 Premium 상태 표시
- DashboardPage services 배열에 PREMIUM LLM LED 추가 → CrtDisplay 자동 렌더
- 백엔드 인디케이터 카드(Local/Premium) + 티어 카드
- 사용량 바: 모든 티어에서 표시 + Premium 모델별(Haiku/Sonnet/Opus) 쿼터 섹션
### i18n
12개 locale에 14개 키 추가 (service.premiumLlm, dashboard.currentBackend 등)
### 알려진 이슈 (이월)
1. ~~**세션 만료 렌더러 미동기화**~~ — ✅ 수정 완료 (stored token 없을 때 auth-changed null emit)
2. **LemonSqueezy 코드 잔존** — LicenseService.ts에 @deprecated 코드. 다음 사이클에서 정리
3. **Payple 테스트 키** — 라이브 전환 시 PAYPLE_CST_ID/CUST_KEY/REFUND_KEY/CLIENT_KEY Supabase Secrets 등록 필요
4. **Payple 정기 갱신 크론** — payple-renew 미구현. pg_cron 또는 외부 스케줄러 필요
### 백로그
- **관리자 백엔드 서비스 웹페이지**: 사용자 관리 CRM, 구독 현황 대시보드, 쿼터 관리, 결제 이력 조회. 별도 admin/ 앱 또는 apps/web 내 admin 라우트 그룹으로 구현 예정.
- **Payple 정기 갱신 크론** (payple-renew)
- **SSE 스트리밍 Premium**
---
## Phase 3.2 Premium LLM 완결 (2026-04-12) — 빅뱅 8/8 마지막 성공 기준 ✅
Supabase Edge Function(llm-proxy)을 통해 Anthropic Claude를 호출하는 PremiumLLMService 구현. 사용자가 Settings에서 Local/Premium 백엔드를 선택하면 VoiceModeService가 자동 분기하고, Premium 실패 시 Local로 silent fallback + 상단 중앙 배너 알림.
### 커밋 3개 (이번 세션)
| SHA | 내용 |
|---|---|
| `a744551` | fix: Bug 13 전면 해소 — Conversation 에러 배너 + Dictation 빈 STT 가드 + recording tip 타이머 leak |
| `d397bcb` | feat: LLM 기본 모델 qwen3:4b → gemma4:e4b 전면 전환 + think:false 안전장치 |
| `6e52c18` | feat: Phase 3.2 Premium LLM — Anthropic Claude 파이프라인 + 모델별 쿼터 + SaaS UI |
### 실측 결과
| 경로 | LLM refine 시간 |
|---|---|
| 이전 qwen3:4b (local, reasoning) | 42.9초 |
| gemma4:e4b (local, non-reasoning) | 3.9초 |
| **Claude Haiku (premium)** | **1.6~3.2초** |
### 주요 신규/변경 파일 (23개)
**신규**: `PremiumLLMService.ts`, `llm-prompts.ts`, migration `20260412000001`
**서버**: `quota.ts` (모델별 쿼터), `llm-proxy/index.ts`, `config.toml` (verify_jwt=false)
**데스크톱**: `VoiceModeService` (router+fallback), `CloudSyncService` (invokeFunction, getAccessToken async), `ConfigService` (llmBackend), `LicenseService` (쿼터 250/1500), `llm-handlers` (safeSendToRenderer+premium IPC), `preload` (premium API)
**UI**: `SettingsModal` (backend 드롭다운+conditional), `LicenseModal` (SaaS 전환), `AppLayout` (fallback 배너)
**타입**: `AppConfig.llmBackend`, `SubscriptionTier pro_plus`, `IPC_CHANNELS.LLM.PREMIUM_*`
### 모델별 쿼터 정책
| | Free | Pro ₩9,900 | Pro+ ₩29,900 |
|---|---|---|---|
| 로컬 | ∞ | ∞ | ∞ |
| Haiku | 250/주간 | 1,500/일 | ∞ |
| Sonnet | ❌ | 300/일 | 1,500/일 |
| Opus | ❌ | 50/일 | 300/일 |
### 알려진 이슈 (이월)
1. **세션 만료 시 렌더러 미동기화** — CloudSyncService.init()에서 stored token 없을 때 auth-changed null 미emit → UI 불일치 (memory/feedback_session_expiry_bug.md)
2. **Payple 결제 연동 미완** — 업그레이드 버튼 클릭 시 "결제 연동 준비 중 (Payple)" alert 스텁. Phase 3.2-B에서 실제 연결
3. **대시보드 Premium 상태 표시** — 현재 대시보드에 Premium LLM 쿼터/모델 정보 미노출
4. **Supabase 2026 verify_jwt 비호환** — sb_publishable_ 키와 Gateway JWT 검증 비호환으로 verify_jwt=false 설정. 함수 내부 requireUser()에서 직접 인증 처리 중
## 빅뱅 Phase 5 Part 7 (2026-04-11) — Bug 13 전면 해소 + Gemma4 기본 모델 전환 ✅
Part 6에서 Voice Conversation 몰입 패널까지 가고 남은 숙제였던 **Bug 13 전면 해소**와 사용자 실측에서 발견된 **LLM refine 42.9초 병목**(qwen3 reasoning 모델 strip empty fallback)을 한 사이클에 정리. 받아쓰기 한 바퀴 51초 → 기대 7~10초로 대폭 단축 예상.
### 7-A. Voice Conversation 에러 배너 UI (Part 6 후속)
`VoiceConversationPage.tsx`의 `onError` 콜백이 기존에는 주석만 있고 UI 피드백이 없었음. MUI `Snackbar + Alert(severity="warning" variant="filled")` 하단 중앙 4초 auto-hide 배너 추가. 에러 매핑 헬퍼 `formatErrorMessage`:
- `phase === 'stt' && message.startsWith('no speech')` → i18n `conversation.error.noSpeech`
- 그 외 → `${phaseLabel}: ${rawMessage}` (phaseLabel 3종 i18n)
**i18n 키 4개 추가** (ko/en): `conversation.error.phase.{stt,llm,tts}` + `conversation.error.noSpeech`
### 7-B. Dictation Bug 13 가드 (VoiceModeService)
`_transcribe()`에 2중 가드 추가 — VoiceConversationService와 동일 패턴.
1. **minBytes 가드** (16kHz 16bit mono 기준 0.5초 = 16000 bytes 미만) → `_handleError(STTAudioTooShort, 'No speech detected...')` → recording-tip popup이 'error' 상태로 전환 → 3초 자동 숨김
2. **빈 result.text 가드** → `_handleError(STTNoAudioData, 'No speech detected...')` (동일 경로)
실측 로그에서 확인된 기존 버그: `1.5초 무음 오디오 → VAD 전체 filter → 빈 전사 → LLM 5초 대기 → 빈 session completed → 빈 history entry 생성` 시퀀스가 모두 차단됨.
### 7-C. Recording Tip hide 타이머 leak 수정 (VoiceModeService)
사용자 실측에서 발견된 추가 버그 — "받아쓰기 꾹 누르고 있는데 2초 뒤에 popup이 사라짐". 원인은 `_handleError`의 `setTimeout(() => hideRecordingTip(), 3000)`이 핸들로 보관되지 않아 다음 세션 시작 후에도 예약대로 발사되어 현재 진행 중인 recording tip을 hide 시킴. Bug 13 가드 추가로 에러 경로가 자주 타면서 드러난 기존 잠재 버그.
**수정**: `_errorHideTimer: NodeJS.Timeout | null` 필드 추가 → `_handleError`에서 핸들 보관 + 기존 타이머 clearTimeout 후 재예약, `_startSession` 초기화 블록에서 `_errorEmitted = false`와 함께 clearTimeout, `dispose()`에서도 leak 방지용 cleanup. 실측 확인: 4.5초 hold 성공, 전사 `"Hey, yeah, yeah..."` 정상 완료.
### 7-D. LLM 기본 모델 전면 교체 — qwen3:4b → gemma4:e4b ⭐
**발견한 병목**: 받아쓰기 한 사이클 51.4초 = STT 7.4초(PyTorch 미설치, CPU int8) + **LLM 43초(!)** + insert 1초. LLM 43초의 원인은 `qwen3:4b`가 reasoning 모델이라 `...` 블록을 길게 생성 → `stripReasoningBlocks` 후 **빈 문자열 → 원본 transcript fallback**. 즉 43초를 기다렸는데 결과는 원본 그대로.
**해결**: 2026-04-02 릴리스된 Google **Gemma 4 e4b** (4.5B effective params)를 기본으로 교체. non-reasoning 기본 동작 + Ollama v0.20+ `think: false` 파라미터로 2중 방어.
**변경된 위치** (한 번에):
- `LocalLLMService.ts` — 3개 default fallback (`generate`/`streamGenerate`/`chatStream`) + Ollama 요청 body에 `think: false` 명시 추가 + NO_THINK 주석 업데이트(legacy 표기)
- `OnboardingModal.tsx:246` + `OllamaGuideModal.tsx:133` — pull 명령어 변경
- `packages/i18n/src/locales/{12개}.json` — `settings.ollamaHint`, `ollama.step2.alt` 2개 키 (qwen3:4b → gemma4:e4b, qwen3:8b → gemma4:26b)
- `site/src/i18n/locales/{10개}.ts` — 마케팅 페이지 detail 문자열 `qwen3 / llama3 / gemma3` → `gemma4 / llama3.2 / phi4`
- `site/src/sections/HowItWorks.tsx:47` — 파이프라인 시각화 `qwen3:4b @ localhost` → `gemma4:e4b @ localhost`
- `apps/desktop/tests/main/services/VoiceModeService.test.ts:58` — test fixture `llmModelId`
- `docs/design/00-master-architecture.md` — `LLMConfig.model` 주석 + `CONFIG_DEFAULTS.llm.model`
- `docs/design/05-external-engine-integration.md` — 6개 API 스키마 예시 + 2개 OllamaClient 코드 예시 + LLM 모델 추천 표(gemma4:e4b를 최상위로 재정렬, qwen3를 reasoning 경고와 함께 후순위로) + 권장 JSON 설정(think:false 추가)
- `docs/phases/phase-14-meeting-mode.md` — 컨텍스트 윈도우 표 갱신
**리서치 경로**: Ollama 라이브러리 확인 → gemma4는 E2B(2.3B), E4B(4.5B), 26B MoE, 31B Dense 4종 + 31b-cloud. Thinking은 `<|think|>` 토큰을 system prompt에 넣어야 켜지는 opt-in 방식. 기본 추천 E4B. 사용자는 이미 Ollama 0.20.5 사용 중(gemma4 요구 0.20+).
### 변경 파일 (대규모) — 40+ 파일
| 영역 | 파일 수 | 성격 |
|---|---|---|
| 코드 (메인) | 1 | LocalLLMService.ts |
| UI 컴포넌트 | 3 | OnboardingModal, OllamaGuideModal, VoiceConversationPage |
| 서비스 (버그 수정) | 1 | VoiceModeService |
| 테스트 | 1 | VoiceModeService.test |
| i18n (desktop) | 12 | ko/en/ja/zh/zh-TW/es/fr/de/pt/ru/vi/th |
| i18n (site) | 10 | ko/en/ja/zh/es/fr/de/pt/ru/vi |
| 마케팅 컴포넌트 | 1 | HowItWorks.tsx |
| 설계 문서 | 3 | 00-master, 05-external-engine, phase-14 |
### 검증 게이트
| 게이트 | 결과 |
|---|---|
| desktop `tsc --noEmit` | ✅ EXIT 0 |
| Bug 13 Dictation minBytes 가드 | ✅ 로그 확인 (전에 빈 history entry 생성되던 경로 차단) |
| Bug 13 Conversation 배너 UI | ✅ Snackbar severity=warning filled 노출 |
| Recording tip hide 타이머 leak | ✅ 4.5초 hold 성공, 전사 정상 |
| 기본 모델 gemma4:e4b 전환 | ⏳ **사용자 수동 pull + 실측 대기** |
### 다음 세션 액션
1. 사용자 `ollama pull gemma4:e4b` (~4.5GB)
2. 받아쓰기 한 사이클 실측 → refine 지연 측정 → **43초 → 몇 초 기대**
3. 결과 컨펌되면 커밋 정리 + 빅뱅 8/8 마지막 1개(`Phase 3.2 PREMIUM_LLM`)로 전환
4. Phase 3.2 선행 조건: 사용자가 `supabase secrets set ANTHROPIC_API_KEY=... OPENAI_API_KEY=...`
## 빅뱅 Phase 5 Part 6++ (2026-04-11) — U1/U3/U4 남은 이슈 정리 ✅
### U1 — Realtime TIMED_OUT 자동 재구독 (CloudSyncService.ts)
Publication/RLS/setAuth 모두 설정 완료 상태에서도 매 기동 시 `TIMED_OUT`이 찍히는 증상. 근본 원인은 Supabase 서버 측 transient(네트워크/WebSocket 타임아웃) 의심으로 판정. Auto push(Phase 3.3) fallback이 완벽히 동작해 블로커는 아니지만, 다른 기기에서의 원격 변경 pull을 놓치는 window가 생김.
**변경**: `subscribe()` 콜백에 지수 백오프 자동 재구독 추가.
- `SUBSCRIBED` → retry count reset + 타이머 클리어
- `TIMED_OUT` / `CHANNEL_ERROR` / `CLOSED` → `_scheduleRealtimeRetry()`
- 백오프: 1s → 3s → 10s, 3회 초과 시 포기하고 경고 로그(Auto push로만 동작)
- `stopRealtime()`에서 타이머 cleanup + count reset
### U3 — Refresh token 실패 시 재인증 이벤트 emit (CloudSyncService.ts)
`init()`의 `refreshSession` 실패 브랜치에서 기존에는 `warn` 로깅 + 토큰 삭제만 수행 → 사용자는 "왜 로그아웃됐지?" 상태로 방치. 재현이 쉽지 않지만 장시간 idle 후 발생 가능성 있어 예방적 개선.
**변경**:
- `logger.warn` → `logger.error`(에러 레벨 승격)
- `sync-error` 이벤트 emit (`"Sign-in session expired. Please sign in again."`)
- `auth-changed { user: null }` emit (renderer 게이트 재잠금)
- `try/catch`의 catch 브랜치도 동일 처리 (기존에는 로깅만)
### U4 — package-lock optional dep (확인 후 유지 결정)
`package-lock.json`에 rollup의 전 플랫폼 `optionalDependencies`(darwin/win32/linux/freebsd/android)가 기록돼 있음. Mac `npm install --force` 시 win32 관련 경고가 나왔지만 런타임 영향 없음. `apps/desktop/package.json:56`에 `@rollup/rollup-win32-x64-msvc`가 명시적 의존으로 선언되어 있는 이유는 Windows 빌드자가 사전에 binding을 확보하기 위함.
**결정**: **유지**. 크로스플랫폼 개발/CI 효율 + Win 빌드자 경험을 우선. 향후 lock 재생성 시에도 선택적 제거 불필요. 이 이슈는 **close**.
### 변경 파일 (2)
- `apps/desktop/src/main/services/CloudSyncService.ts` — U1 재구독 + U3 재인증 이벤트
- `memory/project_status.md` — 본 섹션
## 빅뱅 Phase 5 Part 6+ (2026-04-11) — U7 AudioCaptureService Mac 분기 ✅
`AudioCaptureService.getDevices()`가 `powershell -NoProfile`만 호출하던 것을
`process.platform` 분기로 교체. Mac에서 `system_profiler SPAudioDataType -json`으로
CoreAudio input 디바이스 열거(`coreaudio_device_input > 0` 필터). macOS SoX는
실제 캡처 경로에서 `-d`(default) 고정이므로 `deviceId='default'`, `label`만 사용자
구분용으로 노출. 기존 "Failed to enumerate audio devices via PowerShell" 경고
+ 1 device fallback 해소.
**변경 파일 (1)**: `apps/desktop/src/main/services/AudioCaptureService.ts` —
`_getDevicesWindows` / `_getDevicesMac` helper 추출 + 분기 디스패처.
**검증**: Electron 재기동 후 로그 `Found 2 audio device(s) on darwin` 확인 (경고 없음).
## 빅뱅 Phase 5 Part 6 (2026-04-11) — Voice Conversation UX 몰입 패널 ✅
Part 5-C 설계를 구현으로 완결. 사용자가 listening 상태에서 풀 몰입 계측기 모드 + 사운드 피드백 + 상태별 UI 분기를 실제로 경험할 수 있는 상태.
**변경 파일 (6)**
- `packages/core/src/ipc-channels.ts` — `VOICE_CONVERSATION.AUDIO_LEVEL` 채널 추가
- `apps/desktop/src/main/services/SoundEffectService.ts` — `SoundName`에 `'chime'` 추가 (recording-stop.wav 재사용)
- `apps/desktop/src/main/services/VoiceConversationService.ts` — `_onAudioLevel` forwarding + 사운드 훅 4개(start/stop/chime/error) + Bug 13(빈 STT 피드백)
- `apps/desktop/src/preload/index.ts` — `voiceConversation.onAudioLevel` 구독 API
- `apps/desktop/src/renderer/components/voice-conversation/VoiceRecordingPanel.tsx` — **신규** 9바 phosphor waveform(BAR_COUNT=9, cos 분포, 100ms, SMOOTHING=0.5, RANDOM_FACTOR=0.35) + REC LED + elapsed 타이머 + "SPEAK NOW" 힌트
- `apps/desktop/src/renderer/pages/VoiceConversationPage.tsx` — state 분기(listening=VoiceRecordingPanel / 그 외=메시지 리스트) + thinking 시 typing indicator 버블 + TextField listening 시 숨김·thinking/speaking 시 disabled
- `packages/i18n/src/locales/{ko,en}.json` — `conversation.recording.hint`, `conversation.thinking.placeholder`, `conversation.state.recording` 3개 키
**해소 이슈**
- U6 Voice Conversation UX 몰입 패널 구현
- U8 Bug 13 빈 STT 피드백 부재 (minBytes 미달 + VAD 무음 판정 양쪽 모두 `_emitError('stt', ...)` 훅 추가)
**검증**
- desktop `tsc --noEmit` EXIT 0
- Vite renderer HMR 자동 반영, Electron kill + nohup 재기동(PID 9635/9641)
- 사용자 녹음 테스트: 한국어 5.1초 오디오 전사 성공(`"에이에이에이에이 또 검중이래요"`) → 파이프라인 살아있음 확인
## Mac 환경 부트스트랩 (2026-04-11)
Windows → Mac 핸드오프 완료 (`memory/handoff-latest.md` 참조).
| 항목 | 상태 |
|---|---|
| Homebrew | ✅ 기존 설치 |
| Python 3.13 | ✅ 기존 설치 |
| Node 22.22.2 (brew) | ✅ 신규 설치 |
| sox 14.4.2 (brew) | ✅ 신규 설치 |
| Ollama | ❌ 미설치 (로컬 LLM 필요 시 `brew install ollama && ollama pull qwen3:4b`) |
| `npm install --force` | ✅ 1142 패키지 (rollup win32 옵셔널 dep 무시) |
| `@electron/rebuild@3 --version=33.4.11` | ✅ native 모듈 재빌드 완료 |
| desktop `tsc --noEmit` | ✅ |
| web `tsc --noEmit` | ✅ |
| web `next build` | ✅ (15 라우트, 5.8s compile) |
| api-client `vitest` | ✅ (19 passed, 283ms) |
| git working tree | ✅ clean (`package-lock.json` 포함 변경 없음) |
**남은 사용자 액션**
- OAuth Provider (Google/GitHub) 연결 — Supabase 대시보드 → Auth → Providers
- 외부 API 키 — ANTHROPIC / OPENAI / GOOGLE_CLOUD_STT / RESEND / STRIPE (아래 Edge Function Secrets 섹션)
- (선택) `brew install ollama` + `ollama pull qwen3:4b`
- (필요 시) `npm run dev`로 Electron 앱 수동 검증 — 첫 실행 시 Accessibility/마이크/Apple Events 권한 다이얼로그 승인
## Supabase 프로젝트 연결 (2026-04-11)
**프로젝트**: `yunchan8804-blip's Project` (ref: `llnocwyqvhgwpdjcqqyw`)
**URL**: `https://llnocwyqvhgwpdjcqqyw.supabase.co`
**CLI**: `supabase 2.84.2` (brew install supabase/tap/supabase)
| 단계 | 결과 |
|---|---|
| `supabase link --project-ref llnocwyqvhgwpdjcqqyw` | ✅ |
| `config.toml` db.major_version 15→17 | ✅ (원격 매치) |
| `supabase db push --include-all` | ✅ 8개 마이그레이션 전부 적용 |
| `supabase functions deploy` | ✅ 10개 Edge Functions 배포 |
| `supabase secrets list` | SUPABASE_* 자동 주입 4개만 존재 |
**배포된 Edge Functions** (10개)
`embed-chunks`, `llm-proxy`, `search-knowledge`, `send-push`, `stripe-checkout`, `stripe-portal`, `stripe-webhook`, `stt-proxy`, `team-accept`, `team-invite`
**로컬 env 파일** (git 정책으로 Claude가 직접 쓰지 못함 — 사용자가 수동 생성)
- `apps/web/.env.local` — NEXT_PUBLIC_SUPABASE_URL + ANON_KEY + SITE_URL + SERVICE_ROLE_KEY
- `server/supabase/.env.local` — REF/URL/KEY/DB_PASSWORD/DATABASE_URL 참조용
## Edge Function Secrets — 필요한 외부 키
지금 배포된 함수가 실제로 동작하려면 아래 키를 `supabase secrets set ...`로 주입해야 함. 키가 없으면 해당 함수는 400/500 반환.
| 키 | 쓰는 곳 | 필수? |
|---|---|---|
| `ANTHROPIC_API_KEY` | llm-proxy (채팅/액션/문서생성) | **필수** — 없으면 웹 /chat, /actions, meetings 문서 생성 불가 |
| `OPENAI_API_KEY` | embed-chunks, search-knowledge | **필수** — knowledge 시맨틱 검색에 필수 |
| `RESEND_API_KEY` | team-invite (이메일 발송) | 팀 초대 기능 쓸 때 |
| `RESEND_FROM` | team-invite 보낸이 | 기본값: `D3RO Voice ` |
| `SITE_URL` | team-invite 이메일 내 링크 | 기본값: `https://d3ro.dev` |
| `STRIPE_SECRET_KEY` | stripe-checkout/portal | 결제 기능 쓸 때 |
| `STRIPE_PRICE_PRO` / `STRIPE_PRICE_TEAM` | stripe-checkout | 각 요금제 Price ID |
| `STRIPE_WEBHOOK_SECRET` | stripe-webhook | 웹훅 서명 검증 |
| `GOOGLE_CLOUD_STT_KEY` | stt-proxy | 현재 스캐폴딩 상태 (index.ts 주석처리) |
주입 예:
```bash
cd server
SUPABASE_ACCESS_TOKEN= supabase secrets set \
ANTHROPIC_API_KEY=sk-ant-... \
OPENAI_API_KEY=sk-proj-... \
RESEND_API_KEY=re_...
```
## 현재 단계
**V1 완료** → **V2 1차 전 페이즈 + 2차/3차/4차/5차 고도화 완료** → **Mac 부트스트랩 + Supabase 배포 완료**
V2 1차:
- Phase V2-1 ✅ (Monorepo 전환 a/b/c/d + minor 정리)
- Phase V2-2 ✅ 로컬 + 원격 배포 완료 (Supabase llnocwyqvhgwpdjcqqyw)
- Phase V2-3 ✅ (Web App MVP — Next.js 15, 12 라우트)
- Phase V2-4 ✅ (데스크톱 Supabase push 동기화)
- Phase V2-5 🟡 로컬 완료 (Mac 빌드), 사용자 검증 대기
- Phase V2-6 ✅ (Mobile MVP — Expo)
- Phase V2-7 ✅ (팀 기능 — Web /teams)
- Phase V2-8 ✅ (결제 스캐폴딩 — Stripe Edge Functions + /billing)
V2 2차 고도화 (2026-04-09):
- [A1] ✅ SettingsModal에 CloudSyncSection 통합
- [A2] ✅ web Sidebar 공유 layout (app/(app)/)
- [A3] ✅ 11개 locale 번역
- [B] ✅ V2-4b pull 동기화 — history/dictionary
- [C] ✅ V2-8b Stripe webhook HMAC-SHA256 + Customer Portal
- [D] ✅ V2-6b packages/ui-native 패키지
V2 3차 고도화 (2026-04-10):
- [E] ✅ V2-4b pull 확장 — meetings/meeting_memos/meeting_documents
- [F] ⚠️ Database 제네릭 — interface→type 전환 (4차에서 완전 복원)
- [G] ✅ apps/mobile UI를 @d3ro/ui-native로 실제 교체
- [H] ✅ V2-7b 이메일 invite flow (team_invites + Edge Functions + /accept-invite)
- [I] ✅ Realtime transcripts 구독 (LiveTranscriptList)
- [J] ✅ api-client 유닛 테스트 + web E2E 스모크 스캐폴딩
V2 4차 고도화 (2026-04-10):
- [K] ✅ @supabase/ssr 0.5→0.10, supabase-js 2.45→2.103 bump → Database 제네릭 완전 주입
- [L] ✅ V1 Voice Conversation → web /chat
- [M] ✅ V1 RAG → web /knowledge
- [N] ✅ team-invite Resend 이메일 발송
- [O] ✅ mobile Expo Push
- [P] ✅ meetings/[id] 문서 생성 버튼
V2 5차 고도화 (2026-04-10):
- [Q] ✅ pgvector + knowledge 시맨틱 검색 — embedding 컬럼(1536차원), ivfflat 인덱스, match_knowledge_chunks RPC, embed-chunks/search-knowledge Edge Functions, /knowledge 검색창
- [R] ✅ /chat SSE 스트리밍 — llm-proxy가 Anthropic stream 프록시, chat-panel에서 content_block_delta 파싱 후 progressive 렌더링
- [S] ✅ 회의 문서 편집 DocumentEditor — MUI Dialog + textarea 기반 MarkdownEditor, 저장/삭제
- [T] ✅ V1 VoiceAction → web /actions — 자연어 명령 LLM 파싱 (create_meeting/search_knowledge/create_memo/send_team_invite), 파싱 결과 확인 후 실행
- [U] ✅ Desktop CloudSyncService Realtime 구독 + web record 오디오 Storage 업로드 + 회의 메모 작성 UI (MemoForm)
- [V] ✅ 11개 locale에 nav.chat/knowledge/actions 키 추가, ko.json 정리, Sidebar Actions 메뉴
V2 6차 고도화 (2026-04-11):
- [W] 🐛 fix: AppRouterCacheProvider Emotion key `'d3ro-mui'` → `'mui'` (`d3ro` 안에 숫자 `3` → Emotion 검증 실패로 dev SSR 500)
- [A] ✅ Dark/Light/Theme 토글 — `theme-mode-context.tsx` (localStorage + prefers-color-scheme), Sidebar에 6종 + auto MUI Select, 12개 locale `theme.*` 키 추가
- [B] ✅ DocumentEditor Mermaid + Markdown 렌더 — `markdown-preview.tsx` (react-markdown + remark-gfm + lazy mermaid), Edit/Preview Tabs
- [C] ✅ 회의 오디오 재생 — `meeting-audio-player.tsx` (Storage signed URL 1h TTL → `