Commit graph

64 commits

Author SHA1 Message Date
윤찬
9498380f7f fix(desktop+supabase): OAuth 로그인 완주 + RLS 재귀 픽스 (빅뱅 Phase 5 Part 1)
빅뱅 Phase 5 실증 중 발견한 3개 버그 해결:

1. implicit flow 응답 무시:
   - supabase-js가 persistSession:false 일 때 PKCE code_verifier 저장 못해
     implicit flow로 fallback, fragment(#access_token=...&refresh_token=...)로
     토큰 전달. handleDeepLink는 ?code=만 파싱하고 있어 무시됨.
   - handleDeepLink에 fragment parser 추가 (access_token + refresh_token)
   - CloudSyncService.handleAuthTokens() 신규 — setSession() 후
     기존 _onAuthenticated SSOT로 수렴
   - 에러 query/fragment(error, error_description) 상세 로깅
   - 토큰 값은 로그에 안 찍히도록 (query=yes/no, fragment=yes/no 만)

2. Supabase RLS 무한 재귀:
   - team_members_read_same_team 정책이 자기 테이블 재조회 →
     Postgres RLS 엔진 무한 재귀 탐지 에러
   - meetings / meeting_memos / meeting_documents / team_invites 정책이
     team_members 서브쿼리 경유해서 전부 같이 터짐 + Realtime TIMED_OUT
   - migration 20260411000001: SECURITY DEFINER 함수 2개
     (user_team_ids, user_admin_team_ids) 신규 — Supabase 권장 패턴
   - 영향 정책: team_members(4) + meetings(2) + meeting_memos(1) +
     meeting_documents(2) + team_invites(1) 전부 함수 기반으로 재작성
   - supabase db push 완료

3. LoginScreen stale 번들:
   - Phase 1.5에서 import 제거했는데도 vite HMR/cache 어딘가에서
     stale state 유지해서 렌더러에 계속 뜸
   - LoginScreen.tsx 파일 자체 삭제 (vite 컴파일 대상 제거)

로그인 성공 확인: yunchan8804@gmail.com 으로 Google OAuth 완주 →
users/7da3dd02-9f2f-4ee9-a9b3-1c2c24875a93/d3ro.db 생성 → Initial sync 시작.
2026-04-11 11:55:42 +09:00
윤찬
ceadf1b6f3 feat(desktop): SaaS [6] 로컬 우선 sync + login_required UI (빅뱅 Phase 3)
Phase 3 재정의: Supabase = source of truth 방향 철회.
로컬 entry point 철학 하에 "로컬이 SoT, 클라우드는 로그인 시 mirror"로 확정.

CloudSyncService._initialSync():
- _onAuthenticated 끝에 fire-and-forget 호출
- pushAll → pullAll 순차 실행
- signin: 익명 로컬 데이터를 사용자 계정으로 업로드
- restore: 다른 기기 변경 반영
- 실패 시 warn만, 수동 Sync 버튼으로 재시도 가능

types.ts:
- FeatureAccess.reason: 'login_required' 추가
- UpgradePromptEvent.reason: 'login_required' 추가

LicenseService:
- consumeQuota(): login_required 케이스 — promptUpgrade + D3ROError(TierRequired)
- promptUpgrade(): login_required 시 requiredTier='free'
  (로그인만 하면 free로 바로 사용 가능)
- quota_exceeded 시 현재 tier 상위로 승격 제안

UpgradePromptModal:
- isLoginRequired 분기 — title/desc 교체
- Primary button: "로그인하기" → d3ro:open-settings event (tab=cloud)

i18n ko/en:
- license.loginRequired.title/desc/signIn 추가
2026-04-11 10:35:17 +09:00
윤찬
f1321df83c feat(desktop): SaaS [4] 온보딩 — Cloud Sync 티저 step + AppConfig 타입 정리 (빅뱅 Phase 2)
AppConfig 정식화:
- packages/core types.ts: onboardingCompleted: boolean 추가
- ConfigService CONFIG_DEFAULTS 동기화
- 타입 캐스팅 3곳 제거 (AppLayout/OnboardingModal/SettingsModal)

OnboardingModal 6 step 확장:
- 기존: 환영 → 마이크 → 핫키 → Ollama → 완료
- 신규: 환영 → 마이크 → 핫키 → Ollama → Cloud Sync(선택) → 완료
- saasMode === true 일 때만 Cloud Sync step 노출
- Step 내용: tagline + 3 benefit (sync/premium LLM/team share)
  + "설정에서 언제든지 로그인" 안내
- 온보딩 scope 안에서는 로그인 버튼 미노출 — 부담 제로 유지
  (Settings의 CloudSyncSection이 유일한 OAuth 진입점)

i18n:
- ko/en에 onboarding.cloud.* 6개 키 추가
- 나머지 10개 locale은 추후 일괄 (Phase 2 scope 밖)

의도적 제외:
- Supabase profiles.onboarded_at — OAuth provider 설정 전까진 의미 없음
- OAuth 후 환영 페이지 — Phase 4 티어 enforcement와 함께
- STT 모델 선택 — sidecar가 자동 다운로드
2026-04-11 10:16:06 +09:00
윤찬
1a01cea33c feat(desktop): SaaS [3.5] 로컬 모드 = entry point (빅뱅 Phase 1.5)
비즈니스 모델 수정:
- 무료 로컬 모드 = 사용자 onboarding funnel 입구 (킬러 피처, 회원가입 0)
- 유료 클라우드 SaaS = 수익화 (OAuth 로그인 + pro + premium 모델 + sync)
- 이전 세션 SaaS [2] OAuth 강제 게이트 철회

db/index.ts:
- LOCAL_USER_ID = '_local' 상수 export
- openLocal() 헬퍼 — userData/users/_local/d3ro.db
- isLocalMode() 헬퍼

bootstrap.ts:
- database step 복원 (critical). openLocal() 호출로 앱 시작 시
  로컬 DB 자동 오픈 — 로그인 없이 즉시 메인 UI 진입 가능

CloudSyncService._onSignOut():
- closeCurrent() 후 openLocal() 호출 — 로그아웃 시 로컬 모드 DB로 복귀
- 앱은 계속 로컬 모드로 동작 (entry point 철학)

App.tsx:
- AuthGate 상태머신 제거 (loading/login-required/authenticated/legacy)
- LoginScreen import 제거, 항상 AppLayout 렌더
- LoginScreen.tsx 파일은 유지 (추후 Welcome 화면 재활용 가능)

검증:
- desktop tsc --noEmit ✅
- desktop build ✅
- dev 런타임: [bootstrap] local database opened → Main window shown
- users/_local/d3ro.db 파일 생성 확인
2026-04-11 10:08:35 +09:00
윤찬
dd1c3054dd feat(desktop): SaaS [1+2] 빌드 타임 env + OAuth 강제 로그인 게이트
배경:
사용자가 데스크톱 앱 Settings에서 직접 Supabase URL/Key를 입력해야 했음.
SaaS 데스크톱(Notion/Linear/Slack) 패턴에서는 사용자가 OAuth 로그인만 하면 끝이고
연결 정보는 빌드 타임에 박혀있어야 함. V1 single-user 잔재로 인한 UX 결함이었고
사용자가 'SaaS 설계가 잘못된 것 같다'고 우려할 만한 명확한 흠집.

[1] 빌드 타임 Supabase env 주입
- electron.vite.config.ts: defineConfig가 mode 받아 loadEnv('D3RO_') 호출
  - .env, .env.local 자동 로드 (vite/electron-vite 표준)
  - main 번들에 process.env.D3RO_SUPABASE_URL/ANON_KEY를 inline define으로 박음
- ConfigService:
  - BUILD_TIME_SUPABASE_URL / BUILD_TIME_SUPABASE_ANON_KEY 상수
  - isSupabaseBuildTimeConfigured() / getBuildTimeSupabase{Url,AnonKey}() 헬퍼
  - configGet('supabaseUrl'/'supabaseAnonKey')는 빌드 타임 값을 절대 우선
  - configSet은 빌드 타임 모드일 때 supabaseUrl/AnonKey 변경 거부 (덮어쓰기 방지)
- apps/desktop/.env.local 생성 (gitignored, llnocwyqvhgwpdjcqqyw 프로젝트)
- apps/desktop/env.example 템플릿 (committed)

[2] OAuth 첫 실행 강제 게이트
- CloudSyncState에 saasMode: boolean 필드 추가 (CloudSyncService.getState)
- LoginScreen.tsx 신규: Google/GitHub 버튼만 노출 (URL/Key 입력 필드 없음)
  - MetalCard + PhosphorText D3RO 디자인 시스템
  - i18n 키 재사용 (login.subtitle/google/github/terms)
- App.tsx에 AuthGate 추가:
  - cloudSync.getState로 saasMode + authenticated 조회
  - status: 'loading' | 'login-required' | 'authenticated' | 'legacy'
  - saasMode이고 인증 안 됨 → LoginScreen 강제
  - saasMode이고 인증 됨 → 기존 AppLayout
  - saasMode 아님 → 'legacy' (기존 동작 유지, 개발자가 .env.local 설정 안 한 경우)
  - onAuthChanged 구독으로 로그인 직후 자동 전환
- CloudSyncSection (Settings 안):
  - saasMode일 때 URL/Key 입력 필드 완전 숨김 (OAuth 버튼만)
  - !saasMode (legacy)일 때만 기존 입력 화면 유지

검증:
- typecheck OK
- dev 재시작 시 'CloudSync disabled — Supabase URL/key not configured' 로그 사라짐
  → 빌드 타임 env가 정상적으로 ConfigService에 주입되어 client 생성됨
- 첫 실행 시 사용자가 보는 화면 = LoginScreen (Google/GitHub만)
2026-04-11 09:38:12 +09:00
윤찬
29c24a1520 fix(desktop): Mac sidecar 부팅 + 단축키 녹화 모달 ⌘ 표기
문제 1: STT release 안 됨 → 진짜 원인은 sidecar import 실패
- 시스템 python3 (Python 3.13)에 numpy/faster-whisper 등 sidecar deps 없음
- 매번 spawn 직후 ModuleNotFoundError로 즉사 → /health 30s 타임아웃
- 사용자에게는 '핫키 release 안 됨'으로 보이지만, 실제로는 핫키는 정상,
  STT 응답이 없어 결과 팝업이 안 뜬 것

수정:
- apps/desktop/sidecar/.venv/ 신규 (gitignored)
  - Python 3.13.13 venv
  - 최소 deps: faster-whisper 1.2.1, fastapi 0.135, uvicorn 0.44, python-multipart, numpy 2.4
  - torch/pyannote는 lazy import이므로 화자 구분 쓸 때 추가 설치
- main/utils/paths.ts: getSidecarCommand가 venv python 우선 사용
  - existsSync(.venv/bin/python3) → 그걸로 spawn
  - 없으면 시스템 python3 폴백 (기존 동작)
  - Windows는 .venv/Scripts/python.exe

문제 2: HotkeyRecordModal 칩에 'WIN' 그대로 표기
- getKeyName()이 KEY_DISPLAY_MAP (Windows 전용 매핑) 사용
  - 91/92 → 'Win' 하드코딩
- format-hotkey.ts의 keyCodeToName/getPlatform export
- HotkeyRecordModal.getKeyName이 keyCodeToName(platform) 호출
  → macOS는 ⌘/⇧/⌃/⌥
- 'Shift + Win + 1 — 저장을 눌러주세요' 안내 텍스트도 macOS는 구분자 없이 결합
2026-04-11 09:11:07 +09:00
윤찬
0d17b71866 fix(desktop): macOS 단축키 표기 (Win/Ctrl/Alt → ⌘/⌃/⌥/⇧)
기존: HotkeyRecordModal에서 키 녹화 시 'Win+Shift+1' 같은 정적 displayLabel 저장
- KEY_DISPLAY_MAP이 Windows 전용 (91/92 → 'Win')
- SettingsModal HotkeyDisplay가 binding.displayLabel.split(' + ')로 chip 분리
  - 그런데 displayLabel은 join('+') (space 없음) — split이 작동 안 해서 단일 chip 'Win+Shift+1'
- macOS에서도 'Win'으로 표시되어 사용자 혼동

수정:
- preload/index.ts: process.platform 노출 (electronAPI.platform)
- renderer/utils/format-hotkey.ts 신규
  - formatHotkeySegments(binding) → 플랫폼별 segment 배열
    - macOS: ['⌘', '⇧', '1'] (⌃ ⌥ ⇧ ⌘ 순, Apple HIG)
    - Win/Linux: ['Ctrl', 'Win', 'Alt', 'Shift', '1']
  - formatHotkeyLabel(binding) → 단일 문자열
    - macOS: '⌘⇧1' (구분자 없음, Apple 표준)
    - Win/Linux: 'Ctrl + Shift + 1'
- 사용 위치 일괄 교체 (binding.displayLabel → format 헬퍼):
  - SettingsModal HotkeyDisplay (chip 표시)
  - SettingsModal Agent 모드 설명 ({{key}} 치환)
  - OnboardingModal 단축키 chip + 완료 화면
  - DashboardPage CrtDisplay 'PRESS X TO RECORD'
  - HotkeyRecordModal currentBinding 표시

displayLabel 필드는 ConfigService 호환을 위해 유지 (저장된 값은 그대로)
2026-04-11 09:04:51 +09:00
yunchan8804
0785374804 feat(V2-X-b): UI 정리 + pull 동기화 + Stripe 서명 검증 + ui-native 패키지
묶음 A — UI 정리 + i18n 완성:
[A1] desktop SettingsModal에 CloudSyncSection 통합 (탭 6번째)
  - CloudIcon import, settings.tabs.cloud 키 추가
  - 기존 about 탭은 index 5 -> 6

[A2] apps/web Sidebar 공유 layout 리팩터링
  - app/(app)/layout.tsx 신규 (route group)
  - dashboard/meetings/record/teams/billing을 (app)/ 아래로 git mv
  - app/(app)/layout.tsx에 auth 가드 + Sidebar 통합
  - 기존 개별 page.tsx에서 Sidebar/auth 중복 제거
  - app/(app)/dashboard/layout.tsx 제거 (루트 layout이 처리)

[A3] 11개 locale에 V2 새 키 추가 (en/ja/zh/zh-TW/es/fr/de/pt/ru/vi/th)
  - nav.meetings/record/teams/billing/logout
  - login.subtitle/google/github/terms
  - settings.tabs.cloud

묶음 B — V2-4b pull 동기화:
- CloudSyncService.pullAll() 신규
  - history/dictionary 테이블 원격에서 fetch
  - last_sync_at 이후 updated_at만 필터
  - Last-Write-Wins 충돌 해결 (remote.updated_at > local.updated_at)
  - 로컬에 없는 행은 INSERT, 있는 행은 UPDATE (구체적 컬럼 지정)
- IPC CLOUD_SYNC.PULL_ALL 채널 + handler + preload api
- CloudSyncSection에 Pull 버튼 추가 (Push 옆에 위치)

묶음 C — V2-8b Stripe webhook 서명 검증:
- stripe-webhook Edge Function에 Web Crypto API 기반 HMAC-SHA256 검증
  - Stripe-Signature 헤더 파싱 (t=, v1= 엔트리)
  - Replay 방지 (timestamp tolerance 300초)
  - constantTimeEqual로 타이밍 공격 방지
  - crypto.subtle.importKey/sign으로 HMAC 계산
- stripe-portal Edge Function 신규 (Customer Portal)
  - JWT 인증 -> 기존 customer_id 조회 -> billing_portal/sessions 생성
  - return_url 지원
- apps/web/components/billing/portal-button.tsx (구독 관리 버튼)
- billing 페이지에 Free 외 tier 사용자에게 PortalButton 표시
- config.toml에 stripe-portal 함수 등록 (verify_jwt=true)

묶음 D — V2-6b packages/ui-native:
- @d3ro/ui-native 신규 패키지 (React Native 전용 DS)
  - theme.ts: d3roNativePalette/Typo/Radius (MUI 없는 정적 값)
  - components/MetalCard.tsx: View + 섀시 섀도우
  - components/PhosphorText.tsx: Text + 앰버 glow (textShadow)
  - components/Led.tsx: View 원 + glow
  - components/PhysicalButton.tsx: Pressable + 누름 느낌
- React/React-Native는 peerDependencies
- apps/mobile/package.json에 @d3ro/ui-native를 file: 의존성으로 추가
  (mobile은 npm workspace 제외이므로 file path 필요)

검증:
- desktop typecheck OK
- web typecheck OK
- web next build OK (11 라우트, (app) 그룹 반영)
- 회귀 없음
2026-04-09 21:36:38 +09:00
yunchan8804
5c0f4a2b98 feat(V2-4): 데스크톱 ↔ Supabase 동기화 (push only MVP)
CloudSyncService (싱글톤 + EventEmitter):
- Supabase 클라이언트 lazy init (configGet으로 url/key)
- safeStorage 기반 refresh token 영속화 (cloud-sync.token)
- startSignIn(provider): OAuth URL 발급 후 shell.openExternal로 외부 브라우저
- handleAuthCallback(code): code -> session 교환
- pushAll(): history/dictionary/meetings/meeting_memos/meeting_documents
  를 last_sync_at 이후 변경분만 upsert (onConflict: id)
- 이벤트: auth-changed / sync-progress / sync-complete / sync-error

Deep link (d3ro-voice://auth-callback):
- main/index.ts에 setAsDefaultProtocolClient
- macOS open-url 핸들러
- Windows second-instance argv 검사
- handleDeepLink로 URL 파싱 후 CloudSyncService로 전달

IPC:
- @d3ro/core/ipc-channels에 CLOUD_SYNC 채널 추가
- ipc/cloud-sync-handlers.ts (configure/signIn/signOut/pushAll/state)
- preload index.ts에 cloudSync 노출
- bootstrap의 cloud-sync 단계 추가

Renderer:
- CloudSyncSection 컴포넌트 신규
  - 미설정: Supabase URL/Key 입력 폼
  - 미로그인: Google/GitHub OAuth 버튼
  - 로그인: 사용자 이메일 + Sync Now + 진행 바 + 로그아웃
- d3roPalette/typoSx로 SSOT 준수, useI18n 사용

ConfigService 키 5종 추가:
- hfToken, diarizationEnabled (Phase 15.5 누락분 보충)
- supabaseUrl, supabaseAnonKey, cloudSyncLastAt (V2-4)
- packages/core types.ts AppConfig에도 동일하게 추가

검증:
- desktop typecheck OK
- desktop build OK
- 실제 Supabase 동기화는 사용자가 V2-2 배포 + 설정 입력 후 검증
2026-04-09 16:28:19 +09:00
yunchan8804
9742b2109a fix(V2-1): minor 이슈 정리 [M4][M5]
[M5] EditableSegment.tsx의 '#3b82f6' 하드코딩 제거:
- packages/ui theme.ts의 tag 팔레트에 blue/blueBg 추가
- SPEAKER_COLORS 배열이 d3roPalette.tag.blue 참조

[M4] 루트 .eslintignore 추가:
- site/, server/supabase/functions/ 등 자체 lint 설정이 있는
  서브 프로젝트 제외
- node_modules/out/dist/.next/release/sidecar-dist 제외
- apps/desktop/build/entitlements.mac.plist는 예외로 포함
2026-04-09 02:32:37 +09:00
yunchan8804
3524e958ba feat(V2-1d): packages/i18n 추출 — Electron 독립 i18n 엔진
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 독립성 확보 (monorepo 원칙 준수):
- I18nStorage interface 신설 (load/save 어댑터 추상화)
- window.electronAPI.config 직접 호출 제거
- I18nProvider는 storage prop으로 영속화 어댑터 주입
- 기본값 noopStorage (메모리 한정, 세션 범위)
- 결과: packages/i18n은 React에만 의존, web/mobile에서 재사용 가능

apps/desktop 어댑터:
- App.tsx에 electronI18nStorage 구현 (config.get/set 래핑)
- <I18nProvider storage={electronI18nStorage}>로 주입

apps/desktop 설정:
- package.json: @d3ro/i18n '*' dep 추가
- tsconfig.node/web.json paths에 @d3ro/i18n 추가
- electron.vite.config.ts alias + externalize exclude 추가
- vitest.config.ts alias 추가

일괄 치환 (29 파일):
- ./i18n, ../i18n, ../../i18n → @d3ro/i18n

apps/desktop/src/renderer/i18n/ 디렉토리 완전 제거.

검증: typecheck + build + dev 런타임 모두 통과.
Phase V2-1 전체 완료 (a/b/c/d).
2026-04-08 14:57:27 +09:00
yunchan8804
a041f1b6a9 feat(V2-1c): packages/ui 추출 — DS 컴포넌트 + theme + theme-vars 분리
packages/ui (@d3ro/ui) 신규:
- src/theme.ts (d3roPalette/d3roTypo/d3roShadow/d3roRadius SSOT)
- src/theme-vars.ts (팝업/main 프로세스용 CSS 변수 맵)
- src/components/ds/ (CrtDisplay, InstrumentPanel, Led, MetalCard,
  MetalDial, PhosphorText, PhysicalButton, ScreenPanel, ButtonGroup)
- src/index.ts barrel
- subpath exports: ./theme, ./theme-vars, ./components/ds
- React/MUI/Emotion은 peerDependencies로 선언
- @d3ro/core만 직접 의존성

apps/desktop/src/shared/ 디렉토리 완전 제거:
- theme-vars가 마지막 남은 파일이었음
- tsconfig include에서 src/shared/**/* 제거

일괄 치환 (renderer 전역):
- ../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)

apps/desktop 설정:
- package.json: @d3ro/ui: '*' dep 추가
- tsconfig.node/web.json: @shared/* paths 완전 제거, @d3ro/ui,
  @d3ro/ui/* paths 추가
- electron.vite.config.ts: @shared alias 제거, @d3ro/ui alias 추가,
  externalize exclude에 @d3ro/ui 추가
- vitest.config.ts: alias 교체

DS 컴포넌트 내부의 '../../theme' 상대 경로는 packages/ui 구조에서
동일하게 해결되어 그대로 유효.

검증: typecheck + build + dev 런타임 모두 통과.
2026-04-08 14:50:33 +09:00
yunchan8804
3b0eb3393b feat(V2-1b): packages/core 추출 — 공유 타입/에러/채널/유틸 분리
packages/core (@d3ro/core) 신규 생성:
- types.ts, errors.ts, ipc-channels.ts, constants.ts (shared에서 이동)
- utils/meeting-markdown.ts, utils/markdown-to-docx.ts (main/utils에서 이동)
- subpath exports 정의 (./types, ./errors, ./ipc-channels, ./constants,
  ./utils/meeting-markdown, ./utils/markdown-to-docx)
- src/index.ts barrel export 추가
- docx를 core 자체 dependency로 선언

apps/desktop 연결:
- package.json에 @d3ro/core: '*' dep 추가
- tsconfig.node/web.json paths에 @d3ro/core/* 추가
- electron.vite.config.ts 3개 섹션 alias 추가 (main/preload/renderer)
- externalizeDepsPlugin exclude에 @d3ro/core (workspace 소스 번들 대상)
- vitest.config.ts alias 추가

일괄 치환 (79 파일, 167건):
- @shared/{types,errors,ipc-channels,constants} → @d3ro/core/*
- static/dynamic import + type expression import 모두 포함
- MeetingModeService.ts의 ../utils/* 상대 경로 → @d3ro/core/utils/*
- @shared/theme-vars는 V2-1c 범위로 남김 (WindowManager만 사용)

M1 수정 포함:
- electron.vite.config.ts의 resolve('src/shared') → resolve(__dirname, ...)
  CWD 독립적으로 동작하도록 견고화

검증:
- typecheck 통과
- build 통과 (main+preload+renderer)
- dev 런타임 → DB/핫키/Ollama 모두 정상, 기존 데이터 연속성 유지
2026-04-08 14:39:46 +09:00
yunchan8804
45a580878a feat(V2-1a): Monorepo 구조 전환 — apps/desktop으로 V1 이동
- npm workspaces 루트 (apps/*, packages/*) 세팅
- 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)
- apps/desktop/package.json 신규 (name=@d3ro/desktop)
- productName: 'd3ro-voice' 명시 — app.getName()을 고정하여 userData 경로
  %APPDATA%\d3ro-voice\ 그대로 유지 (기존 DB/설정 연속성 보장)
- 루트 package.json을 workspace 루트로 재구성, 공통 devDep만 유지
  (typescript, eslint, prettier)
- turbo.json, tsconfig.base.json 추가 (Turborepo 자체 설치는 별도 sub-phase)
- memory/project_status.md 생성 (규칙 13)

검증:
- npm run typecheck 통과
- npm run build 통과 (electron-vite main+preload+renderer)
- npm run dev 실제 실행 → DB/핫키/Ollama 자동 실행 모두 정상
2026-04-08 14:04:41 +09:00