Commit graph

70 commits

Author SHA1 Message Date
Yun Chan
1af3cf75c7 fix(release): stop shipping native modules built for the wrong runtime
Some checks failed
deploy-site / deploy (push) Failing after 14m16s
The released installer could not start: it carried a better-sqlite3 build for the
host Node runtime instead of Electron, so the app died immediately with a module
version mismatch when it opened its database.

Packaging now proves the Electron build of every runtime-sensitive native module
before an installer or archive exists, and installers are produced only from that
verified tree, so the mistake cannot pass silently. The release pipelines run the
same check.

The default local model also pointed at a retired model: a *.gguf name that
Ollama cannot serve, while the settings, onboarding, and guide screens
recommended an older model. All of them now use the model the service code
already preferred.
2026-09-18 15:45:03 +09:00
Yun Chan
0fbbbc1756 fix(release): restore automatic updates by shipping the speech engine on demand
Some checks failed
deploy-site / deploy (push) Failing after 1m15s
Auto-update could not work at all: the installer was 189 MB because it carried
the local speech engine and ffmpeg, and the download feed rejects uploads over
about 100 MiB, so update metadata could never be published.

The installer now leaves those components out and the app fetches them the first
time they are needed, verifying every part and the joined archive before
installing. The installer is 90.6 MiB, the update feed is published again, and
updates stay small because the engine is not re-sent on every release.

The fetch is visible and recoverable: the download runs with progress, a failed
install cleans up after itself, and Settings > STT shows the runtime status with
a manual download action for when the automatic one cannot run.
2026-09-18 13:51:49 +09:00
Yun Chan
2d585bfc29 feat(desktop): make local speech transcription work end to end
Local dictation had never produced a transcript on an installed build. The
engine itself was healthy; every connection to it was broken.

Installed builds shipped no speech engine at all: the packaging config had no
entry for the faster-whisper sidecar and no pipeline step built one, so the app
always fell back to a system Python without the runtime. Development was broken
too, because the sidecar and SoX paths were resolved against the Vite output
directory instead of the app root, which also meant recording failed with a SoX
ENOENT. On hosts where localhost resolves only to IPv6, every local request was
refused outright, which silently disabled both local transcription and the local
LLM.

The sidecar is now built and bundled (including the Silero VAD data it needs),
gated by a packaging check that fails when the engine or its data is missing.
Paths are discovered from the app root and fail loudly when the engine is
absent. Local engine URLs are normalized to the IPv4 loopback, decoding is tuned
so repeated hallucinations cannot compound (the same transcript now takes about
a fifth of the time), the engine is warmed up at startup, and holding the hotkey
now shows the text forming live in the recording tip.
2026-09-18 00:48:47 +09:00
Yun Chan
6ba25f53b7 fix(desktop): surface configuration and provider failures instead of hiding them
Several desktop paths quietly substituted defaults or partial results: a
config write could fall back to a throwaway in-memory store, speech provider
errors were absorbed into empty transcriptions, and meeting exports built
file names from raw titles.

Writes now fail explicitly when the store is unavailable, provider and model
failures reach the UI as errors, and export names pass through one
sanitizer. Settings, license, ad, and support surfaces use the shared theme
tokens, unused hotkey helpers are gone, and the package gains strict
node/renderer typecheck configs plus red-team e2e scenarios for these flows.
2026-09-16 23:23:58 +09:00
Yun Chan
c8d802d78f feat(desktop): fill sponsor slots from direct house campaigns
When mediation had no programmatic fill, the banner and rewarded surfaces
collapsed to empty space. Direct house sponsors now serve their own copy and
click-through, with the same settlement accounting used by the mediated
network, and the mediation engine tests cover the added path.
2026-09-16 23:23:39 +09:00
Yun Chan
911c9f0229 feat(desktop): move dictionary entries in and out as files
Users could only rebuild their spoken-word dictionary entry by entry. Import
and export now round-trip the whole list, reporting duplicate and invalid
entries per row instead of failing the batch, so a dictionary survives a
reinstall or a move to another machine.
2026-09-16 23:23:30 +09:00
Yun Chan
7953706142 feat(release): publish desktop updates from a tag through one feed
Desktop clients had two competing update sources: the runtime pointed at a
legacy GitLab registry while the Forgejo packages were filled in by
hardcoded, version-pinned scripts. Operators could not tell which feed was
authoritative, and no release could be reproduced from a tag.

Auto-update now reads a single canonical Forgejo registry feed, updated by
a version-agnostic publisher that runs from the tag on Forgejo, GitLab, and
GitHub CI alike. Channel, minimum supported version, forced install,
full-versus-delta thresholds, staged rollout, and a remote kill switch come
from one policy file the client fetches alongside the feed. Tag creation is
gated on a clean tree, matching version surfaces, and a changelog section.
2026-09-16 23:23:00 +09:00
Yun Chan
5205dcdfa9 feat(release): prepare 1.1.0 candidate 2026-08-29 18:33:45 +09:00
Yun Chan
eedbe97d02 fix(desktop): clean single-instance lock exit and suppress devtools console crash
Some checks are pending
deploy-site-windows / deploy-win (push) Waiting to run
deploy-site / deploy (push) Waiting to run
2026-08-20 22:48:58 +09:00
Yun Chan
63b97c4761 feat(updater): overhaul cross-platform auto-update with user consent, blockmap diffs, skip version, and mobile update manager
Some checks failed
deploy-site-windows / deploy-win (push) Waiting to run
deploy-site / deploy (push) Failing after 19s
2026-08-20 22:17:14 +09:00
Yun Chan
708e20f747 feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
2026-08-20 11:12:05 +09:00
Yun Chan
4fdf8bfdf8 fix(config): v0.2.1-alpha — CONFIG_DEFAULTS 7키 기본값 누락 핫픽스 (앱 실행 불가)
Some checks failed
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 4s
refactor-wave WS2가 AppConfig 인터페이스에 7키 추가 + as never 16건 제거했으나
CONFIG_DEFAULTS(ConfigService.ts) 기본값 누락 → 기존 사용자 config(0.1.x) 마이그레이션
시 configGet undefined → main 프로세스 .map() 크래시 (v0.2.0-alpha 앱 실행 불가).

