d3ro-voice/memory/project_status.md
윤찬 816f527592 feat(desktop): SaaS [3] 사용자별 SQLite 격리 (빅뱅 Phase 1)
db/index.ts 전면 재설계:
- initDatabase/closeDatabase 폐기
- openForUser(userId), closeCurrent, getDatabase, getCurrentUserId 신 API
- 경로: userData/users/${userId}/d3ro.db
- applySchema() 추출 — 새 user DB마다 테이블 + ALTER 마이그레이션 재실행
- legacy d3ro-voice.db는 silent archive (archive-legacy-ISO.db로 rename만)

CloudSyncService 인증 lifecycle SSOT:
- _onAuthenticated(session, reason) 단일 진입점
  init() 세션 복원, handleAuthCallback() 신규 로그인 모두 수렴
  DB open 실패 시 세션 롤백 + sync-error emit
- _onSignOut() 안전 종료 시퀀스
  VoiceMode cancel → MeetingMode stopRecording → Caption stop →
  Realtime stop → auth.signOut → token clear →
  auth-changed(null) emit → closeCurrent
- VoiceMode/MeetingMode/Caption은 dynamic import로 순환 의존 회피

bootstrap.ts:
- database step 제거 (DB는 _onAuthenticated에서만 열림)
- cloud-sync step이 마지막 유지 (세션 복원 → DB open → auth-changed)
2026-04-11 09:58:00 +09:00

38 KiB

D3RO-VOICE 프로젝트 현황

마지막 갱신: 2026-04-11 (Mac 부트스트랩) 규칙 13: 작업 완료 즉시 이 파일 갱신 의무

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 <noreply@d3ro.dev>
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 주석처리)

주입 예:

cd server
SUPABASE_ACCESS_TOKEN=<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 → <audio controls>), /meetings/[id] AUDIO 카드
  • [D] Dashboard 14일 추이 차트 — recharts LineChart (회의/문서 수 일별 집계), 이번 주 stat 자동 계산
  • [E] 🐛 fix(desktop): macOS 단축키 표기 — format-hotkey.ts 신규 (플랫폼별 segment 변환). preload electronAPI.platform 노출. macOS는 ⌃ ⌥ ⇧ ⌘ (Apple HIG 순서) + 구분자 없음 (⌘⇧1), Win/Linux는 Ctrl + Shift + 1. SettingsModal/OnboardingModal/DashboardPage/HotkeyRecordModal 모두 binding.displayLabel 직접 참조 → formatHotkeyLabel/Segments(binding)로 교체. displayLabel split 버그(저장 시 '+' join, 표시 시 ' + ' split 불일치)도 함께 해소.
  • [F] 🐛 fix(desktop): Mac sidecar 부팅 — 시스템 python3에 numpy 등 deps 없어서 매번 ModuleNotFoundError. apps/desktop/sidecar/.venv/ 신규(gitignored), faster-whisper/fastapi/uvicorn/python-multipart/numpy 설치. paths.ts.getSidecarCommand가 venv python 우선 사용 (macOS .venv/bin/python3, Win .venv/Scripts/python.exe).
  • [G] 🐛 fix(desktop): HotkeyRecordModal 칩 'WIN' 표기 잔존 — getKeyName이 KEY_DISPLAY_MAP(Windows 전용 91/92→'Win') 사용. format-hotkey.tskeyCodeToName/getPlatform export → modal에서 호출. 'Shift + Win + 1 — 저장' 안내 텍스트도 mac에서 구분자 없이 결합.
  • [H] 🐛 fix(desktop): hideRecordingTip is not defined ReferenceError — bootstrap.ts의 WindowManager import 목록에 hideRecordingTip/updateRecordingTipState 누락. session-cancelled/error 핸들러에서 호출 → ReferenceError로 RecordingTip 팝업이 닫히지 않는 누적 버그. import 추가.
  • [I] 🐛 fix(desktop): 핫키 누를 때 macOS 시스템 beep — uiohook-napi는 글로벌 모니터링만 하고 OS swallow 안 함. 키가 받는 곳 없어 NSBeep 발생. HotkeyService에 Electron globalShortcut 통합. bindingToAccelerator(binding)이 Windows VK + modifier → Electron Accelerator string 변환 (mac은 Cmd, win은 Super). globalShortcut.register(accel, noop)로 OS swallow + uiohook은 그대로 hold/release 처리. HotkeyConfig.acceleratorString 필드 추가. unregister/stop에서 cleanup. 단일 modifier 키만 등록 시(Right Alt 단독 등) accelerator 변환 불가하면 globalShortcut 건너뛰고 uiohook만 사용.
  • [J] 🐛 fix(desktop): sidecar 동적 포트 + orphan 충돌 — dev mode HMR 재시작 시 이전 sidecar python이 orphan으로 18765 점유. 새 main이 spawn하면 EADDRINUSE로 즉시 exit. LocalSTTService._findFreePort(start, max=20) 추가 (net.createServer().listen(port, '127.0.0.1')로 free 포트 탐색), spawn 진입 시 this._port 동적 갱신. health/load/transcribe URL이 자동으로 새 포트 사용.
  • [K] 🐛 fix(desktop): macOS 텍스트 붙여넣기 — TextInsertService._insertViaClipboardnut.Key.LeftControl + V 하드코딩이라 macOS에서는 paste 안 됨 (Cmd+V여야). pasteModKey = process.platform === 'darwin' ? LeftSuper : LeftControl 분기. 잘못된 클립보드 검증("text 그대로면 정상") 로직 제거 (macOS는 Ctrl+V가 noop이라 항상 거짓 양성). macOS는 paste 후 sleep 250ms.
  • [L] 🐛 fix(desktop): qwen3 reasoning 39초 + 빈 응답 — qwen3:4b는 reasoning model이라 응답에 <think>...</think> 블록을 길게 출력, 본문이 비어 _completeSession("")로 paste 호출 안 됨. SYSTEM_PROMPTS 전체에 /no_think 토큰 추가 (qwen3 비활성화, 다른 모델은 무시). stripReasoningBlocks() 추가 — <think>/<thinking> 태그 안전망. processText가 stripped 결과 길이 0이면 원본 transcript fallback + warn 로그.

