# D3RO-VOICE 프로젝트 현황 > 마지막 갱신: 2026-04-13 (RN CLI 전환 + 안드로이드 빌드 성공) ## RN CLI 전환 (2026-04-13) ✅ ### Expo → React Native CLI 전환 - apps/mobile-rn: RN 0.85 + React 19 + TypeScript - 기존 Expo 코드(apps/mobile-expo) → RN CLI용 6개 화면 이식 완료 - Navigation: @react-navigation/native + bottom-tabs + native-stack - 공유 패키지: @d3ro/ui-native, @d3ro/i18n, @d3ro/core, @d3ro/api-client 연결 ### Android 빌드 환경 (Windows) - JDK 17 (Microsoft OpenJDK 17.0.18) - Android SDK: platform-34, build-tools-34, NDK 27.1, CMake 3.22.1 - **Windows NDK CMake 버그 워크아라운드**: libc++_shared.so가 자동 링크 안 됨 - `scripts/fix-windows-ndk.sh`: ninja 파일 후패치 스크립트 - 빌드 순서: `gradlew configureCMakeDebug` → `fix-windows-ndk.sh` → ninja 수동 빌드 → `gradlew assembleDebug -x :react-native-screens:buildCMakeDebug[arm64-v8a]` - `reactNativeArchitectures=arm64-v8a` (32-bit 제외) - `newArchEnabled=false` ### APK 설치 성공 - SM_F956N (갤럭시 폴드) 무선 디버깅으로 설치 확인 - Metro 번들러 연결 확인 (RN v0.85, Metro v0.84.2) ### 다음 작업 (이어서) - Metro 번들 에러 확인/수정 (화면 렌더링 테스트) - expo-av → react-native-audio-recorder-player 교체 (RecordScreen) - postinstall 스크립트로 Windows NDK 패치 자동화 - 커밋 후 apps/mobile-expo 정리 여부 결정 --- > 규칙 13: 작업 완료 즉시 이 파일 갱신 의무 ## Phase M-2: D3RO 디자인 시스템 모바일 적용 (2026-04-13) ✅ ### DS 컴포넌트 (packages/ui-native) - 기존 4개 + 신규 5개 = **총 9개**: MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel, WaveBars, AppStatusBar, Header, FilterChip - 테마 토큰 SSOT: d3roNativePalette / d3roNativeTypo / d3roNativeRadius / d3roNativeFonts - WaveBars: core Animated API 사용 (reanimated 의존성 제거) ### 탭 화면 D3RO 디자인 적용 (5개 + 로그인) - dash.tsx: Session Overview (ScreenPanel), 2x2 Stats Grid, Backend/Tier Info, Usage Panel - history.tsx: FilterChip (ALL/SAVED/PROC), 날짜 그룹핑, LED 상태, 단어수 배지 - record.tsx: WaveBars 애니메이션, 녹음 상태머신, 실시간 전사 UI - talk.tsx: 채팅 버블 (AI/User), 날짜 배지, 입력 영역 - settings.tsx: 섹션별 카드 (Account, Backend, Preferences) - login.tsx: LED 로고, OAuth 3종, 이메일/비번, D3RO 스타일 ### i18n 연결 - I18nProvider를 root layout에 연결 - 모바일 전용 키 80+개 추가 (ko.json, en.json) - 모든 UI 문자열 t() 함수 사용 ### 인프라 - apps/mobile을 모노레포 workspaces에 추가 - saasMode 분기 제거 커밋 완료 ### 알려진 이슈 - @types/react 버전 충돌 (desktop=19, mobile=18) → TS2786만 발생, Metro에 무영향 - react-native-screens bob build 실패 → --ignore-scripts로 해결, EAS Build에서는 정상 --- ## Admin 콘솔 리디자인 + 랜딩 SaaS 전환 (2026-04-13) ✅ ### Admin CRM 콘솔 스타일 리디자인 - 전체 페이지 D3RO Console 스타일 적용 (Overview, Users, Subscriptions, Audit Log + 모든 상세) - `console-theme.ts` 디자인 토큰 시스템 (C 색상, FONT, panelSx, tableSx, filterBtnSx, statusBadgeSx) - `globals.css` — 스캔라인 애니메이션, 그리드 패턴, 글로우 효과, 커스텀 스크롤바 - JetBrains Mono 폰트, 검정 배경, 라운드 패널, 오렌지 액센트 - MetalCard/PhosphorText → Box + sx 전환 (MUI는 유지, d3ro DS 컴포넌트 미사용) ### SSE 스트리밍 Premium LLM 연결 - VoiceConversationService에 Premium/Local LLM 라우팅 추가 - `_createChatStream()`: configGet('llmBackend') 기반 분기, Premium 실패 시 Local fallback - PremiumLLMService.chatStream() → Anthropic SSE → 토큰 단위 yield → TTS 파이프라인 ### 코드 정리 - **Supabase 클라이언트 공통화**: packages/api-client에 supabase-browser.ts, supabase-server.ts 추출 - Browser: 싱글톤 캐싱 + isSupabaseConfigured() 완전 공유 - Server: CookieStore 인터페이스 팩토리 패턴 (next/headers 의존성 분리) - apps/web, apps/admin → 3줄 re-export 래퍼로 축소 - **RPC 함수 타입 9개 추가**: packages/api-client/src/types.ts Functions 섹션 - generate_invite_token, user_team_ids, user_admin_team_ids, match_knowledge_chunks - consume_quota, increment_daily_usage, admin_usage_by_feature, admin_top_users, admin_dau ### 랜딩 페이지 SaaS 구독 모델 전환 - Pricing: 1회 결제 → 월간($9.9/$19.9) / 연간($99/$199) 토글 - 10개 언어 i18n 전체 동기화 (pricing, FAQ, CTA) - FAQ: 구독 관련 답변 업데이트 - CTA: "구독 없음" → "프리미엄 기능, 당신의 조건으로" - 다운로드 URL: placeholder (#) — 추후 실제 URL 제공 시 교체 --- ## V2-6.1: 3단계 권한 체계 (2026-04-12) ✅ ### 변경 내역 - **DB**: `profiles_role_check`에 `manager` 추가, RLS 정책 7개 재작성 (manager 읽기, admin 쓰기, manager 구독 UPDATE) - **Edge Functions 4개**: `requireManager`/`requireAdmin` 3단계 분기 적용 + 배포 완료 - **Frontend**: `admin-guard.ts` 3단계 가드, UI 컴포넌트 권한별 표시 분기 - **토큰 갱신 버그 수정**: `callAdminApi`에서 `getSession()` → `getUser()` 선행 호출 ### 권한 체계 | 기능 | manager | admin | super_admin | |------|---------|-------|-------------| | 조회 전체 | ✅ | ✅ | ✅ | | 구독 수정 + 메모 | ✅ | ✅ | ✅ | | 구독 생성 (VIP) | ❌ | ✅ | ✅ | | 구독 삭제 | ❌ | ✅ | ✅ | | role 변경 (user↔manager) | ❌ | ✅ | ✅ | | role 변경 (→admin/super_admin) | ❌ | ❌ | ✅ | --- ## 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 → `