SaaS [9] — 실증 A/B/C/D 전부 통과, 로컬→클라우드 push 최초 성공(pushed=1).
## Bug 4: 로컬 nanoid PK vs Supabase UUID PK 불일치
- 증상: Push history failed: invalid input syntax for type uuid: "fvy6bIzr..."
- 원인: 로컬 drizzle schema는 text PK + nanoid() 생성, Supabase는 uuid PK.
빅뱅 사이클 내내 push가 한 번도 성공한 적 없었음 (지난 pushed=0은 데이터 0건이라서).
- 픽스: 로컬을 UUID로 통일 (근본 해결, 땜질 금지).
14개 서비스 20곳 nanoid() → crypto.randomUUID() 일괄 교체.
nanoid 의존성 + electron.vite.config exclude 제거.
drizzle schema는 text PK 그대로 유지 (SQLite는 UUID 문자열 저장 가능).
## Bug 5: supabase_realtime publication 누락
- 증상: 로그인 직후 Realtime 채널 상태: TIMED_OUT
- 원인: initial_schema.sql이 transcripts 테이블만 publication에 추가.
데스크톱이 구독하는 meetings/history/dictionary는 누락 → postgres_changes 흐르지 않음.
- 픽스: 20260411000002_realtime_publication.sql 신규.
pg_publication_tables 카탈로그 체크 + 조건부 ADD TABLE (meetings/meeting_memos/
meeting_documents/history/dictionary 5개). supabase db push 적용.
## Bug 6: persistSession:false에서 realtime.setAuth 자동 전파 안 됨 (부분 픽스)
- 픽스: CloudSyncService.startRealtime()에 client.realtime.setAuth(access_token)
명시 호출 (채널 구성 이전).
- ⚠️ Bug 5+6 적용 후에도 Realtime 여전히 TIMED_OUT. 후속 조사 필요.
블로커 아님 — 주기 pull + Phase 3.3 auto push로 최종 일관성 유지.
## 실증 결과
- A 세션 자동 복원: Restored session for yunchan8804@gmail.com → DB 재오픈
- B push 경로: HistoryService created 56a767ac-... → Sync complete pushed=1 errors=0
- C 로그아웃 복귀: Realtime 종료 → users/_local/d3ro.db 복귀 → local mode
- D 재로그인 복원: 실증 A의 restore 경로와 동일, 같은 uuid DB 파일 보존
- E 웹 크로스 디바이스: Phase 3.3 이후로 지연 (Realtime 이슈 별건)
검증: desktop tsc --noEmit ✅, dev 재기동 ✅, push 최초 성공 ✅
배경:
사용자가 데스크톱 앱 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만)
- 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 자동 실행 모두 정상