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.
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.
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 ≠ 런타임 안전.
- press가 STT 초기화를 await하며 action queue 점유 → release 수십 초 지연·
유령 세션 반복 버그 수정 (initSTT fire-and-forget)
- 프리플라이트: STT 모델 미설치 시 즉시 에러 + 메인 UI 경고 + 온보딩 오픈
- 사이드카: 기동 중 프로세스 사망 시 30초 대기 없이 즉시 실패,
restartCount 리셋, error 리스너 부재 미처리 예외 방지
- 실시간 부분 전사: 1.5s 간격 interim → RecordingTip에 말하는 내용 미리보기
- Ollama 미가용 후처리 스킵 시 warning 배너, voice:error 브로드캐스트 신설
- 앱 아이콘: icon.svg 마스터(d3ro 브랜드 — 메탈 섀시+오렌지 웨이브) → png/ico
생성 스크립트(generate-icons.mjs, sharp) + electron-builder win/mac 연결
- 트레이: createEmpty() 빈 아이콘 → 실제 앱 아이콘 (getAppIconPath)
- 마이크 테스트: 초기 무음 level:0을 종료로 오인하던 조기 종료 버그 —
AUDIO.TEST_LEVEL에 done 플래그 신설(SSOT), STOP 버튼이 실제 캡처 중지,
testDevice 실패 시 상태 롤백
- verifying/manifest 상태(바이트 진행률 없음)에서 인디터미넌트 바 + 친화적 문구
— 9.6GB 검증 ~2분간 멈춘 것처럼 보이던 문제 해소
- OnboardingModal 중복 실행 가드 (runningRef)
- LocalLLMService.pullModel: 동일 모델 동시 pull은 기존 promise 합류
- LocalSTTService.downloadModel: /download 409는 실패가 아닌 기존 진행 합류
RN 0.85 + React 19 기반 apps/mobile-rn 프로젝트 생성.
6개 화면 이식 (Login, Dash, History, Record, Talk, Settings).
@react-navigation/native + bottom-tabs 네비게이션 구성.
Windows NDK CMake libc++_shared 링크 버그 워크아라운드 포함.
Galaxy Fold (SM_F956N) 무선 디버깅 APK 설치 확인.
DS 컴포넌트 9개 (ScreenPanel, WaveBars, AppStatusBar, Header, FilterChip 신규),
5개 탭 + 로그인 화면 D3RO 인스트루먼트 미학 적용,
i18n 연결 (I18nProvider + 모바일 전용 키 80+개),
모노레포 workspaces에 apps/mobile 추가.
- OPEN_BILLING: app.isPackaged 기반 dev/prod URL 분기
- CloudSyncService: stored token 없을 때 auth-changed null emit 누락 수정
- Payple Edge Functions 3개 배포 ACTIVE 확인
## 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.
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')로
사용자 피드백(에러 사운드 + 에러 이벤트)을 노출하도록 수정. 사용자가 "⏹ 눌러도
반응 없음"으로 오해하던 증상 해소.
이번 세션 범위:
- Part 5-A: /simplify 패스 — isImeComposingEvent helper 추출, 8곳 IME 가드 통합 (커밋 3b77be0)
- Part 5-B: Voice Conversation 파이프라인 복구 — Bug 10.5/11/12 3개 버그 연쇄 fix (커밋 ee6ac3d)
- Part 5-C: Voice Conversation UX 개선 인터뷰 설계 — 몰입 패널 + 사운드 피드백 + 상태 분기
(ASCII 와이어프레임 + 구현 작업 리스트 handoff에 저장, 다음 세션 이월)
- Part 5-D: Bug 13 신규 발견 — 빈 STT 결과 시 피드백 부재 (U6 몰입 패널과 함께 처리 예정)
갱신 파일:
- memory/project_status.md — SaaS [13] 엔트리 (Bug 10.5/11/12/13, UX 설계 스펙)
- memory/handoff-latest.md — Part 5 전체 요약 + Part 5-C 와이어프레임 + 다음 세션 프롬프트
- memory/handoff-archive/2026-04-11-2200-phase5-part5a-simplify.md — 직전 Part 5-A 스냅샷 아카이브
다음 세션 우선 작업:
1. Voice Conversation 몰입 패널 구현 (handoff의 Part 5-C 작업 리스트 8번까지)
2. Bug 13 피드백 훅 추가 (몰입 패널과 함께)
3. Phase 3.2 PREMIUM_LLM (선행: Anthropic/OpenAI API key 주입)
빅뱅 성공 기준: 8개 중 7개 완료 (Phase 3.2 PREMIUM_LLM만 잔존).
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곳)
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] 섹션 추가.
SaaS [9] 갱신 + handoff-latest 재작성.
## 이 세션 성과
- Bug 4/5/6 구조 픽스 (nanoid→UUID, realtime publication, setAuth 명시)
- 실증 A/B/C/D/E 전원 통과 — 3 클라이언트 데이터 일원화 end-to-end 검증 완료
- 로컬→클라우드 push 최초 성공 (실증 B pushed=1, 실증 E pushed=5)
- 웹 Dashboard에 데스크톱 미팅 row 반영 스크린샷 캡처 (RECENT MEETINGS "(제목 없음) 2026.4.11 오후 7:09:21")
- 빅뱅 성공 기준 8개 중 7개 완료 (PREMIUM_LLM만 미구현)
## 미해결 (다음 세션 이월)
- Phase 3.3 Auto push on write — 수동 Push 버튼 대신 세션 완료 즉시 자동 push
- U1: Realtime 채널 TIMED_OUT 근본 원인 (블로커 아님, Bug 5+6 적용 후에도 재현)
- U2: 웹 Dashboard Recent Meetings 카드 클릭 → 상세 이동 안 됨 (사용자 피드백, 해석 확인 필요)
- U3: package-lock.json 정리 지연 (rollup-win32 optional dep로 npm install Mac 실패)
다음 세션 시작 프롬프트는 memory/handoff-latest.md 말미에 그대로 붙여넣기 가능.
아카이브: 이전 handoff는 memory/handoff-archive/2026-04-11-1800-phase5-part1-wrap.md로 이동.
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 최초 성공 ✅
빅뱅 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 시작.
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 추가