SaaS 데스크톱 클라이언트 1차 (2026-04-11)

사용자 표현: "설계가 잘못된 것 같다" 실제 상황: 백엔드/웹/모바일은 100% multi-tenant SaaS, 데스크톱만 V1 single-user 잔재로 사용자가 Settings에서 Supabase URL/Key를 직접 입력해야 하는 UX였음. Notion/Linear/Slack 데스크톱 패턴으로 정정.

SaaS [1] 빌드 타임 Supabase env 주입

  • electron.vite.config.ts: defineConfig(({ mode }) => ...)로 변경, loadEnv(mode, cwd, 'D3RO_').env.local 자동 로드 → main 번들에 define으로 inline 박음
  • apps/desktop/.env.local 생성 (gitignored): D3RO_SUPABASE_URL, D3RO_SUPABASE_ANON_KEY
  • apps/desktop/env.example 템플릿 (committed)
  • ConfigService:
    • BUILD_TIME_SUPABASE_URL / BUILD_TIME_SUPABASE_ANON_KEY 상수 (process.env에서 읽음)
    • isSupabaseBuildTimeConfigured() 헬퍼 export
    • configGet('supabaseUrl'/'supabaseAnonKey')은 빌드 타임 값 절대 우선
    • configSet는 빌드 타임 모드일 때 supabaseUrl/AnonKey 변경 거부

SaaS [2] OAuth 첫 실행 강제 게이트

  • CloudSyncStatesaasMode: boolean 필드 추가
  • LoginScreen.tsx 신규: Google/GitHub 버튼만, MetalCard + PhosphorText, URL/Key 입력 노출 0
  • App.tsx AuthGate 상태머신: loading | login-required | authenticated | legacy
    • saasMode && !authenticated → LoginScreen 강제
    • saasMode && authenticated → 기존 AppLayout
    • !saasMode → legacy (기존 동작 유지)
    • onAuthChanged 구독으로 로그인 후 자동 전환
  • CloudSyncSection (Settings 안):
    • saasMode일 때 URL/Key 입력 필드 완전 숨김 (OAuth 버튼만)
    • !saasMode (legacy)일 때만 기존 입력 화면 유지
  • 검증: 이전엔 항상 떴던 CloudSync disabled — Supabase URL/key not configured 로그 사라짐