수정: CONFIG_DEFAULTS에 7키 기본값 추가.
검증: dev 실행 — main 빌드 + electron start 정상 (수정 전엔 이 단계 크래시).
교훈: 배포 전 dev 실행 + 산물 e2e 필수. typecheck GREEN ≠ 런타임 안전.
2026-07-22 04:06:43 +09:00
Yun Chan
5b6e7aae6b refactor(main): ipcSuccess/ipcError 헬퍼 통일 + catch(error) 패턴 (WS-PATTERN)
- cloud-sync-handlers 수동 ok/fail -> ipcSuccess/ipcError 헬퍼 (6 핸들러)
- catch {} -> catch(error) 에러 메시지 보강 32건 (11 ipc handler 파일)
KEEP: template-handlers(주석 명시 에러 무시), license-handlers(이미 헬퍼 사용),
       audio-handlers stop().catch(() => {})(의도적 무시)
정책: docs/REFACTOR_POLICY.md DP2
2026-07-22 02:38:23 +09:00
Yun Chan
4c256202f7 chore(deps): 미사용 의존성 8종 제거 + electron-builder Linux 섹션 삭제 (WS-DEPS)
- apps/desktop/package.json에서 8종 제거(각 역추적 0건):
  @mui/icons-material, @d3ro/api-client, fluent-ffmpeg, @types/fluent-ffmpeg,
  pdf-parse, node-record-lpcm16, electron-audio-loopback, @electron-toolkit/preload
- node-record-lpcm16.d.ts 타입 선언 삭제
- electron-builder.yml Linux 섹션 삭제 (CI는 win/mac만 호출)
참고: @mui/icons-material은 @d3ro/ui 트랜시티브로 잔존(desktop 직접 의존에서는 제거)
정책: docs/REFACTOR_POLICY.md DP9
2026-07-22 02:38:10 +09:00
Yun Chan
00a99e4087 refactor(popup): 5종 팝업 _i18n 주입 + 색상 토큰화 (WS-POPUP)
- 4종 팝업(command/history/result/recording-tip) _i18n 주입:
  WindowManager getPopupI18nStrings() + 팝업 script applyI18nToHtml()
- 한국어/영어 하드코딩 -> popup.* i18n 키 (ko/en 17키)
- theme-vars.ts에 --d3-wave-1~9, --d3-status-error/success 토큰 추가 (6 테마)
- 팝업 style.css hex -> CSS var 토큰
- packages/i18n getI18n() (main 프로세스용) 추가
SKIP: caption-overlay _i18n(별도 IPC 구조), wave box-shadow 보간(opacity 단순화)
정책: docs/REFACTOR_POLICY.md DP3, 이식 인사이트 P5
2026-07-22 02:37:58 +09:00
Yun Chan
078304dda9 refactor(ui): deprecated accent.amber->main 91건 + 매직 리터럴 토큰화 (WS3)
- accent.amber/amberDim/amberGlow -> accent.main/dim/glow 91건 (34파일).
  theme.ts의 @deprecated 별칭 참조 제거 (본체 main 사용).
- inline fontSize/fontWeight -> d3roTypo, borderRadius -> d3roRadius 토큰 37건.
- P4: 화면 고유 수치(스탯 fontSize, 레이아웃 width)는 토큰화 제외.
정책: docs/REFACTOR_POLICY.md DP1, 이식 인사이트 P4
2026-07-22 01:54:37 +09:00
Yun Chan
aef44289a5 refactor(main): IPC 채널 SSOT 일원화 + AppConfig 타입 강화 (WS2)
- ipc-channels.ts SSOT에 9개 그룹/키 추가: INSTRUCTION, SYSTEM_AUDIO,
  POPUP_RESULT/HISTORY/COMMAND/CAPTION, VOICE_PARTIAL, CLIPBOARD, APP,
  VOICE_CONVERSATION.FINISH_LISTENING
- WindowManager/handlers/bootstrap 하드코딩 채널 -> IPC_CHANNELS 교체.
  불일치 4건(result:* vs window:tip*)은 preload 재검증 후 SSOT로 통일.
  notifyRenderer channel: string -> IPCChannel 타입 좁힘.
- AppConfig에 7개 누락 키 추가(customInstructions, llmChains,
  voiceCommandRules, voiceCommandsEnabled, activeInstructionId,
  activeChainId, captionAudioSource) -> as never 16건 제거.
- ChainService/CustomInstructionService/WindowManager dynamic require -> static import.
- services/index.ts 데드 레지스트리 제거 (import 0건).
SKIP: ipcSuccess/ipcError 헬퍼 통일, catch 패턴(별도)
정책: docs/REFACTOR_POLICY.md DP2, DP9
2026-07-22 01:54:18 +09:00
Yun Chan
32db25a74f feat(dashboard): 히어로 + 스탯 타일에 TiltCard 적용
동기 부여된 모션만 — 히어로(maxTilt 8) + 4개 스탯 타일(maxTilt 6). 나머지 데이터 밀집/드롭존/리스트 섹션은 MetalCard 유지 (틸트 = unmotivated slop 회피).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 00:51:30 +09:00
Yun Chan
fd46ac7b14 fix(voice): 받아쓰기 파이프라인 4버그 수정 + 실시간 부분 전사
- press가 STT 초기화를 await하며 action queue 점유 → release 수십 초 지연·
  유령 세션 반복 버그 수정 (initSTT fire-and-forget)
- 프리플라이트: STT 모델 미설치 시 즉시 에러 + 메인 UI 경고 + 온보딩 오픈
- 사이드카: 기동 중 프로세스 사망 시 30초 대기 없이 즉시 실패,
  restartCount 리셋, error 리스너 부재 미처리 예외 방지
- 실시간 부분 전사: 1.5s 간격 interim → RecordingTip에 말하는 내용 미리보기
- Ollama 미가용 후처리 스킵 시 warning 배너, voice:error 브로드캐스트 신설
2026-07-21 20:56:40 +09:00
Yun Chan
082ed2ef02 feat(ui): Midnight Glass v2 마감 — lucide 아이콘 통일 + 몰입 패널 웨이브 + 팝업 정합
- @mui/icons-material → lucide-react 전환 (23파일, 잔여 0)
- VoiceRecordingPanel: 9바 DOM 웨이브 → GradientWave (canvas)
- Vanilla 팝업 5종 style.css v2 정합 (글래스 네이비/헤어라인/블루,
  CSS 변수 주입 구조 유지 — 테마 반응형 보존)
