d3ro-voice/memory/project_status.md

500 lines
32 KiB
Markdown

# 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 주석처리) |
주입 예:
```bash
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.ts``keyCodeToName/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이 자동으로 새 포트 사용.
## 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.tsx``electronI18nStorage` 어댑터 구현 후 `<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 mv``packages/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.json``paths``@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.sql``handle_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.ts``EXE_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.py``IS_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.yml``mac:` 타겟 추가 (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.ts``typoSx(key)` 헬퍼 추가 — d3roTypo를 MUI sx 친화 포맷으로 변환
- `theme.ts``d3roPalette.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
```