SaaS [3] 사용자별 SQLite 격리 — 완료 (2026-04-11, 빅뱅 Phase 1)

  • apps/desktop/src/main/db/index.ts 전면 재설계
    • initDatabase() / closeDatabase() 폐기 (이름 차원에서 제거)
    • 새 API: openForUser(userId), closeCurrent(), getDatabase(), getCurrentUserId(), isDatabaseOpen(), getCurrentDbPath()
    • 경로: userData/users/${userId}/d3ro.db (userId path-separator/.. 가드)
    • applySchema(s)로 테이블 + 인라인 ALTER 마이그레이션 추출 — 새 DB 생성 시마다 재실행
    • 모듈 상태: currentUserId | null, sqlite, db — closeCurrent 후 모두 null
    • 같은 userId 재호출 시 no-op, 다른 userId면 먼저 close 후 재오픈
    • openForUser 실패 시 부분 상태 정리 + D3ROError(DBOpenFailed) throw
  • legacy DB 정책
    • V1 userData/d3ro-voice.db는 한 번도 정식 서비스 안 됨 → archiveLegacyDbIfExists()로 첫 openForUser 직전 silent rename (d3ro-voice.legacy-${ISO timestamp}.db)
    • 파일 삭제 없음, dialog 없음, 데이터 이전 없음 ("클린하게" 요청)
  • CloudSyncService 인증 lifecycle 단일 진입점 (SSOT)
    • _onAuthenticated(session, { reason: 'restore' | 'signin' }):
      1. this._session = session
      2. openForUser(userId) — 실패 시 세션 무효화 + auth-changed(null) + sync-error emit 후 return
      3. auth-changed(user) emit (DB가 열린 뒤에)
      4. startRealtime() 자동 시작
    • init() 세션 복원, handleAuthCallback(code) 신규 로그인 모두 이 함수로 수렴
    • _onSignOut():
      1. VoiceMode active → cancelSession() (녹음 진행 중이면 강제 취소)
      2. MeetingMode active → stopRecording() (await)
      3. Caption → stop() (await)
      4. stopRealtime()
      5. Supabase auth.signOut()
      6. _session = null + _clearStoredRefreshToken()
      7. auth-changed(null) emit — 렌더러 LoginScreen 복귀
      8. closeCurrent() — 이 시점 이후 getDatabase()는 throw
    • signOut() public 메서드는 _onSignOut() 위임
    • import 동적 변경: VoiceMode/MeetingMode/Caption 모두 dynamic import로 순환 의존 회피
  • bootstrap.ts 순서 변경
    • database step 완전 제거 (critical 2번째였던 자리 삭제)
    • cloud-sync step은 마지막 유지 — 세션 복원 성공 시 DB가 여기서 열림
    • initDatabase import 삭제
  • 검증
    • desktop tsc --noEmit
    • desktop npm run build (main bundle 997kB, vite 경고는 기존 동적 import 관련 pre-existing)
    • runtime (dev 띄워서 LoginScreen → OAuth → user DB 생성 확인) — 사용자 수동 검증 대기
  • 남은 작업 (Phase 1 follow-up)
    • 사용자 A → signOut → 사용자 B 로그인 시 A 데이터 안 보이는지 실측
    • ~/Library/Application Support/d3ro-voice/users/${uuid}/d3ro.db 파일 생성 확인
    • 기존 d3ro-voice.db가 있는 환경에서 archive rename 동작 확인

빅뱅 다음 세션 목표 (2026-04-11 wrap-up)

데스크톱을 진짜 SaaS 클라이언트로 완성. 자세한 계획 + 단계별 prompt는 memory/handoff-latest.md 참조.

핵심:

  1. 사용자별 SQLite 격리 (userData/users/${userId}/d3ro.db)
  2. 회원가입/온보딩 흐름 — OAuth 첫 가입 시 환영 + 기본 설정
  3. cloud-first 정책 — Supabase가 source of truth, 로컬 cache는 read-through
  4. 멀티 디바이스 sync 강화 (Realtime + last-write-wins → CRDT 검토)
  5. 결제/티어 enforcement (free 일일 quota, 데스크톱에서도 차단)
  6. 회의/지식 web ↔ 데스크톱 일원화

SaaS 아키텍처 진단 (2026-04-11)

사용자 우려: "single-user로 잘못 만들어진 것 같다" 결론: 잘못된 게 아님 — 100% multi-tenant SaaS로 이미 구축됨

레이어 상태
Supabase 스키마 모든 테이블에 user_id FK + RLS 활성화 (12 테이블)
RLS 정책 user_id = auth.uid() 격리 + 팀 공유 패턴 (205줄)
Auth 트리거 handle_new_user로 가입 시 profile + free subscription 자동 생성
Edge Functions 10개 전부 requireUser(req) JWT 검증
웹 앱 Supabase OAuth + 미들웨어 라우트 가드
모바일 앱 supabase-js + Expo Push 사용자별
결제 Stripe + subscriptions 테이블 multi-tenant
데스크톱 SaaS의 fat client (Notion/Linear/Slack 패턴) — OAuth 로그인 후 user별 sync

오해 가능 지점: 데스크톱 앱은 V1 single-user 출신이라 첫 실행 시 CloudSync가 disabled. Settings에서 Supabase URL/Key + OAuth 로그인을 해야 비로소 SaaS 클라이언트로 동작. 그 이전에는 로컬 SQLite만 쓰는 single-user처럼 보임. 이건 의도된 fat client 아키텍처지만 UX 개선 여지 있음:

향후 데스크톱 SaaS UX 개선 후보 (잘못된 게 아닌 향상)

  1. 첫 실행 시 OAuth 로그인을 강제 (Notion 패턴) — 게스트 모드 분리
  2. Supabase URL/Key를 빌드 타임 환경변수로 박기 — 사용자가 직접 입력하지 않게
  3. 로컬 SQLite를 userData/${user.id}/d3ro.db로 사용자별 격리 — PC 공유 시 데이터 혼동 방지

6차 검증: web typecheck/dev 200, production build 15 라우트 (dashboard 1.6kB→111kB, meetings/[id] 4.8kB→55kB by chart/mermaid)