2026-07-21 20:20:32 +09:00
Yun Chan
a8a1d6e546 feat(ui): Midnight Glass v2 전면 리디자인 — 기반+셸+대시보드
- theme.ts 재작성: glass/gradient/glow 토큰군, 기본 dark=Midnight(블루),
  6종 테마 변형 전부 파생 토큰 유지 (테마 시스템 보존)
- DS 리스킨(API 호환): 글래스 카드/버튼, Pretendard 타이포, 소프트 LED
- 신규: GradientWave(canvas 스펙트럼 음파), StatRing(컬러 링 타일)
- 보더리스: frame:false + 커스텀 TitleBar, 네이티브 스크롤바 제거(폭 불변)
  + OverlayScrollbars 오버레이 스크롤
- AppLayout 와이드 사이드바(lucide, 그라디언트 액티브 필), 대시보드 재구축,
  StatusBar(브랜드/로컬시간), Pretendard 번들, i18n +20키(ko/en)
2026-07-21 20:14:15 +09:00
Yun Chan
cbed451209 feat(icon)+fix(audio): 앱 아이콘 적용 + 트레이 아이콘 + 마이크 테스트 조기 종료 수정
Some checks failed
Build macOS / Build & Package (macOS) (push) Failing after 5s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
- 앱 아이콘: icon.svg 마스터(d3ro 브랜드 — 메탈 섀시+오렌지 웨이브) → png/ico
  생성 스크립트(generate-icons.mjs, sharp) + electron-builder win/mac 연결
- 트레이: createEmpty() 빈 아이콘 → 실제 앱 아이콘 (getAppIconPath)
- 마이크 테스트: 초기 무음 level:0을 종료로 오인하던 조기 종료 버그 —
  AUDIO.TEST_LEVEL에 done 플래그 신설(SSOT), STOP 버튼이 실제 캡처 중지,
  testDevice 실패 시 상태 롤백
2026-07-21 19:00:23 +09:00
Yun Chan
0b9e67afe9 fix(onboarding): 무진행 구간 UX + 중복 실행 가드
Some checks failed
Build macOS / Build & Package (macOS) (push) Failing after 3s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
- verifying/manifest 상태(바이트 진행률 없음)에서 인디터미넌트 바 + 친화적 문구
  — 9.6GB 검증 ~2분간 멈춘 것처럼 보이던 문제 해소
- OnboardingModal 중복 실행 가드 (runningRef)
- LocalLLMService.pullModel: 동일 모델 동시 pull은 기존 promise 합류
- LocalSTTService.downloadModel: /download 409는 실패가 아닌 기존 진행 합류
2026-07-21 18:02:26 +09:00
Yun Chan
c6fd8c9a9a ci: runner 인프라 정렬 + 자동 업데이트 feed 활성화 (프로젝트 1172)
- CI 태그를 실제 사내 runner에 맞게 교체: build-win-x64(TW-VIVEN-BUILD) /
  build-mac-arm64(TW-BUILD-MAC-ARM64) / build-linux-x64(TW-BUILD01, release+검증 잡)
- package-macos: volta 부트스트랩 폴백 추가
- update-feed.ts + electron-builder.yml publish.url에 프로젝트 ID 1172 기입
- release-guide: 완료된 설정 반영 (D3RO_MAC_RUNNER, registry public pull)
2026-07-21 15:33:30 +09:00
Yun Chan
3beba99668 feat(release): switchboard 릴리스 체계 이식 — Mac 배포 + Registry 게시 + 자동 업데이트
- package-macos CI job: arm64 무서명(ad-hoc) dmg+zip, D3RO_MAC_RUNNER 변수 게이트
- sync-version.mjs: 태그 → package.json 버전 동기화
- publish-gitlab-release.mjs: Generic Package Registry(버전별+latest) 업로드 + Release 생성
- electron-builder: publish generic(latest.yml 생성), 공백 없는 artifactName, mac arm64 단일화
- UpdateService: electron-updater 4h 주기 체크 + 재시작 다이얼로그 (update-feed.ts SSOT)
- @rollup/rollup-win32-x64-msvc → optionalDependencies (mac npm ci EBADPLATFORM 해소)
- docs/deployment/release-guide.md: runner 등록·feed 설정·파일명 규칙 가이드

리뷰 워크플로(3관점 적대적 검증)로 확정된 결함 8건 반영:
publish 부재로 latest.yml 미생성, 파일명 공백 vs 레지스트리 404,
mac EBADPLATFORM, needs optional의 실패 미커버, DELETE 권한 의존,
CHANGELOG regex 경계, --arm64 무시, 다이얼로그 parent 부재
2026-07-21 12:43:16 +09:00
Yun Chan
8f7d300b89 feat(conversation): OpenAI gpt-realtime-2.1 라이브 음성 대화 Premium 백엔드
- realtime-token Edge Function: 티어 검증 + realtime_session 쿼터 + ephemeral key 발급
- useRealtimeConversation 훅: WebRTC 직결 (마이크 → OpenAI, 오디오 자동 재생)
- VoiceConversationPage: conversationBackend 분기 + 연결 실패 시 로컬 fallback
- AppConfig.conversationBackend + SettingsModal 음성 대화 엔진 선택
- ErrorCode 799 ConversationRealtimeTokenFailed, i18n ko/en
2026-07-21 12:10:13 +09:00
Yun Chan
983c60cda2 feat(bootstrap): Whisper large-v3-turbo 기본 전환 + 온보딩 2단계 다운로드 진행률
- 기본 STT 모델 base → large-v3-turbo (6배 빠름, 1.6GB)
- 사이드카: /download, /download/status, /download/cancel + --models-dir
- LocalSTTService: downloadModel/cancelDownload + download-progress 이벤트
- IPC: 설계서 02의 stt:downloadModel/cancelDownload/downloadProgress 구현
- OnboardingModal: LLM(gemma4:e4b) → STT(turbo) 2단계 순차 다운로드 UI
- SettingsModal turbo 선택지 + settings.model.largeTurbo 12 locale
- 테스트: 모노레포 잔재 import 수정 (src/shared → @d3ro/core), 41/41 통과
2026-07-21 11:59:49 +09:00
yunchan8804
aa65e710ec feat: 배포 파이프라인 — Ollama/sidecar 번들 + NSIS 자동 VC++ + GitLab CI + 온보딩 모달
- sidecar 슬림화: torch/pyannote 제거, ctranslate2 GPU 감지, /diarize 삭제
- Ollama 번들: resources/ollama/에 포터블 바이너리 배치, LocalLLMService 1순위 탐색
- installer.nsh: VC++ 재배포 x64 자동 다운로드(aka.ms 경유) + 사일런트 설치
- electron-builder: extraResources에 ollama 추가, nsis.include로 installer.nsh 연결
- scripts: download-ollama.ps1/sh 신규
- LLM.PULL_MODEL IPC 핸들러 + LocalLLMService.pullModel() 구현 (api/pull 스트리밍)
- 온보딩 모달: gemma4:e4b 미설치 감지 시 자동 표시, 진행률 UI, i18n(ko/en) 키 추가
- .gitlab-ci.yml: Windows 러너에서 sidecar/sox/ollama 준비 후 NSIS 패키징, 태그 시 Release 자동 생성
2026-04-15 19:47:27 +09:00
yunchan8804
541f04d02b refactor: saasMode 분기 제거 — 항상 SaaS(OAuth only)로 동작
Mac/Windows 환경 불일치 해소. BUILD_TIME 상수, CONFIGURE 채널,
개발자 모드 UI 전부 제거. Supabase 설정은 supabase-config.ts SSOT.
2026-04-13 17:20:55 +09:00
윤찬
feb82abbf7 번역 2026-04-13 14:53:17 +09:00
윤찬
8af75a0a1e feat: V2-7 Admin 콘솔 리디자인 + 3단계 권한 + SaaS 전환 + 코드 정리
- Admin CRM: D3RO Console 스타일 전체 적용 (panelSx/tableSx/filterBtnSx)
- 3단계 권한: manager/admin/super_admin (DB + Edge Functions + Frontend)
- 랜딩 페이지: 1회 결제 → 월간/연간 구독 SaaS 모델 (10개 언어)
- SSE 스트리밍: VoiceConversation Premium LLM 라우팅 + fallback
- Supabase 클라이언트: packages/api-client 공통 추출 (browser+server)
- RPC 함수 타입: 9개 정의 (admin_usage_by_feature 등)
- callAdminApi 401 버그 수정 (getUser() 선행 토큰 갱신)
2026-04-13 01:28:10 +09:00
윤찬
7467bd6ce9 feat(desktop): Premium LLM SSE 스트리밍 — 토큰 단위 실시간 응답
- sse-parser.ts: Anthropic SSE 파서 (content_block_delta.text 추출)
- CloudSyncService.invokeFunctionStream(): raw fetch + SSE ReadableStream
- PremiumLLMService.chatStream(): stream=true, 토큰 단위 yield
- SSE 실패 시 자동 비스트리밍 fallback
- AbortController 연결 (취소 지원)
2026-04-12 20:26:17 +09:00
윤찬
36031acda9 refactor(desktop): LemonSqueezy API 코드 완전 제거 (~150줄)
- LicenseService: LS API 상수/인터페이스/메서드 4개 삭제
- activate(): 로컬 키 검증 전용으로 단순화
- deactivate(): LS API 호출 제거
- initialize(): _tryPeriodicValidation 제거
- site/ taxNote: LemonSqueezy → Payple (10 locale)
2026-04-12 20:15:40 +09:00
윤찬
996def683b refactor(desktop): 라이선스 UI 빅뱅 — 키 입력 제거, 구독 기반 전환
- LicenseTab/LicenseModal: LemonSqueezy 키 입력 삭제, Payple 구독 UI로 전환
- useLicenseState 훅: 라이선스+클라우드 인증 공용 상태 관리 추출
- PREMIUM_MODEL_LIMITS: 3곳 중복 → @d3ro/core/constants 단일 소스
- i18n: LemonSqueezy 전용 키 18개 삭제, 구독 관련 키 13개 추가 (12 locale)
- DashboardPage: dashboard.model* → license.model* 키 통일
2026-04-12 19:54:04 +09:00
윤찬
68881bf0d0 fix(desktop): Payple 배포 완료 + billing URL 환경분기 + 세션만료 버그 수정
- OPEN_BILLING: app.isPackaged 기반 dev/prod URL 분기
- CloudSyncService: stored token 없을 때 auth-changed null emit 누락 수정
- Payple Edge Functions 3개 배포 ACTIVE 확인
2026-04-12 19:31:02 +09:00
윤찬
c9baf031c9 feat(server+web+desktop): Phase 3.2-B Payple 결제 연동 + 대시보드 Premium 상태 표시
Payple PG 연동:
- _shared/payple.ts: Payple API 래퍼 (auth/billing/cancel/deleteBillingKey)
- payple-checkout Edge Function: 빌링키 결제 + 구독 활성화
- payple-webhook Edge Function: 결제완료/취소 이벤트
- payple-manage Edge Function: 구독 취소 (빌링키 해지)
- DB migration: payment_provider + payple_payer_id + payple_pay_oid
- 웹 billing 페이지: Payple JS SDK 결제창 + 관리 버튼 (Stripe 대체)
- Electron LicenseModal: shell.openExternal → 웹 결제 페이지

대시보드 Premium 상태:
- CrtDisplay services에 PREMIUM LLM LED 추가
- 백엔드 인디케이터 카드 (Local/Premium) + 티어 카드
- 사용량 섹션: 전 티어 표시 + 모델별 Premium 쿼터
- 12개 locale × 14개 i18n 키
2026-04-12 19:18:40 +09:00
윤찬
6e52c18e5b feat(desktop+server): Phase 3.2 Premium LLM — Anthropic Claude 프리미엄 파이프라인 + 모델별 쿼터 + SaaS UI
빅뱅 8/8 마지막 성공 기준 달성. Supabase Edge Function(llm-proxy)을 통해
Anthropic Claude를 호출하는 PremiumLLMService 신규 구현. 사용자가 Settings에서
Local/Premium 백엔드를 선택하면 VoiceModeService가 자동 분기하고, Premium 실패 시
Local로 silent fallback + 상단 중앙 배너 알림.

실측: Claude Haiku refine 1.6~3.2초 (이전 qwen3 42.9초 → 13~27배 빠름).