다음 사이클 후보: VoiceAction 카탈로그 확대, RAG → /chat 연동, Knowledge 파일 업로드(PDF/docx), Voice Input UI(/actions 마이크), 알림 센터, 팀 대시보드, recharts dynamic import로 dashboard split, Playwright browsers install + E2E 실 실행

V1 완료 페이즈

Phase 내용 상태
1~9 기본 음성 어시스턴트 (STT/LLM/TTS/핫키/팝업)
10 Killer features (메모, 보이스 커맨드, 스크린 컨텍스트, 체인, 캡션)
11 Monetization (라이선스, 쿼터)
12 Pro features (파일 전사, 회의 요약, 딕테이션 템플릿)
13 음성 대화, RAG, 보이스 액션
14 회의 모드
14.5 회의 고도화 (다중 문서 생성/편집/내보내기)
15 Auto Polish + AI 채팅 + 마인드맵 + 공유
15.5 화자 구분 (LLM 추정 + pyannote)

추가 개선사항 (post-15.5)

  • 히스토리/회의 목록 반응형 그리드 + 카드 UI
  • 설정 LLM 모델 드롭다운
  • 오디오 테스트 실시간 레벨 미터
  • Ollama 미실행 시 설치 경로 탐색 후 자동 스폰 (2026-04-08)

V2 진행 상황

Phase V2-1: Monorepo + Core 추출 — 진행 중

Sub-phase 범위 상태
V2-1a npm workspaces + apps/desktop으로 V1 이동 완료
V2-1b packages/core 추출 (types, errors, ipc-channels, constants, utils) 완료
V2-1c packages/ui 추출 (DS 컴포넌트 + theme + theme-vars) 완료
V2-1d packages/i18n 추출 (locale JSON + 훅) 완료

V2-1d 완료 내역

  • packages/i18n/ (@d3ro/i18n) 신규 생성:
    • src/locales/ 12개 JSON (ko/en/ja/zh/zh-TW/es/fr/de/pt/ru/vi/th)
    • src/index.tsx: TFunction, Locale, LOCALE_META, resolveTranslation, createFormatDate/Number/RelativeDate/Time, I18nContext, useI18n, I18nProvider
  • Electron 독립성 확보: I18nStorage interface 추가 (load/save 어댑터)
    • 기존 window.electronAPI.config 직접 호출 → storage prop 주입 패턴
    • 기본값은 noopStorage (메모리 한정)
  • apps/desktop/src/renderer/App.tsxelectronI18nStorage 어댑터 구현 후 <I18nProvider storage={electronI18nStorage}>로 주입
  • apps/desktop 설정:
    • @d3ro/i18n: "*" dep 추가
    • tsconfig paths, vite alias, externalize exclude에 @d3ro/i18n 추가
  • 일괄 치환: ./i18n, ../i18n, ../../i18n@d3ro/i18n (29 파일)
  • apps/desktop/src/renderer/i18n/ 디렉토리 완전 제거

V2-1d 검증

  • typecheck
  • build
  • dev 런타임 (DB/핫키/Ollama/Main window 모두 정상)
  • Electron 의존이 완전히 apps/desktop에만 남아 monorepo 원칙 준수 (packages/i18n은 React에만 의존, 웹/모바일에서 그대로 재사용 가능)

V2-1c 완료 내역

  • packages/ui/ (@d3ro/ui) 신규 생성:
    • src/theme.ts (d3roPalette/d3roTypo/d3roShadow SSOT 전체)
    • src/theme-vars.ts (팝업/main 프로세스용 CSS 변수 맵)
    • src/components/ds/{CrtDisplay, InstrumentPanel, Led, MetalCard, MetalDial, PhosphorText, PhysicalButton, ScreenPanel, ButtonGroup, index}.tsx
    • src/index.ts barrel
  • packages/ui/package.json: React/MUI/Emotion은 peerDependencies, @d3ro/core만 직접 dep
  • apps/desktop/src/shared/ 디렉토리 완전 제거 (theme-vars가 마지막이었음)
  • subpath exports: ./theme, ./theme-vars, ./components/ds
  • DS 컴포넌트 내부 ../../theme 상대 경로는 그대로 유효 (packages/ui 내부에서 해결)
  • 일괄 치환:
    • ../theme, ../../theme, ./theme@d3ro/ui/theme
    • ../components/ds, ../../components/ds, ./ds, ../ds@d3ro/ui/components/ds
    • ../ds/<Component>, ../../ds/<Component>@d3ro/ui/components/ds (세부 파일 import는 barrel로 통합)
    • @shared/theme-vars@d3ro/ui/theme-vars (WindowManager 1건)
  • apps/desktop/tsconfig.node/web.json: @shared/* paths 완전 제거, @d3ro/ui, @d3ro/ui/* paths 추가, include에서 src/shared/**/* 제거
  • electron.vite.config.ts: @shared alias 제거, @d3ro/ui alias 추가, externalize exclude에 @d3ro/ui 추가
  • vitest.config.ts: alias 교체