주요 변경:
- PremiumLLMService 신규 (싱글톤+EventEmitter, processText/chatStream,
  Supabase functions.invoke 기반, _ensureAuth 가드)
- llm-prompts.ts: SYSTEM_PROMPTS를 Local/Premium 공유 모듈로 추출
  (resolveSystemPrompt 헬퍼)
- VoiceModeService: _getLLMProcessor → _runProcessorWithFallback 라우터 +
  premium-llm-fallback 이벤트
- CloudSyncService: getAccessToken(async), getAnonKey, invokeFunction(auth
  헤더 자동 처리, 에러 body 파싱)
- IPC: LLM.PREMIUM_* 채널 6개 + preload API + llm-handlers 이벤트 전달
  (safeSendToRenderer 헬퍼)
- AppConfig.llmBackend: 'local' | 'premium' (기본 'local')
- Settings UI: Backend 드롭다운 + Premium 선택 시 Ollama UI 숨김 + 라이선스
  모달 자동 오픈
- AppLayout: 상단 중앙 Snackbar fallback 배너 (8초, warning filled)
- LicenseModal: 라이선스 키 입력 제거 → SaaS 구독 관리 UI 전환
  (Free/Pro/Pro+ 업그레이드 버튼, Payple 준비 중 스텁)
- 등급 비교 표: featureLabel i18n 번역 수정

서버 (Supabase Edge Functions):
- quota.ts: 모델별 쿼터 구조 (llm_haiku/sonnet/opus × free/pro/pro_plus),
  주간/일간 기간 분리, modelToQuotaKey 매핑, consumeQuota baseLimit 파라미터화
- llm-proxy: 모델별 쿼터 체크 + 소비 (checkQuota → consumeQuota 원자적),
  verify_jwt=false (2026 sb_publishable_ 키 호환)
- config.toml: llm-proxy verify_jwt = false
- migration 20260412000001: tier team→pro_plus 통일, subscriptions.overage_credits
  컬럼, consume_quota RPC (원자적 base→overage fallback)

Tier/쿼터:
- free: Haiku 250/주간, Sonnet/Opus 불가
- pro ₩9,900: Haiku 1500/일, Sonnet 300/일, Opus 50/일
- pro_plus ₩29,900: Haiku 무제한, Sonnet 1500/일, Opus 300/일
- api-client SubscriptionTier: team→pro_plus, overage_credits 필드 추가
2026-04-12 18:28:02 +09:00
윤찬
d397bcbf57 feat(desktop): LLM 기본 모델 qwen3:4b → gemma4:e4b 전면 전환 + think:false 안전장치
qwen3:4b가 reasoning 모델이라 <think>...</think> 블록을 길게 생성 →
stripReasoningBlocks 후 빈 문자열 → 원본 transcript fallback으로 끝나면서
LLM refine이 42초 걸리는 병목 발견. Google Gemma 4 e4b(4.5B effective
params, 2026-04-02 릴리스)로 교체. non-reasoning 기본 + Ollama v0.20+
think: false 파라미터로 2중 방어.

실측 결과: 받아쓰기 한 사이클 51.4s → 5.5s (9.3배 빠름).
  STT 500ms + LLM 3,925ms + insert 1,092ms.
refine 품질 정상 동작 확인: "테스트하는 중입니다" → "테스트하고 있습니다".

- LocalLLMService: 3개 fallback 기본값 변경(generate / streamGenerate /
  chatStream) + Ollama 요청 body에 think: false 명시 추가. non-reasoning
  모델은 무시, reasoning 모델은 thinking 토큰 차단. NO_THINK 주석을
  legacy 설명으로 업데이트 — qwen3/deepseek-r1 수동 선택자를 위한 3중
  방어(/no_think + think:false + stripReasoningBlocks) 명시.
- OnboardingModal / OllamaGuideModal: pull 명령어 갱신
- 테스트 fixture 갱신
- 12개 i18n locale JSON: settings.ollamaHint / ollama.step2.alt 키 업데이트
  (qwen3:4b → gemma4:e4b, qwen3:8b → gemma4:26b)
- 10개 site i18n locale TS + HowItWorks.tsx 파이프라인 시각화 — detail
  문자열 'qwen3 / llama3 / gemma3' → 'gemma4 / llama3.2 / phi4',
  파이프라인 라벨 'qwen3:4b @ localhost' → 'gemma4:e4b @ localhost'
- 설계서 00 LLMConfig 기본값 + CONFIG_DEFAULTS
- 설계서 05: 6개 API 스키마 예시, 2개 OllamaClient 코드 예시, LLM 모델
  추천 표 재정렬(gemma4:e4b 최상위, qwen3는 reasoning 경고와 함께 후순위),
  권장 JSON 설정에 think:false 추가
- phase-14 meeting mode 컨텍스트 윈도우 표 갱신
- V2-5 Mac 부트스트랩 가이드 pull 커맨드 갱신
- project_status.md Part 7 전체 섹션 추가
2026-04-12 09:47:08 +09:00
윤찬
a744551442 fix(desktop): Bug 13 전면 해소 — Conversation 에러 배너 + Dictation 빈 STT 가드 + recording tip 타이머 leak
- VoiceConversationPage: Snackbar + Alert(severity=warning filled) 하단 중앙
  배너 추가, onError 콜백에서 setErrorBanner. formatErrorMessage 헬퍼 —
  phase=stt + 'no speech' → i18n conversation.error.noSpeech, 그 외 →
  phaseLabel: rawMessage.
- VoiceModeService._transcribe: minBytes 가드(0.5s @ 16kHz 16bit mono =
  16000B) + 빈 result.text 가드. 양쪽 모두 _handleError(STTAudioTooShort /
  STTNoAudioData, 'No speech detected...')로 recording-tip popup error
  상태 3초 표시 재사용. 기존에는 빈 전사가 조용히 session completed +
  빈 history entry 생성되던 경로 차단.
- VoiceModeService._errorHideTimer 필드: _handleError의 setTimeout(hide, 3000)
  핸들 보관, _startSession 초기화 블록과 dispose()에서 clearTimeout. 다음
  세션 시작 후에도 이전 에러 타이머가 살아 진행 중인 recording tip을 ~2초
  지점에 숨기던 잠재 버그 수정 (실측 재현 및 해소 확인).
- i18n ko/en: conversation.error.phase.{stt,llm,tts} + conversation.error.noSpeech
  4개 키 추가.
2026-04-12 09:46:22 +09:00
윤찬
dc7f93349e fix(desktop): CloudSync Realtime 자동 재구독 + Refresh token 실패 재인증 알림 (U1+U3, 빅뱅 Phase 5 Part 6)
## U1 — Realtime TIMED_OUT 자동 재구독

CloudSyncService.startRealtime()의 subscribe() 콜백이 TIMED_OUT 한 번만
로깅하고 끝나던 것을 지수 백오프 재구독으로 교체.
- SUBSCRIBED: retry count reset + timer clear
- TIMED_OUT / CHANNEL_ERROR / CLOSED: _scheduleRealtimeRetry() 트리거
- 백오프: 1s → 3s → 10s (MAX 3회)
- 초과 시 warn 로깅 후 포기 (Auto push fallback으로만 동작)
- stopRealtime()에서 타이머/카운터 cleanup

Publication, RLS, setAuth 모두 설정 완료 상태에서도 매 기동 시 TIMED_OUT이
찍히는 증상. 근본 원인은 Supabase 서버 transient(네트워크/WebSocket
타임아웃) 의심. 이 패치로 완전 해결은 아니지만 일시적 네트워크 jitter
복구 윈도우를 확보하고, 3회 실패 시 명확한 포기 메시지를 남긴다.

## U3 — Refresh token 실패 시 재인증 이벤트

init()의 refreshSession 실패 브랜치에서 warn 로깅 + 토큰 삭제만 수행해
사용자가 "왜 로그아웃됐지?" 상태로 방치되던 문제.
- logger.warn → logger.error(에러 레벨 승격)
- sync-error 이벤트 emit (사용자에게 보여줄 메시지 포함)
- auth-changed { user: null } emit (renderer 게이트 재잠금)
- catch 브랜치도 동일 처리 (기존엔 로깅만)

## U4 — package-lock.json optional dep (유지 결정)

rollup 전 플랫폼 optional deps가 lock에 기록돼 있지만 Windows 빌드자
사전 fetch 효율 + 크로스플랫폼 CI 환경을 고려해 유지. apps/desktop/
package.json:56에 @rollup/rollup-win32-x64-msvc 명시 의존이 있는 이유도
동일. 이 이슈는 확인 후 close.
2026-04-11 23:24:22 +09:00
윤찬
dd591d06e7 fix(desktop): AudioCaptureService.getDevices Mac 분기 — system_profiler CoreAudio 열거 (U7, 빅뱅 Phase 5 Part 6)
getDevices()가 powershell -NoProfile을 플랫폼 불문 호출해 Mac에서
"Failed to enumerate audio devices via PowerShell" 경고 + 1 device fallback이
발생하던 문제. process.platform 분기로 _getDevicesWindows / _getDevicesMac
helper로 분리하고, Mac은 system_profiler SPAudioDataType -json 을 파싱해
coreaudio_device_input > 0 인 항목만 input 디바이스로 추출.

macOS SoX 빌드는 실제 캡처 경로에서 -d(default) 고정이므로 deviceId는
'default'로 통일하고 label만 사용자 구분용으로 노출한다. Linux는 default
1개만 반환(pulseaudio/alsa 열거는 sox -d 로 충분).

Electron 재기동 후 로그: "Found 2 audio device(s) on darwin" 확인.
V2-5 플랫폼 감사 누락 항목 해소.
2026-04-11 23:18:18 +09:00
윤찬
412a2e71f9 feat(desktop): Voice Conversation 몰입 UX 패널 + 사운드 피드백 + Bug 13 빈 STT (빅뱅 Phase 5 Part 6)
listening 상태에서 풀 몰입 계측기 모드로 전환되는 VoiceRecordingPanel 추가.
recording-tip 팝업의 9바 cos-분포 waveform(BAR_COUNT=9, SMOOTHING=0.5,
RANDOM_FACTOR=0.35, 100ms)을 React로 포팅해 REC LED + elapsed 타이머 +
"SPEAK NOW" 힌트까지 구성. thinking/speaking 상태에서는 메시지 리스트로
복귀해 대화 맥락 유지 + 점 3개 typing indicator 버블 추가.

VoiceConversationService에 AudioCaptureService audio-level forwarding과
사운드 훅 4개(recording-start / recording-stop / chime / error)를 삽입.
chime은 recording-stop.wav 재사용(SoundEffectService SoundName 확장).
VOICE_CONVERSATION.AUDIO_LEVEL 채널 신설 + preload onAudioLevel API.

U8 Bug 13 동반 해소: finishListening에서 minBytes 미달 또는 VAD 무음 판정으로
빈 텍스트가 나오는 경우 조용히 listening으로 복귀하던 것을 _emitError('stt')로
사용자 피드백(에러 사운드 + 에러 이벤트)을 노출하도록 수정. 사용자가 "⏹ 눌러도
반응 없음"으로 오해하던 증상 해소.
2026-04-11 23:16:34 +09:00
윤찬
ee6ac3dbfc fix(desktop): Voice Conversation Mac 파이프라인 복구 — TTS 플랫폼 분기 + finishListening 스냅샷 순서 + Whisper 사전 로드 (빅뱅 Phase 5 Part 5)
Voice Conversation이 Mac에서 한 번도 end-to-end 동작한 적 없었음을 발견. 3개 독립 버그가
중첩돼 있었고, 진단 로그를 추가해 단계적으로 분해하여 모두 해결.

Bug 10.5 — TTSPlaybackService 'spawn powershell ENOENT' (Mac)
- 원인: _speakOne이 spawn('powershell', ...) 하드코딩. 파일 주석부터 "Windows SAPI (PowerShell)
  기반". V2-5 플랫폼 감사 때 이 파일 누락.
- Fix: _speakOne을 platform dispatcher로 변경, _speakOneMac(/usr/bin/say -r <wpm> --) +
  _speakOneWindows(기존 PowerShell) 분리. 공통 close/error 핸들러는 _bindProcessHandlers로
  추출. rate 매핑은 _getMacRate(180*speed, WPM 기준) / _getWindowsRate(-10~10, SAPI) 분리.
- 파급: 음성 대화뿐 아니라 TTS를 쓰는 모든 경로가 Mac에서 벙어리였음.