V2-1c 검증

  • typecheck
  • build
  • dev 런타임 (DB/핫키/Ollama/Main window 정상)
  • apps/desktop/src/shared/ 디렉토리 자체가 사라져 @shared/*는 이제 프로젝트에 존재하지 않음

V2-1b 완료 내역

  • packages/core/ 패키지 신규: @d3ro/core, subpath exports (types/errors/ipc-channels/constants/utils/*), docx dep 포함
  • 6개 파일 git mvpackages/core/src/로 이동:
    • apps/desktop/src/shared/{types,errors,ipc-channels,constants}.ts
    • apps/desktop/src/main/utils/{meeting-markdown,markdown-to-docx}.ts
  • packages/core/src/index.ts barrel export 추가
  • apps/desktop/package.json"@d3ro/core": "*" dep 추가
  • apps/desktop/tsconfig.node.json, tsconfig.web.jsonpaths@d3ro/core/* 추가, @shared/*는 theme-vars용으로 유지
  • apps/desktop/electron.vite.config.ts alias 3개 섹션에 @d3ro/core 추가 + M1 수정: resolve('src/shared')resolve(__dirname, 'src/shared')
  • externalizeDepsPlugin exclude에 @d3ro/core 추가 (main/preload 둘 다)
  • apps/desktop/vitest.config.ts@d3ro/core alias 추가
  • 일괄 치환: @shared/{types,errors,ipc-channels,constants}@d3ro/core/* (사용자 79개 파일, 167건)
    • static import (from '@shared/...')
    • dynamic import (import('@shared/...'))
    • type expression import (keyof import('@shared/...').AppConfig)
  • MeetingModeService.ts의 상대 경로 utils import (../utils/meeting-markdown, ../utils/markdown-to-docx)를 @d3ro/core/utils/*로 교체
  • packages/core 내부 자기 참조: meeting-markdown.ts@shared/types../types 상대 경로

V2-1b 검증

  • npm run typecheck
  • npm run build (main+preload+renderer)
  • npm run dev 런타임 실행 (DB/핫키/Ollama 모두 정상)
  • apps/desktop/src/shared/에는 theme-vars.ts 1개만 남음 (V2-1c에서 이동 예정)

V2-1a 완료 내역

  • 루트 package.json을 npm workspaces 루트로 재구성 (workspaces: apps/, packages/)
  • turbo.json, tsconfig.base.json 추가 (Turborepo 자체 설치는 뒤로 미룸)
  • V1 파일 전체를 apps/desktop/으로 git mv:
    • src/, resources/, tests/, sidecar/, scripts/
    • electron.vite.config.ts, electron-builder.yml, vitest.config.ts
    • tsconfig.node.json, tsconfig.web.json (+ 새 tsconfig.json)
  • apps/desktop/package.json 신규 (name: "@d3ro/desktop", 기존 deps 그대로)
  • @electron/rebuild --version=33.4.11로 native 모듈 재빌드 (better-sqlite3, uiohook-napi)
  • 루트 tsconfig.json → apps/desktop references 형태

V2-1a 결정사항

  • 패키지 매니저: npm workspaces 유지 (pnpm 전환은 별도 sub-phase)
    • 이유: Turborepo는 npm workspaces와 호환, 구조 이동과 PM 전환을 동시에 하면 롤백 불가
  • site/: 루트 유지 (랜딩 페이지, V2 무관)
  • docs/, memory/, CLAUDE.md: 루트 유지
  • productName: "d3ro-voice" 필수 — package name을 @d3ro/desktop으로 바꾸면 Electron의 app.getName()이 바뀌어 userData 경로가 %APPDATA%\@d3ro\desktop\으로 이동(기존 DB/설정 유실). productName을 명시하면 app.getName()이 이를 우선 반환하여 %APPDATA%\d3ro-voice\ 그대로 유지됨.

검증 결과

  • npm run typecheck
  • npm run build (electron-vite main+preload+renderer)
  • npm run dev 실제 실행 → DB 경로 d3ro-voice/로 복원, 기존 핫키(Alt keyCode=2) 로드, Ollama 자동 실행 감지 모두 정상
  • npm run dist (electron-builder installer)은 V2-1 완료 후 별도 검증

Phase V2-2: Supabase 인프라 — 로컬 작업 완료

작성된 파일 (server/supabase/)

  • config.toml — Supabase CLI 설정 (로컬 + OAuth providers)
  • migrations/20260409000001_initial_schema.sql — 12개 테이블 (profiles, teams, team_members, meetings, meeting_memos, meeting_documents, transcripts, history, dictionary, memo_tags, daily_usage, subscriptions) + 인덱스 + FK CASCADE
  • migrations/20260409000002_rls_policies.sql — 개인/팀 이중 RLS 패턴
  • migrations/20260409000003_auth_triggers.sqlhandle_new_user (profiles + subscriptions 자동 생성), moddatetime (8개 테이블 updated_at 트리거), increment_daily_usage (service_role 전용 쿼터 RPC)
  • migrations/20260409000004_storage_buckets.sql — audio/exports/avatars 버킷 + 경로 기반 정책
  • functions/_shared/ — cors.ts, auth.ts (requireUser), quota.ts (티어별 쿼터)
  • functions/stt-proxy/index.ts — Google Cloud STT 래퍼 스캐폴딩 (placeholder 응답)
  • functions/llm-proxy/index.ts — Anthropic Messages API 래퍼 스캐폴딩 (티어별 모델 정책)
  • seed.sql — 개발 시드 (빈 상태)
  • .gitignore — .branches/, .temp/, .env

설계 문서

  • docs/v2/phase-V2-2.md — 상세 설계 (스키마/RLS/Edge Functions/실시간/Storage)
  • docs/v2/phase-V2-2-setup.md — 사용자 액션 가이드 (Supabase 계정/OAuth/CLI/배포)

사용자 액션 대기 중 (로컬 외 작업)

  1. Supabase 프로젝트 생성 (https://supabase.com)
  2. supabase login && supabase link --project-ref <REF>
  3. supabase db push (4개 마이그레이션 적용)
  4. Google/GitHub OAuth 앱 등록 + Supabase Auth Providers 설정
  5. Google Cloud STT 키 + Anthropic API 키 발급
  6. supabase secrets set + supabase functions deploy

설계 결정사항

  • UUID PK: V1은 nanoid(text), V2는 PostgreSQL gen_random_uuid(). V2-4 (동기화)에서 기존 SQLite 데이터는 nanoid 유지, 새 데이터는 UUID.
  • timestamptz: V1은 epoch ms(integer), V2는 timestamptz. V2-4 동기화 시 타입 변환 필요.
  • 멀티테넌시: 모든 테이블에 user_id. 회의 관련 테이블은 team_id nullable 추가.
  • RLS 이중 패턴: 개인 전용(history/dictionary)은 단순 user_id = auth.uid(). 팀 공유(meetings)는 본인 OR team_members 조회 subquery.
  • 쿼터 집계: increment_daily_usage RPC는 SECURITY DEFINER + service_role only. 일반 유저는 읽기만.
  • Realtime: transcripts 테이블만 supabase_realtime publication에 추가 (회의 중 세그먼트 동기화 용).
  • Edge Functions placeholder: 인증/쿼터 파이프라인은 완성, 실제 AI API 호출은 주석 처리된 실제 구현 코드 포함. V2-3 (Web MVP) 진행 전까지 유지.

Phase V2-5: Mac 빌드 — 로컬 작업 완료

작성/수정된 파일

  • apps/desktop/src/main/utils/paths.tsEXE_SUFFIX 상수 도입, sox/sidecar/ffmpeg 경로 플랫폼별 분기
  • apps/desktop/src/main/services/SoundEffectService.ts — Mac afplay, Linux aplay 분기 추가 (execFile로 안전하게)
  • apps/desktop/src/main/services/ScreenContextService.ts_getActiveWindowInfo를 win32/darwin로 분기. macOS는 osascript로 frontmost process + 윈도우 타이틀 조회 (Accessibility 권한 필요)
  • apps/desktop/scripts/build-sidecar.pyIS_WINDOWS/IS_MACOS 분기, EXE_SUFFIX, --noconsole은 Windows에서만, 출력 경로 플랫폼 통합
  • apps/desktop/scripts/install-sox.sh — Mac/Linux에서 시스템 sox를 resources/sox/로 복사. macOS는 otool로 dylib 의존성 함께 복사 + install_name_tool로 rpath 변경
  • apps/desktop/electron-builder.ymlmac: 타겟 추가 (dmg+zip, arm64+x64), hardenedRuntime, entitlements, extendInfo(권한 메시지), Linux AppImage placeholder
  • apps/desktop/build/entitlements.mac.plist — 마이크/카메라/네트워크/Apple Events/JIT/dylib 권한
  • apps/desktop/resources/icons/README.md — 플랫폼별 아이콘 생성 가이드 (sips/iconutil/imagemagick)
  • .github/workflows/build-mac.yml — macos-14 runner, brew sox, sidecar 빌드, electron-builder dist (arm64/x64 매트릭스), CSC/notarize 환경변수, artifact 업로드
  • docs/v2/phase-V2-5-mac-guide.md — Mac 사전 조건/dev 실행/dist 빌드/서명/공증/CI/트러블슈팅

검증 결과 (Windows에서)

  • typecheck — Mac 분기 추가에도 회귀 없음
  • build — Windows 번들 정상
  • dev 런타임 — Ollama 자동 실행 + 모든 서비스 정상

Mac에서 검증 대기 (사용자 액션)

  • brew install sox node@22 python@3.11
  • npm install && npx @electron/rebuild@3 --version=33.4.11
  • npm run dev → Accessibility/마이크/Apple Events 권한 허용
  • 핫키, 녹음, STT, LLM, 화면 컨텍스트 동작 확인
  • 결과 리포트 받아 미세 조정 필요할 수 있음

감사 결과 vs 처리

  • C1 sox.exe/sidecar.exe/ffmpeg.exe 하드코딩 → 수정
  • C2 아이콘 부재 → placeholder 디렉토리 + README
  • C3 mac 타겟 부재 → 추가
  • C4 SoX 바이너리 부재 → install-sox.sh + 가이드
  • C5 ScreenContextService 미지원 → AppleScript 분기
  • C6 SoundEffectService 미지원 → afplay 분기
  • C7 build-sidecar.py Windows 전제 → 크로스 플랫폼
  • M6/M7/M8/M9 (이미 분기 완료) → 변경 없음

Phase V2-3: Web App MVP — 완료

신규 패키지

  • packages/api-client/ (@d3ro/api-client) — Supabase 래퍼
    • src/types.ts — V2-2 스키마에 맞춘 수동 Database 타입 정의 (profiles/teams/meetings/transcripts/history/dictionary/daily_usage/subscriptions)
    • src/client.ts — createD3roSupabaseClient 팩토리 (URL/KEY 주입형, env 없어도 placeholder로 작동)
    • src/auth.ts — signInWithOAuth/signOut/getSession
    • src/meetings.ts — CRUD 함수 (meetings, memos, documents, transcripts)
    • src/history.ts — 음성 입력 이력 CRUD
    • src/usage.ts — daily_usage 조회 (쿼터 UI)
    • 루트 barrel은 types만 re-export (함수는 subpath로, 순환 의존 방지)

신규 앱 — apps/web/

  • Next.js 15 App Router + MUI 7 + Emotion
  • transpilePackages로 @d3ro/core/ui/i18n/api-client 소스 TypeScript 그대로 번들
  • 라우트 (총 9개):
    • / — auth 상태에 따라 리다이렉트
    • /login — Google/GitHub OAuth (Supabase 미설정 시 경고)
    • /auth/callback — OAuth code → session 교환 route handler
    • /dashboard — 요약 카드 4개 + 최근 회의 5건
    • /meetings — 회의 카드 그리드 리스트
    • /meetings/[id] — 상세 (transcripts + memos + documents 탭)
    • /record — 녹음 페이지 (getUserMedia + MediaRecorder + stt-proxy)
  • Providers: ThemeProvider(MUI+AppRouterCacheProvider) / I18nProvider(localStorage 어댑터) / AuthProvider(Supabase session)
  • 인증 가드: /dashboard layout + /meetings, /record, /meetings/[id] 개별 redirect
  • Sidebar: Dashboard / Meetings / Record / Logout
  • Recorder: 9바 웨이브폼 (Speakly 스타일), 실시간 레벨, elapsed timer, cancel/stop

packages/ui 확장 (@d3ro/ui)

  • MetalCard: BoxProps 확장하여 sx/onClick 등 전달 가능
  • theme.tstypoSx(key) 헬퍼 추가 — d3roTypo를 MUI sx 친화 포맷으로 변환
  • theme.tsd3roPalette.tag.blue 추가 (M5 수정의 부산물)
  • DS 컴포넌트 9개에 'use client' directive 추가 — RSC 경계 명시
  • MetalDial.tsx의 미사용 import 3개 제거 (tsc strict 회귀)

packages/i18n 확장

  • ko.json에 7개 새 키: nav.meetings, nav.record, nav.logout, login.subtitle, login.google, login.github, login.terms
  • 다른 locale은 fallback 체인으로 ko/en 사용 (추후 번역 작업에서 채움)

설계 결정사항

  • Database 제네릭: 현재 SupabaseClient 기본 타입 사용, V2-4에서 supabase gen types typescript --local 자동화 후 제네릭 주입 예정. 이유: 수동 Database 타입 정의가 @supabase/supabase-js 2.102의 복잡한 제약(PostgrestVersion, Views/Functions/Enums 형식)을 만족시키기 어려움.
  • api-client barrel 분리: 루트에서는 types만 export. 함수는 subpath(@d3ro/api-client/meetings)로만 접근. 이렇게 하지 않으면 apps/web이 api-client 루트를 import할 때 내부 meetings/history 등이 모두 tsc 스캔 대상이 되어 타입 에러가 전파됨.
  • DS 컴포넌트 'use client': MUI + React 훅(CrtDisplay)을 쓰는 컴포넌트들은 Next.js RSC에서 쓰려면 client directive 필요. barrel 대신 각 파일마다 붙임.
  • Supabase env 없이도 빌드 성공: placeholder URL/key로 createBrowserClient/createServerClient를 호출. 런타임에 isSupabaseConfigured() 체크로 UI 경고 표시.

검증 결과

  • web typecheck (apps/web)
  • desktop typecheck (회귀 없음)
  • web next build — 9 라우트 (/ /login /auth/callback /dashboard /meetings /meetings/[id] /record /_not-found + root)
  • desktop npm run build (main+preload+renderer)
  • 실제 Supabase 연결은 V2-2 사용자 액션 이후 검증 가능

Phase V2-4: 데스크톱 ↔ Supabase 동기화 — 완료

핵심 컴포넌트

  • apps/desktop/src/main/services/CloudSyncService.ts — 싱글톤 + EventEmitter
    • init/startSignIn/handleAuthCallback/signOut/pushAll + 4종 이벤트
    • safeStorage 기반 refresh token 영속화
    • upsert(onConflict: id) 방식 push, 매핑 테이블 없음
  • ipc/cloud-sync-handlers.ts + preload index.ts cloudSync API
  • renderer/components/CloudSyncSection.tsx — Settings 섹션
  • main/index.ts deep link (d3ro-voice://auth-callback) 핸들러 추가
  • bootstrap.ts cloud-sync 단계 추가
  • packages/core/src/ipc-channels.ts CLOUD_SYNC 11종 채널
  • packages/core/types.ts AppConfig + ConfigService defaults에 supabaseUrl/supabaseAnonKey/cloudSyncLastAt + hfToken/diarizationEnabled (Phase 15.5 누락분 보충)

설계 결정

  • Local-first push only (V2-4b에서 pull 추가)
  • 마지막 동기화 epoch ms 기준 변경분만 전송
  • nanoid PK를 PostgreSQL text로 그대로 upsert
  • macOS open-url + Windows second-instance argv 방식 deep link

Phase V2-6: Mobile MVP — 완료

구조 (apps/mobile/, npm workspace에서 제외)

  • Expo SDK 51 + React Native + Expo Router
  • lib/supabase.ts (AsyncStorage), lib/auth-context.tsx
  • app/_layout.tsx, app/index.tsx, app/login.tsx
  • app/(tabs)/{_layout, meetings, record, profile}.tsx
  • expo-av로 녹음 → stt-proxy 호출
  • README.md + setup 가이드

설계 결정

  • workspace 제외 (Expo native deps 부담 회피)
  • MUI 미사용, 인라인 RN 스타일 (V2-6b에서 @d3ro/ui-native 분리)
  • Deep link scheme: 'd3ro-voice'

Phase V2-7: 팀 기능 (Web) — 완료

  • apps/web/src/app/teams/page.tsx — 가입한 팀 카드 그리드
  • apps/web/src/app/teams/[id]/page.tsx — 팀 상세 (멤버 + 공유 회의)
  • components/teams/create-team-form.tsx — 팀 생성 + owner 자동 등록
  • components/teams/invite-member-form.tsx — user_id 직접 입력 (V2-7b에서 정식 invite flow)
  • Sidebar에 Teams/Billing 메뉴 + ko.json 새 키
  • V2-2의 teams/team_members RLS 활용

Phase V2-8: 결제 스캐폴딩 — 완료

  • apps/web/src/app/billing/page.tsx — Free/Pro/Team 가격표 + 현재 티어
  • components/billing/checkout-button.tsx — Edge Function 호출
  • server/supabase/functions/stripe-checkout/index.ts — JWT 인증, customer 조회/생성, Checkout Session
  • server/supabase/functions/stripe-webhook/index.ts — checkout.session.completed, subscription.* 이벤트 → subscriptions 업데이트
  • config.toml에 두 함수 등록 (webhook verify_jwt=false)

사용자 액션 필요

  • STRIPE_SECRET_KEY / STRIPE_PRICE_PRO / STRIPE_PRICE_TEAM / STRIPE_WEBHOOK_SECRET 설정
  • Stripe 대시보드에서 product + price 생성

미구현 (V2-8b)

  • Webhook signature 정식 검증 (현재 placeholder)
  • Customer Portal
  • Per-seat billing

알려진 차단/이슈

  • @nut-tree-fork/nut-js 포크 사용 (V1 잔여 이슈)
  • Ollama 의존성: 없으면 LLM 기능 사용 불가 (2026-04-08 자동 실행 기능 추가로 UX 개선)

디렉토리 구조 (V2-1a 목표)

d3ro-voice/                   # 루트 (monorepo)
├── apps/
│   └── desktop/              # 기존 V1 전체
│       ├── src/
│       ├── resources/
│       ├── build/
│       ├── package.json      # name: @d3ro/desktop
│       ├── electron.vite.config.ts
│       ├── electron-builder.yml
│       ├── tsconfig*.json
│       └── drizzle.config.ts
├── packages/                 # V2-1b/c/d에서 채움
├── site/                     # 랜딩 페이지 (루트 유지)
├── docs/                     # 설계서/페이즈 문서
├── memory/                   # 이 파일
├── pnpm-workspace.yaml       # 또는 package.json "workspaces"
├── turbo.json
├── package.json              # 루트 workspace
├── tsconfig.base.json
└── CLAUDE.md