Bug 11 — VoiceConversationService.finishListening 오디오 버퍼 스냅샷 순서 (진짜 root cause)
- 증상: finishListening 호출 시 audioBuffer가 항상 0 bytes → "audio too short"로 조용히
  listening 복귀. STT가 한 번도 안 탐. Windows에서도 동일 버그였을 가능성 매우 높음.
- 원인: _stopListening() 내부에 this._audioBuffers = []로 리셋하는 라인이 있는데,
  finishListening이 concat을 stop 호출 **이후**에 수행 → 이미 빈 배열에서 concat.
  19초짜리 1.7MB 오디오가 매번 증발.
- Fix: _stopListening() 호출 **전**에 const audioBuffer = Buffer.concat(this._audioBuffers)
  스냅샷 저장. finishListening 내 redundant한 this._audioBuffers = [] 재설정도 제거(이미
  _stopListening이 수행).

Bug 12 — Whisper 모델 사전 로드 누락
- 증상: Bug 11 fix 후 STT 경로로는 진입하지만 LocalSTTService가 "모델 로딩 중, 오디오 버퍼에
  적재"로 pending 큐에 쌓기만 하고 아무도 로드를 트리거하지 않아 영원히 대기.
- 원인: MeetingMode/CaptionService는 startRecording 시 LocalSTTService.initialize()를 명시적으로
  호출하지만 VoiceConversationService는 이 호출 누락.
- Fix: startSession에서 void getLocalSTTService().initialize().catch(...) fire-and-forget.
  initialize는 같은 모델이 Ready면 즉시 return하므로 idempotent.

검증:
- desktop tsc --noEmit EXIT=0
- /usr/bin/say -r 180 "TTS 분기 테스트 성공" 직접 호출 → 스피커 소리 확인
- 사용자 재시연: Mic → 말함 → ⏹ Stop → 전사 → LLM 스트리밍 → TTS 소리 → 자동 listening
  복귀 연속 대화까지 end-to-end 통과

남은 과제 (다음 세션 U6/U8):
- Voice Conversation UX 몰입 패널 (9바 waveform + REC LED + 타이머) — 설계 완료
- Bug 13: 빈 STT 결과 시 사용자 피드백 부재 (현재는 조용히 listening 복귀 → 버튼 먹통처럼 보임)
2026-04-11 22:59:08 +09:00
윤찬
3b77be01bc refactor(desktop): IME composition 가드 helper 추출 + 8곳 Enter 핸들러 통합 (빅뱅 Phase 5 Part 5)
Bug 10 fix(MeetingModePage 인라인 가드)를 isImeComposingEvent helper로 추출하고,
한글 위험도 있는 나머지 7개 Enter 핸들러에 일괄 적용. 총 8곳이 이제 동일 helper 경유.

신규:
- apps/desktop/src/renderer/utils/keyboard.ts — isImeComposingEvent(e)
  JSDoc에 Bug 10 원리(Chromium이 IME 조합 중 Enter를 2번 발화) + 권장 사용 패턴 포함

적용 8곳:
- pages/MeetingModePage.tsx:164 회의 메모 (기존 인라인 가드 4줄 교체)
- pages/KnowledgeBasePage.tsx:84 RAG 쿼리
- pages/VoiceConversationPage.tsx:113 텍스트 채팅
- pages/CommandsPage.tsx:336 키워드 추가 (Enter+Esc)
- components/meeting/MeetingChatPanel.tsx:111 미팅 챗
- components/meeting/EditableSegment.tsx:70 전사 세그먼트 편집 (Enter+Esc)
- components/meeting/MeetingDetailTabs.tsx:242 미팅 타이틀 (인라인 arrow → 블록)
- components/shared/HistoryEntryCard.tsx:64 태그 추가 (Enter+Esc)

/simplify 패스 품질 리뷰:
- Phase 3.3 CloudSyncService.pushOne 훅 8곳은 이미 fire-and-forget 1줄로 일관.
  내부 try-catch가 에러 삼켜 로컬 write 차단 금지 철학 준수 → 수정 없음, 현 상태가 최적.

검증:
- desktop tsc --noEmit EXIT=0
- Vite HMR로 dev 프로세스 자동 반영 (재기동 없음)
- 한글 Enter 시연은 사용자 실측 대기 (VoiceConversationPage / MeetingDetailTabs 대표 2곳)
2026-04-11 22:06:16 +09:00
윤찬
96e23f57bc fix(desktop): 한글 IME Enter 중복 addMemo 가드 + Meeting pre-push 직접 실증 (빅뱅 Phase 5 Part 4)
Phase 5 Part 3에서 남겨둔 U2(Fix 1 직접 시연 미수행)를 해소하고,
실증 중 발견한 한글 IME Enter 중복 addMemo 버그(Bug 10)를 같이 픽스.

## Bug 10 — 한글 IME Enter 중복 addMemo
- 증상: 실증 녹음 중 한글 메모 "메모 기능이 잘 작동하는지 봅니다"가
  2ms 간격으로 2번 저장 (로그 21:47:57.410/.412, UUID c7f0db45 + a6e442cc).
  5개 메모 중 이 하나만 중복 — 조합이 아직 확정 안 된 상태에서 Enter를
  친 메모에서만 발생.
- 원인: 한글 IME 조합 중 Enter → Chromium이 keydown 2번 발화
  ((1) IME 조합 확정 isComposing=true/keyCode=229,
   (2) 실제 Enter 액션). MeetingModePage.tsx:164-174의 handleMemoKeyDown이
  e.key === 'Enter' && !e.shiftKey만 검사하고 isComposing을 무시해
  두 이벤트에서 모두 handleAddMemo() 발화 → IPC 2회 → addMemo() 2회.
- 수정: MeetingModePage.tsx:168에 이중 가드 추가.
  isComposing은 모던 표준, keyCode === 229는 조합 확정 시점에
  isComposing이 false로 내려가면서 keyCode만 229로 주는 Chromium
  엣지 케이스 호환.
- 검증: Vite HMR로 즉시 반영 후 재시연 녹음(78421e75)에서 한글 메모
  5건 연속 → 전원 1:1 매핑, 중복 0.

## Fix 1 (Phase 3.3 startRecording pre-push) 직접 실증
재시연 녹음 78421e75 (21:51:24~21:51:50, 약 26초) 로그 타임라인:

  21:51:24.937  pushOne meetings/78421e75 ok      <- Fix 1 pre-push (676ms)
  21:51:30.499  pushOne meeting_memos/4ed7e59a ok <- addMemo #1 (113ms)
  21:51:32.202  pushOne meeting_memos/de011c15 ok <- addMemo #2 (78ms)
  21:51:35.893  pushOne meeting_memos/3184fe5e ok <- addMemo #3 (66ms)
  21:51:38.263  pushOne meeting_memos/00f041ef ok <- addMemo #4 (82ms)
  21:51:40.740  pushOne meeting_memos/0808c4d2 ok <- addMemo #5 (64ms)
  21:51:50.681  pushOne meetings/78421e75 ok      <- _runPostProcessing (108ms)
  21:51:50.709  pushOne history/70388090 ok       <- CaptionService→History

meetings/78421e75가 녹음 시작 + 종료 두 번 push된 것이
Fix 1의 직접 증거 — 부모 row가 메모 push 시점에 이미 Supabase에
존재했기 때문에 메모 5건이 RLS/FK 차단 없이 통과. Bug 7 재현 방지 확인.

## 잠재 IME 버그 7곳
동일 패턴의 Enter 핸들러 7곳에 가드 없음 (KnowledgeBasePage:85,
VoiceConversationPage:114, MeetingChatPanel:113, EditableSegment:72,
MeetingDetailTabs:242, HistoryEntryCard:65, CommandsPage:337).
다음 세션 /simplify 패스에서 일괄 처리 예정.

## 검증
- desktop tsc --noEmit EXIT=0
- Meeting 녹음 2회차 실증 통과

memory/project_status.md SaaS [11] 섹션 추가.
2026-04-11 21:58:20 +09:00
윤찬
046ac857cb feat(desktop+web): Phase 3.3 Auto push on write + transcript fallback + dashboard Link (빅뱅 Phase 5 Part 3)
Desktop — CloudSyncService:
- pushOne(table, id) 제네릭 단건 push 신규 (history/dictionary/meetings/meeting_memos/meeting_documents)
- 로그인 미상태 silent noop, 실패 시 warn만 (로컬 write 절대 차단 금지)
- 5개 row→payload 매퍼(_mapHistoryPayload 외 4개) private 메서드로 추출, pushAll과 공유 (DRY)

Desktop — write hook 8곳 fire-and-forget 연결:
- HistoryService.create / generateTitle
- MeetingSummaryService.generateSummary (summaryText 저장 후)
- DictionaryService.add / update
- MeetingModeService.startRecording (부모 meeting pre-push — 자식 memo/doc RLS 통과용)
- MeetingModeService.addMemo / generateDocument
- MeetingModeService._runPostProcessing (완료 상태 최종 저장 후)

Web — meeting detail page:
- transcripts 테이블이 비고 status!=recording인 경우 meeting.raw_transcript(또는 edited_transcript) 를 whiteSpace:pre-wrap으로 fallback 렌더
- 실시간 구독 경로(LiveTranscriptList)는 recording 상태 또는 transcripts 존재 시에만 마운트

Web — dashboard:
- RECENT MEETINGS의 MetalCard를 next/link Link로 감싸 /meetings/[id] 이동 가능
- cursor pointer + hover lift 효과

실증: 재기동 직후 pushAll 캐치업이 이전 세션 RLS 실패 메모 2건을 재시도 → pushed=4 성공.
새 녹음 → pushOne history/... ok 실시간 발화 → 웹에서 전사/메모 렌더 확인 (사용자 "잘 전사와 메모가 올라왔어")
2026-04-11 21:34:02 +09:00
윤찬
e55687d298 fix(desktop+supabase): 로컬 ID UUID 통일 + Realtime publication (빅뱅 Phase 5 Part 2)
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 최초 성공 
2026-04-11 18:46:22 +09:00
윤찬
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
윤찬
46b77f1118 feat(desktop): SaaS [5] 티어 enforcement — 로컬 해방 + 클라우드 gate (빅뱅 Phase 4)
비즈니스 모델 확정:
- 익명 로컬 사용자 → 로컬 기능 16개 전부 무제한 (entry point)
- 클라우드 기능 (PREMIUM_LLM, CLOUD_SYNC) → 로그인 필수
- 로그인 free → PREMIUM_LLM 5/일, CLOUD_SYNC 허용
- 로그인 pro → PREMIUM_LLM 500/일 (fair use cap)
- 로그인 pro_plus → PREMIUM_LLM 무제한

기존 정책 폐기:
- DICTATION 20/일 quota 삭제
- LLM_PROCESS 10/일 quota 삭제
- LIVE_CAPTION/VOICE_MEMO/SCREEN_CONTEXT/VOICE_COMMAND/LLM_CHAIN
  pro lock 해제 → free
- FILE_TRANSCRIPTION/VOICE_CONVERSATION/DICTATION_TEMPLATE/
  MEETING_SUMMARY/LOCAL_RAG/OS_AUTOMATION pro_plus lock 해제 → free

types.ts:
- Feature enum: PREMIUM_LLM, CLOUD_SYNC 추가
- FeatureAccess.reason: 'login_required' 추가

LicenseService.ts:
- QUOTA_LIMITS / FEATURE_MIN_TIER 전면 재작성
- CLOUD_FEATURES Set 신규
- canUse(): CLOUD_FEATURES && isLocalMode() → login_required 조기 반환
- syncFromCloud(tier): Supabase subscriptions 티어를
  electron-store에 캐시 + tier-changed emit
- resetToFree(): 로그아웃 시 pro/pro_plus 캐시 + 라이센스 필드 전부 null
- isLocalMode() import 추가

CloudSyncService.ts:
- _fetchSubscriptionTier(userId): subscriptions 테이블에서 활성 티어 조회
  (row 없음 / 비정상 값 → free 폴백)
- _onAuthenticated: DB open → tier fetch → syncFromCloud → auth-changed → Realtime
- _onSignOut: Voice/Meeting/Caption stop → Realtime stop → auth.signOut →
  token clear → license.resetToFree → closeCurrent → openLocal → emit null

검증:
- desktop tsc --noEmit 
- desktop npm run build 
- 로컬 사용자 모든 기존 기능 접근 가능 (pro lock 완전 제거)
2026-04-11 10:23:50 +09:00