d3ro-voice/memory/project_status.md
Yun Chan db8d9448a3
Some checks failed
deploy-site / deploy (push) Failing after 28s
docs(release): record the v1.6.0 publish
Notes the canonical feed, runtime and portable aliases serving 1.6.0 with the
installer hash verified, the site reporting commit 2fe20fa / 1.6.0, the new
minimum-runtime rule that spares 1.5.0 engines a re-download, and the local
packaging trap where a Node-ABI better-sqlite3 ships an installer without a
database.
2026-09-24 20:12:04 +09:00

170 KiB
Raw Blame History

D3RO-VOICE 프로젝트 현황

v1.6.0 릴리스 완료 (2026-09-24)

  • 버전 SSOT 1.6.0(android/iOS 1060000). 커밋 2fe20fa, annotated v1.6.0, chanpaca·origin main+tag push.
  • 내용: 다음 문장 제안 순차 생성 최대 12개·3개씩 페이지(Ctrl+Alt+↑↓/←→/Enter, Esc), 오버레이 재설계, 개인 문구 코퍼스 학습 제외(터미널·에디터·Agent Switchboard·비문장·플레이스홀더) + 기존 데이터 정리, 제안 단축키→받아쓰기 오작동 수정, 사이드카 최소 버전 1.5.0 재다운로드, Ollama 러너 콘솔 창·사이드카 이중 기동 수정, 자막 창 드래그 이동·준비 안내.
  • 무서명 로컬 게시: runtime/portable 1.6.0, latest.yml 1.6.0(95,628,392 bytes, sha512 일치, 익명 206), 사이트 release-identity = 2fe20fa/1.6.0.
  • 다음: 스트리밍 자막(1.7.0) — 1초 주기 롤링 인식 + LocalAgreement 확정 + 무음 시 문장 마감 + gemma 문맥 다듬기, large-v3-turbo 권장, Qwen3-ASR 실험.

v1.5.0 릴리스 완료 (2026-09-23) 🚀

  • 버전 SSOT 1.5.0(android/iOS 1050000, releaseDate 2026-09-23). annotated 태그 v1.5.0 생성 후 chanpaca(canonical)와 origin(legacy mirror) main+tag push 완료.
  • 게이트: version:check GREEN, release:metadata GREEN, security:secrets GREEN, check:design 0 violations, lint clean, typecheck clean, 데스크톱 빌드/check:desktop-renderer(7 pages) GREEN. 테스트는 native ABI 미스매치 구성에서 366 failed | 1099 passed (1465) — 실패 수는 기존 베이스라인과 동일(365 ABI + 1 stale sidecar 메시지, GAP-QA-02)로 신규 실패 0.
  • 인증서가 없어 CI 서명 파이프라인 대신 무서명 로컬 게시: sidecar:build(UIA/uiautomation/comtypes 포함) → release:portable:build → release:portable(runtime-latest + portable-latest) → release:updater --build --ack-unsigned.
  • 검증된 라이브 상태: canonical feed latest.yml=1.5.0(설치본 D3RO-Voice-Setup-1.5.0-x64.exe 95,612,108 bytes, sha512 일치, 익명 206), runtime-latest/runtime.json·portable-latest/portable.json=1.5.0(원격 sha256 = 로컬), 사이트 d3ro.chanpaca.net/release-identity.json=commit 5c11ee2/1.5.0.
  • 게시 중 portable 별칭(runtime-latest 등)의 "동일 파일 건너뛰기"가 1MiB 초과 파일을 크기만 비교해 낡은 .001과 새 .002가 섞이는 사고를 발견 → publish-portable-release.mjs를 파일 목록 API sha256 비교 + 별칭 버전 원자적 재게시로 수정(GAP-REL-12).
  • 사이트 CI(deploy-site.yml)는 CF_API_TOKEN 시크릿 부재로 여전히 실패(GAP-REL-09b). 이번에는 인증된 로컬 wrangler로 수동 배포했다.
  • 남은 검증: 실제 설치본 자동 업데이트 E2E, Electron GUI에서 제안 오버레이/수락 삽입/비밀번호 차단(GAP-INPUT-07).

Local Flow Intelligence 10-pack — 문서/자동 증거 handoff (2026-09-22)

  • 데스크톱 기존 InputTelemetry/Suggestion/UI 확장으로 Flow Radar, Edit Friction, App DNA, App Quality, Privacy Receipt, Smart Exclusion, Why This Suggestion, Memory Decay, Instant Recall, Shortcut Safety Audit을 구현했다. raw key events/key codes/content stream은 저장하지 않지만, 동의한 학습 텍스트는 typing_samples/personal_phrases에 별도 정책으로 남을 수 있다. personal_phrases는 나이 기반 자동 만료가 없고 개별 삭제·전체 삭제·동의 철회로 제거된다.
  • 이 handoff 시점의 자동 증거는 domain 9/9 + service 12/12 = targeted 21/21, desktop build passed다. 이는 strict/full suite 전체 또는 GUI 증거가 아니다.
  • 재시작 가능한 다음 게이트: 외부 터미널에서 run-desktop.bat을 실행해 편집 가능/읽기 불가/비밀번호 앱, flow·friction·app-quality 화면, receipt count와 failure, exclusion recommendation, provenance·local fallback, shortcut audit, 좁은 overlay geometry를 수동으로 확인한다. 전체 strict/full suite와 Electron GUI 검증은 별도 미완료 게이트로 유지한다.

입력 인텔리전스 — 속도 진단 정정 + 토큰 스트리밍 (2026-09-22 09:40~09:55) ⚡

사용자 지적: "gemma4 로컬에서 엄청 빠르지 않나". 맞았고 내 이전 측정이 틀렸다.

재측정 (ollama ps / /api/generate, 이 머신)

항목 값
로드 상태 gemma4:e4b 3.2GB 100% GPU, 컨텍스트 4096
단문 생성 126~137 tok/s
제안형 프롬프트(400자 접두 + 64토큰) wall 1.11초 (load 0.56 + prompt 0.06 + eval 0.48)

→ 앞서 기록한 24.7초(콜드)/4.9초(32토큰)는 Ollama 기본 keep_alive 5분 때문에 매 요청 모델을 다시 올린 비용이었다. 내 측정 스크립트가 load_duration 을 분리하지 않아 지연 전체를 모델 탓으로 오진했다. (keep_alive: 30m 적용 후 부팅 워밍업 1.8~2.5초, 요청 ~1초.)

UX 변경 (사용자 요청: "계속 생성되서 올라왔으면")

  • 트리거 1000 → 300ms (Continue 350 / Tabby 250 / twinny 300 구간 복귀).
  • 토큰 스트리밍: 생성 중 도착하는 대로 오버레이에 흘려보낸다(120ms 간격 방출, partialText), 팝업은 깜빡이는 커서와 함께 부분 텍스트를 표시.
  • 지속 갱신: 접두가 12자 이상 자라면 다시 생성(SUGGESTION_REGENERATE_GROWTH_CHARS) — "한 번 뜨면 끝" 이 아니다.
  • 토큰 상한 48, 몇 초 멈출 필요 없이 치면서 갱신된다.

검증: 유닛 70건 GREEN, lint/design clean, build 성공, typecheck:strict 로 이 작업 파일 오류 0건(main 13 / renderer 35 는 선재). 앱 09:52 재기동 + 워밍업 1.8초.


입력 인텔리전스 — 실사용 피드백 4건 수정 (2026-09-22 11:00~11:15) 🔧

사용자 신고

  1. Ctrl+Alt+화살표 를 누르면 음성 전사가 뜬다 → 후보 탐색 불가
  2. X를 눌러도 생성 중이면 안 꺼진다
  3. 입력창을 클릭만 해도 제안이 만들어진다
  4. 엔터로 입력을 끝낸 뒤에도 계속 제안한다

원인과 수정

  • AltGr 함정: 오른쪽 Alt 는 Windows 가 Ctrl+Alt 로 보낸다. 저장된 바인딩이 command = Ctrl + RightAlt 였으므로 Ctrl+Alt+화살표에서는 Alt 를 누르는 순간 command 가 발동해 음성 파이프라인이 돌았다. → 수정자만으로 끝나면서 Ctrl+Alt 가 함께 눌린 트리거는 280ms 보류하고, 그 사이 다른 키가 이어지면 취소(+ released 도 억제). 받아쓰기(Alt 단독)·Alt+Shift 계열은 모양이 달라 영향 없음.
  • 닫기 무효화: 진행 중이던 생성이 나중에 완료되며 결과를 다시 표시했다. → 생성 세대 토큰을 두고, 닫으면 증가시켜 늦게 온 결과를 폐기.
  • 클릭만으로 제안: 백그라운드 샘플러가 마우스 이벤트에도 돌았다. 유휴 시간도 _lastEventAt(키+마우스) 기준이었다. → 키보드 기준(_lastKeyAt) 으로 게이트.
  • 엔터 후 반복: 엔터 뒤 UIA 가 옛 텍스트를 돌려주면 같은 텍스트로 재요청했다. → 동일 접두 가드(같은 텍스트는 다시 만들지 않음, 더 치면 자연히 새로 생성, 수동 "지금 제안 받기" 는 우회).

함께: 직전 배치에서 KeyBindingService 의 emit 블록에 중복 브레이스가 생겨 빌드가 깨졌던 것을 복구하고, AltGr 취소 키를 서비스 내부 bindingKey 와 일치시켰다(불일치 시 release 만 남아 음성이 오작동할 수 있었다). 검증: 데스크톱 유닛 73 + core 117 GREEN, lint clean, build 성공, 11:10 재기동.


입력 인텔리전스 — 실사용 성공 + 연속 샘플링 (2026-09-22 10:40~11:00) ✅

사용자 피드백: ①계속 치면 잘 안 나옴(백그라운드에서 부단히 돌아야) ②후보 2개는 적다, 5개+스크롤 ③워밍업/생성 중에도 반응이 보여야.

근본 원인 (치명적): 스냅샷이 "타이핑을 멈춘 뒤"에만 예약돼 있어(디바운스 타이머가 키 입력마다 리셋), 에이전트 개발처럼 끊김 없이 치면 샘플이 하나도 만들어지지 않았다. → 최근 입력이 있는 동안 800ms 주기로 도는 백그라운드 샘플러를 추가.

함께 고친 것

  • 후보 2 → 5개, 토큰 48 → 128, 스트리밍 유지. 오버레이 높이 96 → 240 + 목록 내부 스크롤.
  • 워밍업 스피너: 모델 적재 중이면 오버레이에 스피너 + "모델 준비 중" 을 띄운다(무반응 = 고장으로 보이던 문제).
  • 생성 스피너: 후보 도착 전에도 스피너 + 도착한 부분 텍스트(깜빡이는 커서).
  • 재생성 최소 간격 1500 → 900ms (지속 갱신 체감).
  • 터미널: Windows Terminal 은 TextPattern 은 있으나 ${b('GetSelection()')} 이 0개라 "편집 불가" 로 판정되던 것을, TextPattern 존재 기준으로 변경.
  • 통계 UI: inner tab(요약/키보드/마우스/앱/문구·제안) 재구성 + 막대 그래프를 축·라벨·고정폭으로 다시 구현(1일치 데이터에서 거대 사각형이 되던 버그). 통계는 지식 베이스 > 입력 인사이트 로 이동, 설정 > 입력은 동의·정책·진단만.

검증 (10:57, 카톡 타이핑 중 실제 로그)

  • 스냅샷 67건/50초 · 제안 생성 3회 · 실패 0건 · 삭제 20건
  • 제안 5개 생성 (1436ms, model=gemma4:e4b) — 조합 중(comp=true, idle=25ms)에서도 생성됨

입력 인텔리전스 — 실기동 검증에서 잡은 결함 6건 (2026-09-22 09:00~09:20) 🔧

사용자가 실제로 앱을 띄우고 타이핑한 로그로 검증하면서, 유닛 테스트가 못 잡는 결함을 순서대로 잡았다.

측정/관찰 (실제 로그)

  • UIA 자체는 정상: Notepad 는 edit=true src=value len=4000 anchor=yes, WindowsTerminal 은 not-editable 로 정확히 거부.
  • KakaoTalk 은 legacy/len=0 → 이후 edit=true src=value 인데도 len=0 → 커스텀 렌더 앱은 텍스트를 노출하지 않는다(결함 아님).
  • 제안 생성은 시도됐으나 "LLM generation cancelled" — 6초 제한이 워밍업 후 4.9초 요청까지 잘라냈다.
  • 워밍업+keep_alive 적용 후 모델 재적재 2.5초(콜드 24.7초 대비).

고친 결함

  1. settle 게이트 미개방(치명적): 트리거 지연(1000ms)이 스냅샷 디바운스(700ms)보다 커 "멈춘 뒤" 조건이 열리지 않았다 → settle 스냅샷 추가.
  2. 부팅 시 워밍업 미실행: 이미 켜 둔 설정으로 켜면 워밍업이 안 돌았다(가용성 폴링 전 early return) → 부팅 워밍업 + 대기 재시도(2초×15).
  3. 케어렛 폴백 부재: 오프셋이 없는 앱에서 문서 전체를 접두로 썼다 → tail 400자 + 진단 플래그.
  4. 오버레이 지연 표시: 후보 도착 후에야 떠서 "아무것도 안 나옴" 으로 보였다 → 요청 즉시 "생성 중" 표시.
  5. 느린 하드웨어 대응: 컨텍스트 600→400자, 토큰 96→64, 후보 3→2, 응답 제한 6→12초, 트리거 320→1000ms, keep_alive 30m.
  6. 진단 불가: 왜 안 뜨는지 알 수 없었다 → 설정 > 입력에 실시간 진단 줄(포커스 앱·읽기 가부·소스·글자 수·비밀번호/케어렛 폴백), 12개 로케일 8키.

검증 도구 주의: 데스크톱 npm run typecheck 는 no-op(GAP-INFRA-04)이라 이번에도 "clean" 이 거짓이었다. typecheck:strict 로 이 작업이 만든 타입 오류 7건을 고쳤다(main 13 / renderer 35 는 전부 선재). 유닛 70건 GREEN.

남은 검증: 오버레이 위치·외관, Alt+Shift+← 수락 삽입, 비밀번호 필드 차단, 주간 수치 정확도, PyInstaller 번들에 uiautomation 포함 여부.


입력 인텔리전스 — 입력 수집 + 다음 문장 제안 (2026-09-21) ⌨️

"일본어 IME 처럼 다음에 칠 문장을 커서 옆에 제시" 요청. 뇌피셜 대신 조사 먼저 지침으로 받았다.

조사로 확정한 사실 (설계 근거)

  • 케어렛 위치: GetGUIThreadInfo.rcCaret(AutoHotkey CaretGetPos 방식)은 Chromium/Electron/VS Code 에서 아무것도 돌려주지 않는다(자체 커서 렌더). 정본은 UIA TextPattern.GetSelection() → ExpandToEnclosingUnit(Character) → GetBoundingRectangles(). Chromium 은 ITextProvider 는 구현하지만 ITextPattern2::GetCaretRange 는 미구현.
  • 입력창 읽기: ValuePattern.Value → TextPattern.DocumentRange → LegacyIAccessiblePattern 순서, 비밀번호는 UIA_IsPasswordPropertyId(30019) 로 읽기 전에 차단. 트리 전체 순회 금지 (PowerToys #46385: Chrome/VS Code UIA 트리 워크 10~30초).
  • 한/일 IME: 키코드로 텍스트 복원은 원리적으로 불가능(조합 결과가 텍스트). 그래서 UIA 로 커밋된 텍스트를 읽어 스냅샷 diff(최장 공통 접두/접미)로 계산한다. 조합 중에는 통계·제안을 모두 억제한다.
  • Node 후보 비교: koffi 3.3.1(N-API prebuild) 채택. selection-hook(선택 스트만·케어렛 없음), @crowecawcaw/xa11y(caret rect 없음·IsPassword 미검사), get-windows(ESM + install script 필요 → 이 저장소 정책과 충돌), node-ffi-napi(2021년 이후 방치) 배제. UIA COM vtable 을 JS 로 직접 다루는 것은 크래시 위험으로 배제.
  • 입력 수집 정책: ActivityWatch aw-watcher-input 그대로 — presses/clicks/deltaX/deltaY/scroll, 키 내용 미저장, 5초 heartbeat. 마우스 거리는 축별 절대값 합(맨해튼).
  • 제안 타이밍: Continue 350 / Tabby 250(adaptive) / twinny 300 / minuet 400 ms, Zed p50<200ms, 출력 토큰 Tabby 64 / KeyType 4~16 → 기본 320ms·96토큰·후보 3개.

구현

  • core SSOT packages/core/src/input-intelligence.ts(정책·집계·정제·앵커 전부 순수 함수), IPC 3그룹 (INPUT_TELEMETRY/SUGGESTION/POPUP_SUGGESTION), 키바인딩 션 3개(suggestion-accept/next/dismiss).
  • 메인: InputTelemetryService(uiohook + koffi 포그라운드 창 + UIA diff 학습), UiaContextService(사이드카 브리지, fail-closed+백오프), SuggestionService(디바운스→buildSuggestionPrompt→streamGenerate(호출자 시그널 취소)→수락 시 TextInsertService 삽입), global-input-hook.ts(uiohook 참조 카운트 — KeyBindingService 와 공존).
  • 사이드카: uia_bridge.py + GET /uia/focus(전용 COM 스레드, uiautomation 2.0.29). 실측 워 015ms / 최초 200590ms.
  • DB: input_activity(시간×앱), typing_samples, personal_phrases, suggestions + 30일 보존 정리.
  • UI: 설정 > 입력 탭(동의·정책·주간 인사이트·문구 관리), 대시보드 주간 카드, suggestion-overlay 팝업(케어 앵커, 클릭 통과 기본).
  • 검증: 유닛 48건(input-intelligence.test.ts 44 + llm-prompts.test.ts 4) GREEN, typecheck/lint/design GREEN, Electron ABI 전체 실행에서 신규 실패 0건(베이스라인 366 failed/994 passed → 366 failed/1042 passed). 사이드카 /uia/focus 실제 호출 확인(200, 포커스 요소 반환).

라이브 실측 (실제 Ollama + 프로덕션 프롬프트, 2026-09-21)

  • 프롬프트/품질은 정상: 한국어 "오늘 회의에서 논의한 내용을 정리해서" → "관련 자료와 함께 다시 한번 정리해서 보내드릴게요." (후보 3개 전부 정제 통과), 영어도 자연스럽게 이어졌다.
  • 속도가 예산 밖: gemma4:e4b 96토큰 한도에서 콜드 24.7초 / 워 4.9초(32토큰). Zed 목표 p90<500ms 와 두 자릿수 차이. → 완화 3종을 넣었다: 활성화 시 워밍업(1토큰), keep_alive: 30m (Ollama 기본 5분 → 재콜드로딩 방지), 응답 제한 (기본 6초, 설정 > 입력에서 조절, 초과 시 그 요청을 버리고 다음 타이핑 주기에 재시도). 설정 패널이 마지막 응답 시간을 보여주고 1.5초 초과면 경고해 사용자가 더 작은 모델(suggestionModelId)을 고를 수 있다. (GAP-INPUT-06)

남은 것 (GAP-INPUT-01~06, GAP-LLM-03): 실 타이핑 검증(수락 삽입·오버레이 위치·비밀번호 차단), PyInstaller 번들에 uiautomation/comtypes 포함 확인, IME preedit 미수집, 중국어 단어 수 과소, macOS/Linux 미지원.


v1.3.3 — 배포본 시작 실패(네이티브 ABI) + gemma4 기본값 (2026-09-18) 🛠️

배포한 1.3.2 설치본이 시작 즉시 죽었다: NODE_MODULE_VERSION 131 ... requires 130.

  • 원인: 로컬에서 npm install(ffmpeg/tar)을 돌리면서 better-sqlite3가 **Node ABI(131)**로 재빌드됐고, 패키징이 네이티브 재빌드를 건너뛰어(npmRebuild=false) 그 모듈이 설치본에 들어갔다. 또 그 설치는 예전 경로(Programs\@d3rodesktop)를 덮어써 1.0.0을 망가뜨렸다.
  • 대책: scripts/ci/verify-native-abi.mjs(패키징된 모듈이 Electron ABI인지 호스트 Node 로드 거부로 판별) + fix-native-abi.mjs(로컬 잠금 우회 주입). 설치본은 검증된 트리에서만 생성(--prepackaged). GitLab/Forgejo/GitHub 패키징 단계에 게이트 삽입. 검증: 1.3.3 설치본을 7za로 풀어 그 안의 바이너리로 재검증 → Electron ABI GREEN, 피드 sha512가 로컬 빌드와 일치.
  • 함께 수정: 로컬 모델 기본값이 레거시 *.gguf였고 UI가 gemma2:2b를 추천하던 것을 gemma4:e4b로 통일(ConfigService 기본값, LocalLLMService 폴백/상태, 설정·온보딩·Ollama 가이드).
  • publish-*의 "동일 파일 건너뛰기"가 크기만 비교해 버전만 바뀐 latest.yml을 놓쳤다(피드가 1.3.2로 남는 사고) → 1MiB 이하는 내용까지 비교하도록 수정.
  • 배포: 1.3.3 업데이터 피드 게시 완료(latest.yml = 1.3.3). 기존 설치본은 1.3.3을 1회 수동 설치하면 이후 자동.

v1.3.2 — 자동 업데이트 복구: 런타임을 설치본에서 분리 (2026-09-18) 🔄

자동 업데이트가 왜 안 되는지 끝까지 추적했다. 원인은 서명만이 아니라 업로드 크기 한도였다.

실측한 사슬

  1. canonical feed는 Cloudflare 뒤에 있고 업로드 본문이 100MiB를 넘으면 HTTP 413으로 거부한다 (60MiB → 201, 110MiB → 413. chunked 업로드도 413).
  2. 사이드카 엔진(242MB)을 포함한 NSIS는 189MB → 인증서가 있어도 latest.yml을 게시할 수 없다.
  3. 게다가 이 PC의 설치본(1.0.0)은 app-update.yml이 legacy GitLab mirror(0.2.1-alpha)를 봐서 어떤 릴리스가 나와도 스스로 올라오지 못한다 → 1회 수동 설치 필수.

해결

  • RuntimeProvisioner 신설: 설치본에서 엔진/ffmpeg를 빼고, 처음 필요할 때 feed에서 부품 단위로 내려받아 SHA-256(부품 + 결합본) 검증 후 tar 해제 (%APPDATA%/d3ro-voice/runtime). 실패 시 부분 설치 정리, 동시 요청 공유, 진행률 이벤트(runtime:progress).
  • 패키징: extraResources/asarUnpack에서 엔진·ffmpeg 제거 + electronLanguages: ko, en-US. 설치본 688.5MiB → 345.4MiB, NSIS 189MB → 90.6MiB (한도 통과).
  • 런타임 번들 게시: runtime-<v>/runtime-latest (sidecar 94.4MiB = 2부품, ffmpeg 21.7MiB = 1부품). tar.gz는 242MB → 94.4MiB로 줄어 첫 설치 다운로드가 116MiB로 끝난다.
  • 설정 > STT에 런타임 상태 + 내려받기 버튼 추가(진행률 표시).
  • 업데이터 게시 스크립트(release:updater) 추가: 100MiB 가드 + 409 대응(메타데이터는 교체) + 무서명 게시는 --ack-unsigned 명시 승인 필수(정책 예외를 조용히 만들지 않는다).

검증: 설치본 90.6MiB → canonical feed latest에 게시 완료(latest.yml = 1.3.2, 익명 다운로드 확인). 실제 feed로 런타임 통합 테스트 GREEN(엔진 94.4MiB/17초, ffmpeg 21.7MiB/5초 → 실행 파일 + _internal 확인). strict typecheck 신규 오류 0(기존 38건은 무관), lint clean, version/metadata/secret 게이트 GREEN.

남은 것(외부): 인증서. 지금은 무서명으로 stable 게시한 예외 상태(GAP-REL-06)라 인증서 확보 후 더 높은 버전을 서명 게시하여 대체해야 한다. 1.0.x 이하 설치는 1.3.2를 1회 수동 설치하면 이후 자동.


v1.3.0 — 로컬 전사가 실제로 동작하게 (2026-09-18) 🎙️

설치본에서 로컬 전사가 한 번도 성공한 적 없던 원인을 끝까지 추적해 수정. 엔진 자체는 정상이었고, 배선(경로/루프백/패키징)이 전부 어긋나 있었다.

원인 (실측 근거)

  1. 패키지에 사이드카가 없었다: electron-builder.yml extraResources에 sidecar 항목이 없었고 CIS도 PyInstaller 빌드를 하지 않았다. 설치본 resources/에는 icons/sounds/sox만 존재 → 항상 faster-whisper 없는 시스템 Python으로 폴백 → 즉사.
  2. dev 경로 해석 실패: electron-vite는 out/main/index.js를 직접 띄워 app.getAppPath()가 apps/desktop/out/main이 된다. 그래서 out/main/sidecar/main.py(없음) + 시스템 Python 3.14, SoX도 spawn sox ENOENT. 실제 venv(apps/desktop/sidecar/.venv, py3.11 + faster-whisper 1.2.1)는 한 번도 사용되지 않았다.
  3. localhost = ::1 only: 이 호스트는 localhost가 IPv6로만 해석된다. 사이드카(uvicorn)와 Ollama는 127.0.0.1에만 바인딩 → http://localhost:... 요청 전부 ECONNREFUSED. STT와 로컬 LLM이 동시에 죽어 있었다.
  4. VAD 데이터 누락: faster-whisper의 assets/silero_vad_v6.onnx를 PyInstaller가 수집하지 않아, 사이드카를 번들했더라도 VAD 사용 시 런타임 실패했을 것.
  5. CAP-03(실시간 부분 전사)은 프로듀서가 없었다: 채널/팝업/preload는 있는데 emit하는 코드가 0.

수정

  • paths.ts: 앱 루트를 마커(sidecar/main.py, resources/sox)로 탐색. 패키지 모드에서 사이드카가 없으면 조용히 폴백하지 않고 즉시 실패(명확한 조치 안내).
  • loopback.ts: localhost → 127.0.0.1 정규화 (LocalSTT/LocalLLM/RAG/OnlineLLM/STTManager, ConfigService 기본값). 단위 테스트 9개.
  • 패키징: extraResources에 sidecar-dist/sidecar → resources/sidecar, resources/ffmpeg → ffmpeg, ffmpeg는 @ffmpeg-installer 의존성 + asarUnpack. setup-sidecar.mjs/build-sidecar.mjs + scripts/ci/verify-sidecar-bundle.mjs(VAD onnx 존재 검증) → CI package-windows/macos에 추가. 실측 번들: sidecar.exe 9.4MB + _internal 242MB, GPU 전사 왕복 성공.
  • 사이드카: 모델 재사용, --collect-all faster_whisper/ctranslate2, console 모드 유지(로그 수집) + spawn windowsHide, UTF-8 강제(PYTHONIOENCODING/reconfigure), 줄 단위 로그 파이프.
  • 성능/정합성: condition_on_previous_text=False, 온도 폴백 제한([0.0,0.2,0.4]), no_speech/compression/log_prob 임계, VAD min_silence_duration_ms=300, cpu_threads 8 상한. 7.7초 오디오: 1609ms → 303ms (약 5배), 전사문 동일.
  • 체감: 앱 시작 시 warmUpLocal()로 sidecar+모델 예열(bootstrap stt-warmup), 녹음 중 1.5초마다 최근 7.5초 창 부분 전사 → RecordingTip 미리보기(삽입되지 않음, 최종 전사 전에 drain).

검증: typecheck 0, 변경 파일 lint 0, 신규 테스트 18개 GREEN(loopback 9 / paths 6 / STTManager 3 — 기존 361건 실패는 better-sqlite3가 Electron ABI(130)로 빌드되어 Node 23(131)에서 로드 실패하는 선행 환경 문제로 본 변경과 무관). 실제 사이드카 통합 테스트(실제 PCM 7.7초)로 최종 전사/부분 전사 확인. electron-builder --dir로 패키지 산출물에 sidecar/_internal/VAD/ffmpeg/sox 존재 확인.

미해결(외부): Windows 공개 서명(WIN_CSC_*)이 없어 로컬에서 서명된 설치본을 만들 수 없다 → v1.3.0 태그는 CI가 서명 게이트와 함께 빌드해야 한다(11 GAP-REL-02).


D3RO NAS Docker 클라우드 서비스 배포 체계 구축 (2026-08-19 /goal) 🚀

D3RO Voice의 클라우드 API 백엔드, AI 프록시, 관리자 백오피스(BackOffice)를 Synology, QNAP, Linux 등 NAS 환경에 Docker 컨테이너로 손쉽게 배포하고 운영할 수 있는 완전 자동화 인프라를 구축 완료.

  • 컨테이너 아키텍처 (d3ro-voice-api:latest):
    • ASP.NET Core 10 Web API + EF Core SQLite 기반 경량 컨테이너 (~116MB 압축).
    • 영구 스토리지 볼륨(DATA_DIR=/app/data -> ./data/d3ro_api.db) 매핑으로 컨테이너 재생성 시에도 사용자, 모델 설정, 토큰/비용 통계 완벽 보존.
    • 컨테이너 헬스체크 REST 엔드포인트 (/health, /api/health) 및 Swagger 문서 (/swagger) 내장.
    • 통합 다크 글래스 테마의 메인 포털(wwwroot/index.html) 및 관리자 백오피스(wwwroot/admin/index.html + apps/admin/) 연동.
  • Docker Compose & 환경 설정:
    • docker-compose.yml 및 독립형 docker-compose.nas.yml 작성.
    • .env.example 포트(5000), 데이터 경로, JWT 시크릿, 타임존 설정 지원.
    • 선택적 On-NAS 로컬 Ollama AI 프로필(docker compose --profile ai up -d) 지원.
    • 로그 파일 크기 제한(max-size: 10m, max-file: 3)으로 NAS 디스크 보호.
  • 자동화 배포 & 제어 스크립트:
    • scripts/deploy-nas.ps1: Windows PowerShell 기반 원클릭 이미지 빌드, out/nas-package/ 생성, SSH 원격 배포 지원.
    • scripts/deploy-nas.sh: Linux/macOS/NAS 터미널 셸 스크립트.
    • scripts/nas-control.sh: NAS 현장에서 start, stop, restart, status, logs, backup(SQLite DB tar.gz 백업), update 지원.
  • 문서화:
    • docs/deployment/nas-deployment-guide.md — Synology DSM Container Manager, QNAP Container Station, HTTPS 역방향 프록시, 앱 연동 가이드.
    • README-NAS.md — 루트 빠른 시작 가이드.
  • 실증 검증 및 외부 공개 HTTPS 연동 (2026-08-19):
    • Cloudflare Tunnel (kd-nas, ID: fe6293f8-17a5-47c3-bfe9-2374b95a003d) Ingress 규칙 API 업데이트 완료: d3ro.chanpaca.net -> 포털/API(5050), admin.chanpaca.net -> Next.js 어드민 CRM(3001).
    • 공용 인터넷 HTTPS 실시간 통신 검증 완료: https://d3ro.chanpaca.net/health (HTTP 200 OK, Healthy).
    • 포털(https://d3ro.chanpaca.net/), 어드민 CRM(https://admin.chanpaca.net/), Swagger(https://d3ro.chanpaca.net/swagger) 정상 가동.
  • 포트리스(Fortress) 다계층 심층 보안 및 레드팀(Red Team) 방어 체계 구축 (2026-08-19):
    • 초기 최고 관리자 계정 시딩 및 인증 연동 완료: admin / Test1234!.
    • 미들웨어 엣지 라우트 가드 + RSC 이중 세션 검증 (미인증 시 즉시 /login 307 리다이렉트).
    • Anti-Crawling & Anti-Reconnaissance (X-Robots-Tag: noindex, nofollow, noarchive, robots.txt Disallow: /).
    • 브루트포스 방어: 슬라이딩 윈도우 기반 5회 실패 시 15분 IP/계정 락아웃 + Timing Attack 방어 인위적 딜레이.
    • 자동화 봇 방어: 허니팟 트랩(Honeypot Trap) 필드 감지 즉시 403 차단.
    • 세션 암호화: HMAC-SHA256 암호화 서명 쿠키 (HttpOnly, Secure, SameSite=Lax, 30일 유효) 적용 및 위변조 실시간 감지 차단.
    • 레드팀 모의 침투 5개 시나리오 전수 통과 실측 검증 완료.

v0.2.1-alpha 핫픽스 (2026-07-22) 🐛

v0.2.0-alpha 치명적 버그(앱 실행 불가) 핫픽스. 사용자 보고 "설치 실행 안 됨".

  • 원인: refactor-wave WS2(AppConfig SSOT)가 AppConfig 인터페이스에 7키 추가 + as never 16건 제거했으나 CONFIG_DEFAULTS(ConfigService.ts) 기본값 누락. 기존 사용자 config(0.1.x) 마이그레이션 시 configGet()이 undefined → ChainService/VoiceCommandService .map() 크래시 → main 시작 실패. typecheck는 통과(런타임 값 검사 못 함).
  • 수정: CONFIG_DEFAULTS에 7키 기본값 추가([],[],[],false,'',null,'mic').
  • 검증: dev 실행 — main 빌드 성공 + electron start(① 수정 전엔 이 단계에서 configGet undefined 크래시). ② getI18n(main 번들 포함)은 순수 함수라 문제 아님.
  • 근본 교훈: 배포 전 dev 실행 + 산물 설치 e2e 필수. typecheck GREEN ≠ 런타임 안전. refactor-wave SKILL R7에 "배포 전 런타임 게이트(dev 실행 + 산물 e2e)" 추가 필요 (현재 R7은 typecheck/lint/test만).
  • 배포: tag v0.2.1-alpha → CI 재빌드 → Windows 자동업데이트로 전달.

v0.2.0-alpha 릴리스 (2026-07-22) 🚀

Midnight Glass v2 + refactor-wave Wave 1/2 + TiltCard 정리를 묶은 첫 마이너 릴리스.

  • CHANGELOG [0.2.0-alpha] 섹션 작성 (switchboard 스타일 — ## [Unreleased] 최상단 정렬, Added/Changed/Fixed/Removed/Internal). publish-gitlab-release.mjs가 Release 노트로 자동 추출.
  • release-guide.md 보강: "Release 페이지 운영" 섹션 추가 (CHANGELOG = Release 노트, 권장 구조, 릴리스 체크리스트, 버전 정책). switchboard(D:\workspace\agent-switchboard-client) 패턴 벤치마크.
  • 포함: Wave 1(amber 91/i18n 967/IPC SSOT 9그룹/AppConfig 7키/DS 3종 삭제) + Wave 2(web-admin amber 21/ui-native 50/팝업 i18n+토큰/의존성 8/catch 32) + 사용자 노출 버그 3건 해소.
  • 배포: git tag v0.2.0-alpha + push → GitLab CI (Win NSIS exe + Mac dmg/zip + GitLab Release + Package Registry). Windows 자동업데이트 feed 갱신 → 기존 사용자 4시간 내 수신.
  • 후속: switchboard의 check-release.mjs/verify-*-release.mjs 빌드 검증 스크립트 이식 검토 (D3RO는 publish만 이식 상태).
  • Linux(deb/AppImage) = 별도 Phase (2026-07-22 합의): 신규 플랫폼 추가급 작업. 현재 안 함(electron-builder win/mac만, 사이드카도 Win/Mac만). 장벽 — ① 사이드카 Linux PyInstaller(ctranslate2 CPU/GPU) ② native 모듈 rebuild(uiohook-napi/nut-js/better-sqlite3) ③ TTS 백엔드 신규(espeak-ng/Piper — 현재 Windows SAPI 전용이라 음성대화 안 됨, 핵심 장벽) ④ SoX/Ollama Linux 번들 또는 apt 의존 ⑤ electron-builder linux(deb+AppImage) + afterInstall ⑥ CI package-linux 잡(TW-BUILD01은 현재 검증용 Docker, 패키징 툴체인 확장 필요) ⑦ QA(Ubuntu/Debian/Fedora). runner는 있음(TW-BUILD01) but Electron 패키징 환경 별도. switchboard 원본도 Windows-only라 참조 없음.

리팩토링 Wave 2 (2026-07-22 /refactor-wave) ✅

/refactor-wave Wave 2. Wave 1 이월 항목 처리 + 범위 확장(web/admin/site/mobile/ui-native). 5 WS 병렬 → 분리 커밋 5개. 보고서 docs/REFACTOR_WAVE2_REPORT.md.

산출 (efac690/660e622/00a99e4/4c25620/5b6e7aa):

  • WS-AMBER: web 11 + admin 10건 amber→main 치환, theme.ts(packages/ui) deprecated 별칭 제거. 전 모노레포 amber 잔여 0건.
  • WS-NATIVE: ui-native theme.ts accent amber→main 개명(case B, 값 보존), mobile/mobile-rn/ui-native 47참조 치환.
  • WS-POPUP: 4종 팝업 _i18n 주입(WindowManager getPopupI18nStrings + applyI18nToHtml), 한국어/영어→popup.* 17키, theme-vars 11 토큰(--d3-wave-1~9/status), 팝업 CSS 토큰화, i18n getI18n() 추가.
  • WS-DEPS: 미사용 의존성 8종 + node-record-lpcm16.d.ts + electron-builder Linux 섹션 제거.
  • WS-PATTERN: ipcSuccess/ipcError 헬퍼 통일(6핸들러) + catch(error) 32건(11파일).

검증: desktop/admin/api-client/ui-native typecheck 0, 변경 영역 lint 0 errors, test 41+19 GREEN. (web pre-existing 1건, mobile/RN 환경 에러 — 별도.)

메타 학습: (1) P7 입증 — 공유 패키지 별칭 제거는 전체 소비 앱 치환 "후"에만 가능(Wave 1 desktop + Wave 2 web/admin 치환 후 theme.ts 별칭 제거 성공). (2) RN 패키지는 palette 구조가 다를 수 있음 — case A(본체 존재→치환) vs B(개명) 사전 판단(WS-NATIVE). (3) 팝업(Vanilla JS)은 typecheck 무관 — dev 육안 확인 필수.

누적(Wave 1+2): 분리 커밋 10개, amber 치환 162건 + 별칭 2곳 제거, 데드코드(DS 3종 + services/index + 의존성 8 + i18n 967키), 사용자 노출 버그 3건 해소(date "TODAY"/팝업 색 충돌/팝업 한국어), IPC SSOT 9그룹+헬퍼+catch, AppConfig 7키 + as never 16건.

다음 Wave 3: theme-vars 구조 단일화, caption-overlay i18n, color prop amber API 마이그레이션, site 토큰, unused 정리, web teams 타입, mobile/RN 인프라. 별도 Phase: 서비스-IPC 분리, 타 locale 번역, lint pre-existing.


리팩토링 Wave 1 (2026-07-22 /refactor-wave) ✅

/refactor-wave 스킬 첫 실전 적용. 범위: desktop + 공유 packages. 3 에이전트 공격적 감사(106건) → 4 WS 병렬 → 분리 커밋 4개. 정책 docs/REFACTOR_POLICY.md + 보고서 docs/REFACTOR_WAVE1_REPORT.md 신규.

산출 (b820c78/aef4428/078304d/87f2dac):

  • WS1 토큰/DS: theme-vars dark/light SSOT 정합(v1 주황→파랑, 팝업 색 충돌 해소), DS 미사용 3종 삭제(MetalDial/ButtonGroup/CrtDisplay), StatRing 토큰화
  • WS2 메인/IPC: ipc-channels SSOT 9그룹 추가, 하드코딩 채널 SSOT 교체(불일치 4건 preload 재검증), AppConfig 7키 추가 + as never 16건 제거, require 3건→static, services/index.ts 데드 제거
  • WS3 렌더러: accent.amber→main 91건/34파일, 매직 리터럴→d3roTypo/d3roRadius 37건
  • WS4 i18n: date.today/yesterday 중복 버그 수정(ko/en), 고아 키 967건 삭제

사용자 노출 버그 2건 해소: 한국어 날짜 "TODAY"→"오늘", 팝업=주황/렌더러=파랑 색 충돌 정합.

검증: desktop/core/ui/i18n typecheck 0, 내 영역 lint 0 errors, test 41+19 GREEN. (web typecheck pre-existing 1건, 전체 lint mobile-rn config 붕괴 — 별도 wave.)

메타 학습: (1) 공유 패키지 토큰 변경은 전체 소비 앱 영향 — theme.ts amber 별칭은 web/admin/site 사용으로 제거 불가 → 전체 치환 wave 이월. (2) typecheck GREEN이 고아 키 검증 충분조건 아님 — WS4 camelCase license.feature 15키가 featureLabel 프로퍼티 패스스루로 typecheck 우회, P1 grep 재검증이 런타임 버그 15건 예방. (3) 감사/구현 에이전트는 typecheck 회피 — 프롬프트에 EXIT 0 강제(SKILL P7/P8/P9 갱신).

다음 Wave 2: WS5 팝업(2차) + 전체 amber 치환 + theme-vars 구조 단일화 + 미사용 의존성 8종 제거 + ipcSuccess/catch 패턴. 별도 Phase: 서비스-IPC 분리, 타 locale 번역 파이프라인, lint pre-existing 정리.


마지막 갱신: 2026-07-22 (TiltCard 3D 틸트 + 빛 반사 효과)

TiltCard — 3D 틸트 + 커서 추종 빛 반사 효과 (2026-07-22 /design-taste-frontend + /refactor-wave) ✅

대시보드 카드에 마우스를 올리면 커서를 따라 3D로 미세 기울임 + 커서 위치에서 빛이 반사되는 스페큘러 하이라이트가 흐르도록 프리미엄 물리감 추가. 정적이던 Midnight Glass v2 글래스 카드에 깊이.

  • 기법(웹 리서치): 부모 perspective 대신 요소 자신의 transform: perspective(900px) rotateX(var) rotateY(var) (Chromium 단일 요소 틸트). 스페큘러는 커서 %→CSS 변수를 radial-gradient(circle at var(--mx) var(--my), …)에 주입해 이동. 라이브러리 없이 네이티브 pointer 이벤트 + ref.style.setProperty(리렌더 0, 60fps). 참고: frontend.fyi/CSS-Tricks/armandocanals.
  • theme.ts: glass.specular 토큰 추가(rgba 색 — geometry는 컴포넌트가 소유해야 커서가 하이라이트를 움직일 수 있음). RawTheme 인터페이스 + 6개 RAW 테마 + buildCssVars + d3roPalette (9곳). 색은 테마별 톤 파생(dark rgba(180,205,255,0.22), light rgba(255,255,255,0.5) 등).
  • TiltCard.tsx (신규 DS): MetalCard 표면 레시피(동일 토큰) 복제 + 3D 틸트 + 스페큘러. 구조 — 루트 Box(perspective/transform/표면/blur/시인 ::before/헤어라인 링 ::after/overflow) + 자식 span.d3-tilt-spotlight(커서 추종, z1, mixBlendMode screen, :hover opacity 0→1) + 콘텐츠 Box(z2). useTiltCard 훅 인라인(ref+CSS 변수 갱신). 핵심 디테일: pointerleave에서 변수 중립 리셋(0deg/50%) 후 data 속성 제거 → 복귀 트랜지션(리셋 안 하면 마지막 각도에 고정). Props: maxTilt(기본 8)·spotlight(기본 true) + BoxProps.
  • reduced-motion 완전 존중: useEffect로 matchMedia 구독 → reduceRef 게이트로 틸트/추종 no-op, @media에서 transform:none·transition:none. OS 토글에도 즉시 반응(theme-mode-context 패턴 차용).
  • 대시보드 적용(모션 동기 부여된 곳만): 히어로 카드 + 4개 스탯 타일(타일 maxTilt=6). 나머지 5개 섹션(백엔드/사용량/자막/시스템상태/파일전사/히스토리)은 데이터 밀집·드롭존·리스트라 MetalCard 유지(틸트 = unmotivated slop 회피).
  • 검증: desktop tsc --noEmit EXIT 0, 변경 파일 eslint clean. (전체 npm run lint은 기존 apps/mobile-rn/.eslintrc.js의 @react-native 설정 누락으로 별도 이슈 — 본 변경 무관.)
  • 커밋 3건 분리(theme / TiltCard+export / dashboard) + docs(memory).
  • 후보: 다른 페이지 히어로 카드 확장, spotlight 반경·maxTilt 페이지별 튜닝.

스킬 이식: harness-gate + refactor-wave (2026-07-21 /goal) ✅

HaramLog에서 범용 워크플로우 스킬 2종을 D3RO에 맞게 커스터마이징해 이식 (.claude/skills/):

  • harness-gate: 지속 규칙 발언("이제부터~", "항상~") 감지 시 규칙 SSOT 3계층(CLAUDE.md 코딩 규칙 / settings.json 강제 규칙 훅 / memory 피드백) 중 갱신 위치를 게이트. L1↔L2 중복 규칙 동기화 주의점 포함.
  • refactor-wave: 정책(docs/REFACTOR_POLICY.md) 합의 → 공격적 감사(3 에이전트) → 파일 비충돌 워크스트림 병렬(WS1 토큰/WS2 메인/WS3 렌더러/WS4 i18n/WS5 빌드) → 분리 커밋 → 보고서. HaramLog wave 110 학습 중 범용 인사이트 6개(P1P6) 이식, 검증은 npm typecheck/lint/test 체계로 치환.
  • 조사 결과: vignette 스킬 없음, GlassDeck 13종·RFS 디자인 계열 전부 글로벌(~/.claude/skills)과 중복, RFS 나머지는 Unity/RFS 전용이라 제외. HaramLog의 implement-phase/test-and-commit은 D3RO 기존 버전이 더 우수해 유지.

받아쓰기 파이프라인 4버그 수정 + 실시간 부분 전사 (2026-07-21 /goal) ✅

증상 → 근본 원인 (로그 실측)

  1. "전사가 안 되는데 되는 것처럼 보임" + 4. "키를 떼도 안 끝남": press 액션이 STT 초기화(사이드카 죽으면 30초 헬스 타임아웃)를 await하며 action queue를 점유 → release가 수십 초 지연, 연타한 press들이 큐에 쌓여 30초 간격 유령 세션 반복(마이크 게이지는 동작해 "되는 척"). 트리거는 dev 사이드카 의존성 미설치(venv 부재)였지만 구조 결함이 본질.
  2. 사이드카 crash 루프: 재시작 카운트가 MAX(3) 초과해도 세션마다 재spawn + emit('error') 리스너 부재로 ERR_UNHANDLED_ERROR 미처리 예외.

수정

  • VoiceModeService: _initSTT()를 fire-and-forget으로(await 제거 — release 즉시 처리), 프리플라이트(STT 모델 미설치 시 즉시 에러+경고), 실시간 부분 전사 루프(1.5s 간격, 최근 12초 윈도우, interim 실패 무시·최종 전사가 진실), Ollama 미가용 후처리 스킵 시 warning 이벤트
  • LocalSTTService: 헬스체크 중 프로세스 사망 감지 → 30초 대기 없이 즉시 실패, 헬스 성공 시 restartCount 리셋, 기본 error sink로 미처리 예외 방지
  • voice-handlers: VoiceMode 'error' → voice:error 브로드캐스트(severity 포함, 설계서 02 채널)
  • AppLayout: voice.onError 구독 → 에러/경고 스낵바 + 모델 미설치(101)면 온보딩 자동 오픈. i18n 3키(ko/en)
  • RecordingTip 팝업: 부분 전사 표시 영역 추가(2줄 클램프, 팝업 320x110), voice:partialTranscript 채널
  • dev 환경: sidecar venv 구축 (재발 방지)

E2E 실측 (dev, GPU cuda float16 + large-v3-turbo 로컬 디렉토리 로드)

  • 콜드: 모델 로드 3.4s, 전사 5.6s→2.3s / 웜: 전사 0.5~1.0s
  • 10초 홀드: interim 2회 발동(부분 전사 팁 전송) → 릴리스 1ms 내 캡처 중지 → 최종 전사 → 텍스트 삽입 43자 → WAV 저장
  • vitest 41/41 (VoiceModeService mock에 getModels 추가)

UI 전면 리디자인 "Midnight Glass" v2 (2026-07-21) — 기반 완성 ✅

방향 (사용자 레퍼런스 이미지 기반, /goal)

미드나이트 네이비 + 글래스모피즘 카드(옅은 헤어라인) + 시안→블루→퍼플→마젠타 그라디언트 + 보더리스 UI. 기존 테마 시스템(6종 변형) 유지가 필수 조건.

구현

  • theme.ts 전면 재작성: RawTheme에 glass(surface/raised/hairline/sheen/blur)·gradient(accent/logo/bar/barAlt/wave1-4)·glow 토큰군 추가. 기본 dark = Midnight(#0a0e1c 베이스, 블루 #3b82f6 액센트). 6개 테마 전부 파생 토큰 보유(Nord로 실사용 검증됨 — 테마 전환 정상). d3roPalette에 glass/gradient/glow + accent.main 별칭(amber는 deprecated 별칭 유지 → 전 페이지 무수정 호환)
  • DS 리스킨(동일 API): MetalCard→글래스 카드, PhysicalButton→글래스/그라디언트 버튼, PhosphorText→Pretendard 산세리프(메타류만 모노), Led→소프트 글로우(+blue/purple), ScreenPanel/ButtonGroup/InstrumentPanel 글래스화, CrtDisplay를 WebGL 오실로스코프→그라디언트 스펙트럼 웨이브로 교체(API 호환)
  • 신규 DS: GradientWave(canvas 2D, CSS 변수로 테마 반응, 미러 스펙트럼+글로우+유휴 애니메이션 — "음파" 자체 개발), StatRing(컬러 그라디언트 링 타일)
  • 셸: frame:false 보더리스 + TitleBar(드래그+min/max/close, lucide), AppLayout 와이드 사이드바(그라디언트 로고, 액티브=그라디언트 헤어라인 필+글로우, lucide 아이콘), 앰비언트 그라디언트 오브 배경
  • 스크롤: 네이티브 스크롤바 width 0 전역 제거(레이아웃 폭 불변) + OverlayScrollbars(os-theme-dark 변수 오버라이드; 커스텀 테마 클래스는 포지셔닝 CSS가 없어 오동작 — 스톡 테마+변수 방식이 정답)
  • 폰트: Pretendard Variable 로컬 번들(2MB woff2, 오프라인)
  • 대시보드 재구축: 히어로(헤드라인+수치+CTA+웨이브), 링 타일 4종, 백엔드/요금제, 사용량(그라디언트 바)/실시간 자막, 시스템 상태(웨이브+세그먼트 레벨미터), 파일 전사, 히스토리. i18n 신규 19키(ko/en)
  • StatusBar: 브랜드+버전+모델 / 로컬 시간 시계
  • 검증: tsc 0, vitest 41/41, 실행 스크린샷으로 대시보드/히스토리/대화/회의/지식/설정 전 페이지 확인. 스크롤 시 폭 불변 확인. 상단 1px 잡티는 detached DevTools 겹침(앱 버그 아님)

카드 글로우 고도화 (사용자 레퍼런스 #5)

  • glass.borderGradient(좌상단 발광→우하단 소멸 그라디언트 헤어라인, mask 링 기법 ::after) + glow.card/cardHover(블루 앰비언트+깊이 그림자) + sheen 재정의(라디얼 앰비언트+수직 시인 레이어) — 6개 테마 전부 액센트 파생
  • 적용: MetalCard, InstrumentPanel, MuiCard 오버라이드. 반투명 표면에서도 정확한 1px 그라디언트 링 (padding-box 기법 대신 mask exclude 사용 — 반투명 배경 투과 문제 회피)

마감 완료 (워크플로 8 에이전트 병렬)

  1. ✅ @mui/icons-material → lucide-react 전환 (23파일, 잔여 0 — 의존성 제거는 추후)
  2. ✅ Vanilla 팝업 5종 CSS v2 정합 (글래스 네이비 + 블루 액센트, var(--) 주입 구조 유지)
  3. ✅ VoiceRecordingPanel → GradientWave 교체
  4. 검증: tsc 0 / vitest 41/41 / 실화면 캡처 (Midnight·Nord 두 테마)
  5. 잔여(후속): 라이트 테마 정밀 검수, @mui/icons-material 의존성 package.json 제거, MetalDial(미사용) 정리, v0.2.0-alpha 릴리스

모델 현대화 트랙 (2026-07-21) — B 완료, A 완료(코드), C 완료(코드)

방향 결정 (사용자 컨펌)

  • A+B 병행: B(로컬 Whisper turbo 전환 + 부트스트랩 진행률 UI) 먼저 → A(OpenAI gpt-realtime-2.1 라이브 음성 대화 Premium 백엔드)
  • C (방향 반전 확인됨): agent-switchboard-client의 릴리스 체계를 D3RO로 이식 (원래 지시가 반대로 전달됐었음 — 대상은 이미 우수한 파이프라인 보유). Mac 배포 + Release 자동화 + 자동 업데이트.

C 완료(코드): switchboard 릴리스 체계 이식 ✅

  • package-macos CI job: arm64 무서명(ad-hoc, identity=- + hardenedRuntime=false + CSC_IDENTITY_AUTO_DISCOVERY=false — switchboard 검증 조합). 사이드카 venv PyInstaller + brew sox 번들 + Ollama darwin. D3RO_MAC_RUNNER="true" CI 변수 게이트 + allow_failure(mac 실패 시 Windows 단독 릴리스).
  • sync-version.mjs: 태그 → apps/desktop/package.json 버전 동기화 (기존 "Setup 1.0.0.exe" 불일치 해소)
  • publish-gitlab-release.mjs: 산출물을 Generic Package Registry 버전별+latest 업로드 + GitLab Release asset 링크 (기존 PowerShell curl 잡 대체). latest.yml 필수 검증, latest 삭제는 best-effort.
  • electron-builder.yml: publish: generic 추가(latest.yml 생성 트리거 — 없으면 update info 미생성!), 공백 없는 artifactName(D3RO-Voice-Setup-*), mac arm64 단일화(x64 사이드카 불가)
  • UpdateService (신규, 싱글톤+EventEmitter): electron-updater generic provider, 4h 주기, 다운로드 완료 시 재시작 다이얼로그(mainWindow parent). update-feed.ts SSOT — URL 미기입 시 비활성. Windows 전용(mac 무서명은 Squirrel.Mac 제약).
  • 의존성: electron-updater 추가, @rollup/rollup-win32-x64-msvc를 optionalDependencies로 이동(mac npm ci EBADPLATFORM 해소), lockfile에서 workspace 밖 RN 의존성 163개 정리
  • 가이드: docs/deployment/release-guide.md — mac runner 등록, feed URL 2곳 SSOT, 파일명 규칙, 후속(서명/공증/Ed25519) 정리
  • 검증: ultracode 리뷰 워크플로(16 에이전트, 3관점 → 적대적 검증)로 결함 8건(중복 제외) 확정 → 전부 수정. tsc 0 / vitest 41 / YAML·mjs OK.

D 완료: v0.1.1-alpha 실배포 성공 (2026-07-21) ✅

  • runner 인프라 변화 발견: YUNCHAN-1 소멸 → 사내 공용 TW-BUILD01(linux docker/shell), TW-VIVEN-BUILD(build-win-x64), TW-BUILD-MAC-ARM64(build-mac-arm64). CI 태그 전면 교체.
  • 사전 설정 완료(API): 프로젝트 ID 1172 → feed URL 2곳 기입, D3RO_MAC_RUNNER=true 변수, package_registry_access_level=public
  • 1차 파이프라인(#69730) 실패 → 수정 → 2차(#69733) 성공:
    • 원인 1: electron-builder가 prerelease에서 채널 alpha 감지 → alpha.yml 생성 → detectUpdateChannel: false로 latest 고정
    • 원인 2: mac runner에 sox/brew 없음 (allow_failure로 릴리스는 진행)
    • Windows 빌드 11분 (VIVEN runner, signtool 서명까지 됨 — 머신 전역 인증서)
  • Release v0.1.1-alpha: D3RO-Voice-Setup-0.1.1-alpha-x64.exe(735MB) + latest.yml + blockmap이 Generic Package Registry 버전별+latest에 게시, Release asset 링크 생성
  • E2E 검증 완료: 익명 다운로드 200 OK → 사일런트 설치(%LOCALAPPDATA%\Programs\@d3rodesktop\D3RO Voice.exe) → 앱 기동 → 로그에서 전체 bootstrap 정상 + UpdateService가 feed 체크 후 "0.1.1-alpha is not available (latest)" 정확 판정 — 자동 업데이트 루프 프로덕션 검증됨
  • mac: PATH 수정(brew/volta) 커밋 후 main 파이프라인으로 검증 중. 사이드카 arm64 PyInstaller 빌드는 이미 성공 확인(183MB).

E 완료: v0.1.2-alpha — Mac 포함 첫 멀티플랫폼 릴리스 + 자동 업데이트 라이브 검증 ✅

  • mac 빌드 수정 2건: ①brew/volta PATH 명시(CI 셸은 비로그인), ②Ollama darwin 자산명(ollama-darwin 단일 바이너리, tgz 아님)
  • 파이프라인 #69743 전 잡 그린: Windows exe + Mac dmg/zip(arm64, ad-hoc 서명) + latest.yml/latest-mac.yml 전부 Release 게시
  • 자동 업데이트 실전 검증: 실행 중이던 0.1.1-alpha 앱 재시작 → 30초 후 feed 체크 → "Found version 0.1.2-alpha" → 735MB 다운로드 7초(사내망) → 재시작 다이얼로그 표시. 전체 루프 완주.
  • 알려진 사소 이슈: 차등(blockmap) 다운로드 시 이전 버전 blockmap URL에서 :8443 포트가 탈락해 404 → full download 폴백으로 동작 (electron-updater 비표준 포트 quirk, 비차단)

F: 온보딩 gemma4 pull 412 실패 → Ollama 번들 v0.32.1 범프 (v0.1.3-alpha)

  • 증상: 설치 앱 온보딩에서 "다운로드 실패" — Ollama 레지스트리가 412 반환 ("requires a newer version of Ollama")
  • 원인: 번들 Ollama v0.5.7 ≪ gemma4:e4b 요구 0.20+. 4/11 모델 전환과 4/15 번들 결정이 서로 버전 정합 안 됨. 개발 환경은 시스템 Ollama 0.20.5가 가려줌 (번들 우선은 패키지 앱에서만 발동)
  • 수정: OLLAMA_VERSION v0.32.1(최신), 다운로드 스크립트 2종에 .ollama-version 스탬프 캐시 무효화(runner가 checkout 재사용), cuda*/rocm* 재귀 가지치기(신구 zip 레이아웃 모두), darwin은 v0.6+부터 tgz 복귀(bin/ollama·루트 모두 지원), 1.5GB 초과 경고
  • 배포 경로: v0.1.3-alpha 태그 → 설치 앱이 자동 업데이트로 수신 ✅
  • v0.1.3-alpha 릴리스 완료 (파이프라인 #69747 전 잡 그린): Ollama v0.32.1 CPU 전용 가지치기 후 119MB (기존 ~1GB), 설치파일 735MB → 225MB
  • 차등 다운로드도 이번엔 동작: 0.1.2→0.1.3 델타 27MB만 다운로드(12%) — updater 캐시의 current.blockmap 사용. 이전 404는 첫 업데이트라 old blockmap이 없어서였음
  • v0.1.3 실측 결과 (2026-07-21 17:55~17:58): 온보딩 전체 성공 — gemma4:e4b pull 완료(레이어가 로컬 캐시에 있어 검증만 ~2분), Whisper medium 1.5GB 다운로드 25초, onboardingCompleted 저장. 사용자는 "조용한 실패"로 인지 → 원인은 verifying(무진행) 구간 ~2분간 UI 피드백 부재 (UX 버그, 기능 정상)
  • G: 온보딩 UX 수정 (v0.1.4-alpha) — 릴리스 완료(#69756 전 잡 그린), 차등 24MB로 설치 앱 전달 확인: ①total 없는 상태(pulling/verifying/writing manifest)에서 인디터미넌트 바 + 친화적 문구(i18n 3키), ②중복 클릭 가드(runningRef), ③LocalLLMService.pullModel 동시 호출 dedupe(in-flight map), ④STT /download 409 시 기존 진행 폴링 합류
  • 참고: 온보딩 STT 단계는 사용자의 기존 설정 모델(medium)을 존중해 다운로드함 — turbo 강제 아님 (설계 의도)

H: 앱 아이콘 + 트레이 + 마이크 테스트 버튼 수정 (v0.1.5-alpha) — 릴리스+전달 완료 ✅ (#69759 전 잡 그린, 설치 앱 0.1.5.70 확인)

  • 앱 아이콘 신규: build/icon.svg(마스터, d3roPalette SSOT — 메탈 섀시+다크 글래스+오렌지 웨이브 5바 cos 분포) → icon.png(1024)+icon.ico(6사이즈). scripts/generate-icons.mjs(sharp, ico 컨테이너 직접 조립). electron-builder win.icon/mac.icon 연결 + extraResources icons/
  • 트레이 아이콘 버그: createEmpty()로 빈 이미지였음 → getAppIconPath()(paths.ts, win=ico/기타=png, dev는 build/) 사용
  • 마이크 테스트 버튼 조기 종료 버그 (mic-test-debug 에이전트 진단): 렌더러가 초기 무음 level:0을 종료 신호로 오인 → 즉시 구독 해제. 수정: TEST_LEVEL 이벤트에 done 플래그(SSOT 채널 AUDIO.TEST_LEVEL/STOP_TEST 신설, 하드코딩 'audio:testLevel' 제거), 렌더러는 done으로만 종료, STOP 버튼이 실제로 메인 캡처 중지, testDevice 실패 시 상태 롤백
  • 잔여(경미): 설계서 02의 audio:testDevice 명세는 "짧은 캡처 후 결과 반환"인데 실제는 스트리밍 — 명세 갱신 후보. AudioCaptureService.testCapture() dead code 정리 후보

남은 것

  1. 설치 디렉토리명 @d3rodesktop 정리 (electron-builder가 package name에서 유도 — 다음 릴리스에서 명시)
  2. VIVEN runner의 signtool 서명이 어떤 인증서인지 확인 (twentyoz 조직 인증서면 유지)
  3. Mac dmg 실기기 설치 테스트 (Gatekeeper 우회: 우클릭→열기)
  4. blockmap 차등 다운로드 포트 탈락 quirk 조사 (선택)
  5. 다음 릴리스부터는 태그 푸시만 하면 끝

B 완료: Whisper large-v3-turbo 전환 + 온보딩 2단계 부트스트랩 ✅

  • 기본 STT 모델: base → large-v3-turbo (CONFIG_DEFAULTS). 6배 빠름, 정확도 손실 1~2%, 1.6GB
  • 사이드카 다운로드 API: POST /download(백그라운드 스레드, HF 파일 스트리밍, 멱등), GET /download/status, POST /download/cancel, --models-dir 인자
    • 저장 위치: userData/whisper-models/<model_id>/ — /load가 로컬 디렉토리 우선, 없으면 HF 자동DL 폴백
    • requirements: faster-whisper>=1.1.0 (turbo 지원), huggingface_hub/requests 명시
  • LocalSTTService: downloadModel()(폴링 500ms + download-progress emit), cancelDownload(), getModels()가 fs로 downloaded 판정
  • IPC: 설계서 02에 이미 정의돼 있던 stt:downloadModel/stt:cancelDownload/stt:downloadProgress 채널 구현 완료 (기존 미구현)
  • OnboardingModal 2단계: LLM(gemma4:e4b) → STT(large-v3-turbo) 순차 다운로드, 단계 표시 + MB 진행률, 필요한 단계만 실행(멱등 재시도)
  • 설정: SettingsModal에 large-v3-turbo 선택지, settings.model.largeTurbo 12 locale
  • i18n: onboarding.sttModelMissing/sttModelSize/step/progressDetail (ko/en)
  • 설계서 동기화: 00(STTConfig union + 기본값), 01(카탈로그 주석), 05(/load 주석)
  • 테스트 수리: errors.test.ts / VoiceModeService.test.ts의 모노레포 전환 잔재 import(src/shared/* → @d3ro/core/*) 수정 → 41/41 통과
  • 검증: desktop tsc EXIT 0, vitest 41 passed

A 구현 완료(코드): gpt-realtime-2.1 라이브 음성 대화 (Premium) ✅

  • 2026-05 출시 OpenAI Realtime 계열: gpt-realtime-2/2.1($0.05/분), 2.1-mini($0.016/분), GPT-Realtime-Whisper($0.017/분), GPT-Realtime-Translate($0.034/분)
  • 서버: realtime-token Edge Function 신규 — requireUser → 티어 검증(free 차단, pro=2.1-mini, pro_plus=2.1) → realtime_session 쿼터(일간 pro 30/pro+ 120 세션) 소비 → OpenAI POST /v1/realtime/client_secrets → ephemeral key 반환. quota.ts에 realtime_session 키 추가. config.toml verify_jwt=false.
  • IPC: voiceConversation:getRealtimeToken (main이 CloudSync invokeFunction 경유), ErrorCode 799 ConversationRealtimeTokenFailed
  • 렌더러: useRealtimeConversation 훅 — getUserMedia → RTCPeerConnection → SDP를 POST api.openai.com/v1/realtime/calls(Bearer ek_) → data channel 'oai-events' 이벤트 매핑(response.created=thinking, *audio_transcript.delta=speaking+스트리밍, input_audio_transcription.completed=유저 메시지, response.done=확정+listening). dc open 시 session.update로 whisper-1 입력 전사 활성화(실패해도 대화 유지).
  • VoiceConversationPage: conversationBackend 설정 분기 — realtime이면 훅 사용, 연결 실패 시 로컬 파이프라인 자동 fallback + 배너. LIVE 배지, 텍스트 입력 상시 표시.
  • 설정: AppConfig.conversationBackend('local'|'realtime', 기본 local), SettingsModal LLM 탭에 "음성 대화 엔진" 선택 + realtime 선택 시 라이선스 모달
  • i18n: settings.convBackend.* 5키 + conversation.realtime.* 3키 (ko/en, 나머지는 en fallback)
  • 검증: desktop tsc EXIT 0, vitest 41/41. deno check의 @ts-expect-error 에러는 기존 llm-proxy와 동일(관행)

A 남은 사용자 액션 (배포/E2E)

  1. supabase secrets set OPENAI_API_KEY=sk-...
  2. supabase functions deploy realtime-token
  3. Pro 계정으로 로그인 → 설정 → 음성 대화 엔진 = Live → 대화 페이지에서 마이크 시작 → 실측
  4. (선택) 이벤트명이 2.1 API와 다르면 훅의 handleServerEvent 케이스 보강
  5. (후속) 분 단위 정밀 쿼터 (세션 수 → 분 집계), settings.convBackend i18n 10개 locale 확장

v0.1.0-alpha 배포 전체 흐름 (2026-04-15 night) ✅

GitLab CI 파이프라인 확립

  • d3ro-voice-win-build runner 등록(YUNCHAN-1 PC, shell/powershell executor, tags: windows+shell)
  • config.toml에 machine PATH 고정 삽입(C:\Program Files\Python311;...) → Windows Store Python stub 우회
  • Python 3.11.9 공식 설치(machine-wide)
  • 파이프라인 성공 순서: npm ci → pip install → build-sidecar.py(PyInstaller 232MB) → download-sox.ps1 → download-ollama.ps1 → electron-vite build → electron-builder NSIS

해결된 빌드 이슈 타임라인

  1. Python 미감지 → machine-wide 설치 + runner config.toml의 environment로 PATH 주입
  2. SoX SourceForge 다운로드 차단 → 바이너리 직접 커밋(apps/desktop/resources/sox/sox.exe + DLL들)
  3. Ollama 다운로드 한글 인코딩 파싱 실패 → download-ollama.ps1 메시지 전부 영문화
  4. Invoke-WebRequest 대용량(~2GB) hang → curl.exe + 30분 타임아웃 + 진행률 바
  5. package.json의 build.configFile electron-builder v26에서 미지원 → --config 플래그로 전환
  6. 모노레포 hoisting → electron 자동감지 실패 → electronVersion: "33.3.0" 명시
  7. 아이콘 빈 폴더 자동 스캔 실패 → directories.buildResources: build/로 변경, win/mac/linux.icon 제거
  8. GitLab artifacts 413 Too Large → release/*/*.exe + latest.yml + blockmap만 업로드
  9. expose_as와 와일드카드 path 병행 불가 → expose_as 제거

해결된 설치러너 크래시

  • 증상: D3RO Voice Setup 1.0.0.exe 실행 직후 System.dll 0xc0000005 액세스 위반
  • 근본 원인: 설치 파일이 NSIS 2GB 한도 초과(Ollama 전체 ~2GB)
  • 해결 3종:
    1. download-ollama.ps1에서 GPU 런타임(cuda_v11/cuda_v12/rocm_avx) 가지치기 → ~1GB 절감
    2. nsis.oneClick: true + allowToChangeInstallationDirectory: false → electron-builder issue #7921 회피
    3. installer.nsh 커스텀 include 비활성화(커스텀 ReadRegDWORD/MessageBox는 v0.1 이후 재검토)

온보딩 모달 버그(비크래시)

  • {model}, {status}, {message} 플레이스홀더가 화면에 그대로 노출
  • 원인: @d3ro/i18n의 resolveTranslation은 {{var}}(이중 중괄호) 포맷 요구
  • 다음 태그 빌드부터 반영 (수정 완료, main push 완료)

현재 상태

  • 설치파일 설치 성공
  • 앱 기동 성공
  • 온보딩 모달(gemma4:e4b 미설치 감지) 표시 확인
  • 다음 검증: 다운로드 시작 → 진행률 UI → 전사 E2E

다음 작업

  1. 현재 설치 앱으로 "다운로드 시작" 누르고 gemma4:e4b pull 동작 확인
  2. Alt+Shift+1 단축키로 전사 E2E 테스트 (SoX + 번들 sidecar + Whisper large-v3 자동 DL)
  3. i18n 플레이스홀더 수정본 재배포용 v0.1.0-alpha.1 태그 (선택)
  4. Whisper 모델 다운로드 진행률 UI (Phase 2 bootstrap)
  5. 설정 페이지에 모델 선택지(gemma4:e2b / gemma3:4b fallback)
  6. 아이콘 디자인 준비 후 build/icon.png|.ico 추가 + win.icon 복원

참고 파일

  • .gitlab-ci.yml — 현재 Windows runner 단독 파이프라인
  • C:\GitLab-Runner\config.toml — environment PATH 주입, 백업은 config.toml.bak
  • apps/desktop/resources/sox/ — 바이너리 직접 커밋(약 6MB)

배포 파이프라인 확장 (2026-04-15 PM) ✅

추가 변경

  • installer.nsh: NSISdl로 aka.ms/vs/17/release/vc_redist.x64.exe 사일런트 다운로드/설치. 실패 시 수동 설치 안내.
  • LocalLLMService.pullModel(modelId): Ollama /api/pull 스트리밍 구독 → pull-progress 이벤트 emit.
  • LLM IPC 확장: LLM.PULL_MODEL 핸들러 + LLM.PULL_PROGRESS 이벤트 브로드캐스트. preload에 llm.pullModel(), llm.onPullProgress() 추가.
  • OnboardingModal 재작성: gemma4:e4b 미설치 자동 감지(15초 간격 polling) → 진행률 모달(0~100%) → 성공 시 config.onboardingCompleted=true 저장.
  • i18n ko/en: onboarding.* 키 14개 추가.

태그 푸시

  • v0.1.0-alpha 푸시 완료 → GitLab d3r0/voice 파이프라인 트리거됨.
  • CI 파이프라인: lint/typecheck/test (Linux docker) → package-windows (Windows runner, 태그 필요) → release-create (GitLab Releases 자동 생성).

다음 작업

  1. Windows GitLab Runner 등록 (이 PC, yunchan8804)
    • gitlab-runner.exe 설치 + shell executor
    • tags: windows, shell
    • Python 3.11 권장(현재 3.13 — faster-whisper/ctranslate2 호환 확인 필요)
  2. 첫 파이프라인 실행 관찰 + 실패 시 튜닝
  3. Whisper large-v3 다운로드 진행률 UI (Bootstrap 2단계)
  4. 설정 페이지에서 모델 변경(gemma4:e2b/gemma3:4b) 선택지 노출

배포 파이프라인 기반 구축 (2026-04-15) ✅

결정 사항

  • 기본 LLM 모델: gemma4:e4b (9.6GB, 텍스트+이미지+오디오 멀티모달)
  • Ollama 배포 방식: 바이너리 번들 (포터블 zip을 resources/ollama/에 동봉)
  • Whisper 기본 모델: large-v3 (3GB, 첫 실행 시 자동 다운로드)
  • Sidecar 슬림화: torch, pyannote.audio 제거 → GPU 감지는 ctranslate2 사용. /diarize 엔드포인트 삭제. 화자 구분은 LLM 추정 경로로 폴백(기존 MeetingModeService가 자동 처리).
  • SaaS 지향: 고정밀 화자 구분은 추후 서버 사이드 API로 제공 예정.

변경된 파일

  • apps/desktop/sidecar/main.py: torch/pyannote 의존 제거, /diarize 삭제, ctranslate2 GPU 감지
  • apps/desktop/sidecar/requirements.txt: torch/pyannote 제거 (faster-whisper + fastapi + uvicorn + numpy만)
  • apps/desktop/src/main/utils/paths.ts: getBundledOllamaPath() 추가
  • apps/desktop/src/main/services/LocalLLMService.ts: _findOllamaBinary()가 번들 ollama를 1순위로 탐색
  • apps/desktop/electron-builder.yml: resources/ollama/ extraResources + nsis.include: build/installer.nsh
  • apps/desktop/scripts/download-ollama.ps1 (Windows), download-ollama.sh (mac/linux) 신규
  • apps/desktop/build/installer.nsh 신규 — VC++ 재배포 확인 + 언인스톨 시 번들 ollama 종료
  • .gitlab-ci.yml: Windows self-hosted runner에서 sidecar 빌드 → sox 다운로드 → ollama 다운로드 → electron-builder 파이프라인. 태그 푸시 시 GitLab Release 자동 생성.
  • .gitignore: resources/ollama/ 빌드 산출물 무시

다음 작업

  1. Windows self-hosted GitLab runner 등록 (tags: windows, shell)
  2. 첫 실행 bootstrap UI — gemma4:e4b + Whisper large-v3 다운로드 진행률 모달
    • 위치: Dashboard 진입 전 게이트
    • IPC 채널: 설계서 02의 ipc-channels.ts에 setup:pull-model, setup:download-whisper, setup:progress 추가 필요
  3. VC++ 재배포 자동 다운로드(inetc plugin 검증 필요) 또는 번들화(25MB 추가)
  4. 첫 태그 푸시(예: v0.1.0-alpha)로 CI 검증

차단 이슈 (기존)

  • @nut-tree-fork/nut-js 포크 사용 (변경 없음)

RN CLI 전환 (2026-04-13) ✅

Expo → React Native CLI 전환

  • apps/mobile-rn: RN 0.85 + React 19 + TypeScript
  • 기존 Expo 코드(apps/mobile-expo) → RN CLI용 6개 화면 이식 완료
  • Navigation: @react-navigation/native + bottom-tabs + native-stack
  • 공유 패키지: @d3ro/ui-native, @d3ro/i18n, @d3ro/core, @d3ro/api-client 연결

Android 빌드 환경 (Windows)

  • JDK 17 (Microsoft OpenJDK 17.0.18)
  • Android SDK: platform-34, build-tools-34, NDK 27.1, CMake 3.22.1
  • Windows NDK CMake 버그 워크아라운드: libc++_shared.so가 자동 링크 안 됨
    • scripts/fix-windows-ndk.sh: ninja 파일 후패치 스크립트
    • 빌드 순서: gradlew configureCMakeDebug → fix-windows-ndk.sh → ninja 수동 빌드 → gradlew assembleDebug -x :react-native-screens:buildCMakeDebug[arm64-v8a]
    • reactNativeArchitectures=arm64-v8a (32-bit 제외)
    • newArchEnabled=false

APK 설치 성공

  • SM_F956N (갤럭시 폴드) 무선 디버깅으로 설치 확인
  • Metro 번들러 연결 확인 (RN v0.85, Metro v0.84.2)

다음 작업 (이어서)

  • Metro 번들 에러 확인/수정 (화면 렌더링 테스트)
  • expo-av → react-native-audio-recorder-player 교체 (RecordScreen)
  • postinstall 스크립트로 Windows NDK 패치 자동화
  • 커밋 후 apps/mobile-expo 정리 여부 결정

규칙 13: 작업 완료 즉시 이 파일 갱신 의무

Phase M-2: D3RO 디자인 시스템 모바일 적용 (2026-04-13) ✅

DS 컴포넌트 (packages/ui-native)

  • 기존 4개 + 신규 5개 = 총 9개: MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel, WaveBars, AppStatusBar, Header, FilterChip
  • 테마 토큰 SSOT: d3roNativePalette / d3roNativeTypo / d3roNativeRadius / d3roNativeFonts
  • WaveBars: core Animated API 사용 (reanimated 의존성 제거)

탭 화면 D3RO 디자인 적용 (5개 + 로그인)

  • dash.tsx: Session Overview (ScreenPanel), 2x2 Stats Grid, Backend/Tier Info, Usage Panel
  • history.tsx: FilterChip (ALL/SAVED/PROC), 날짜 그룹핑, LED 상태, 단어수 배지
  • record.tsx: WaveBars 애니메이션, 녹음 상태머신, 실시간 전사 UI
  • talk.tsx: 채팅 버블 (AI/User), 날짜 배지, 입력 영역
  • settings.tsx: 섹션별 카드 (Account, Backend, Preferences)
  • login.tsx: LED 로고, OAuth 3종, 이메일/비번, D3RO 스타일

i18n 연결

  • I18nProvider를 root layout에 연결
  • 모바일 전용 키 80+개 추가 (ko.json, en.json)
  • 모든 UI 문자열 t() 함수 사용

인프라

  • apps/mobile을 모노레포 workspaces에 추가
  • saasMode 분기 제거 커밋 완료

알려진 이슈

  • @types/react 버전 충돌 (desktop=19, mobile=18) → TS2786만 발생, Metro에 무영향
  • react-native-screens bob build 실패 → --ignore-scripts로 해결, EAS Build에서는 정상

Admin 콘솔 리디자인 + 랜딩 SaaS 전환 (2026-04-13) ✅

Admin CRM 콘솔 스타일 리디자인

  • 전체 페이지 D3RO Console 스타일 적용 (Overview, Users, Subscriptions, Audit Log + 모든 상세)
  • console-theme.ts 디자인 토큰 시스템 (C 색상, FONT, panelSx, tableSx, filterBtnSx, statusBadgeSx)
  • globals.css — 스캔라인 애니메이션, 그리드 패턴, 글로우 효과, 커스텀 스크롤바
  • JetBrains Mono 폰트, 검정 배경, 라운드 패널, 오렌지 액센트
  • MetalCard/PhosphorText → Box + sx 전환 (MUI는 유지, d3ro DS 컴포넌트 미사용)

SSE 스트리밍 Premium LLM 연결

  • VoiceConversationService에 Premium/Local LLM 라우팅 추가
  • _createChatStream(): configGet('llmBackend') 기반 분기, Premium 실패 시 Local fallback
  • PremiumLLMService.chatStream() → Anthropic SSE → 토큰 단위 yield → TTS 파이프라인

코드 정리

  • Supabase 클라이언트 공통화: packages/api-client에 supabase-browser.ts, supabase-server.ts 추출
    • Browser: 싱글톤 캐싱 + isSupabaseConfigured() 완전 공유
    • Server: CookieStore 인터페이스 팩토리 패턴 (next/headers 의존성 분리)
    • apps/web, apps/admin → 3줄 re-export 래퍼로 축소
  • RPC 함수 타입 9개 추가: packages/api-client/src/types.ts Functions 섹션
    • generate_invite_token, user_team_ids, user_admin_team_ids, match_knowledge_chunks
    • consume_quota, increment_daily_usage, admin_usage_by_feature, admin_top_users, admin_dau

랜딩 페이지 SaaS 구독 모델 전환

  • Pricing: 1회 결제 → 월간($9.9/$19.9) / 연간($99/$199) 토글
  • 10개 언어 i18n 전체 동기화 (pricing, FAQ, CTA)
  • FAQ: 구독 관련 답변 업데이트
  • CTA: "구독 없음" → "프리미엄 기능, 당신의 조건으로"
  • 다운로드 URL: placeholder (#) — 추후 실제 URL 제공 시 교체

V2-6.1: 3단계 권한 체계 (2026-04-12) ✅

변경 내역

  • DB: profiles_role_check에 manager 추가, RLS 정책 7개 재작성 (manager 읽기, admin 쓰기, manager 구독 UPDATE)
  • Edge Functions 4개: requireManager/requireAdmin 3단계 분기 적용 + 배포 완료
  • Frontend: admin-guard.ts 3단계 가드, UI 컴포넌트 권한별 표시 분기
  • 토큰 갱신 버그 수정: callAdminApi에서 getSession() → getUser() 선행 호출

권한 체계

기능 manager admin super_admin
조회 전체 ✅ ✅ ✅
구독 수정 + 메모 ✅ ✅ ✅
구독 생성 (VIP) ❌ ✅ ✅
구독 삭제 ❌ ✅ ✅
role 변경 (user↔manager) ❌ ✅ ✅
role 변경 (→admin/super_admin) ❌ ❌ ✅

Phase V2-6: Admin CRM 고도화 (2026-04-13 09:30) ✅

DB 마이그레이션 (20260413000004_admin_enhancement.sql)

  • audit_log 테이블: admin_id, action, target, before/after JSON diff, memo
  • subscriptions.admin_note 컬럼 추가
  • profiles.role CHECK 확장: user/admin/super_admin
  • RLS: admin+super_admin 조회, super_admin 쓰기
  • SQL RPC 3종: admin_usage_by_feature, admin_top_users, admin_dau

Edge Functions 4개 (admin-users, admin-subscriptions, admin-payments, admin-audit-log)

  • admin-users: 유저 목록/상세 + super_admin role 변경
  • admin-subscriptions: 구독 CRUD (생성/수정/소프트삭제) + 메모 필수 + 감사로그 자동 기록
  • admin-payments: DB + Payple API 병행 결제 이력 조회
  • admin-audit-log: 감사로그 목록/상세 (필터: target_type, admin_id, 날짜)
  • 공유 유틸: _shared/admin-auth.ts (권한 검증), _shared/audit.ts (감사 기록)

Swagger UI (apps/admin-swagger/)

  • 독립 정적 페이지 + OpenAPI 3.0 spec
  • Bearer token 입력으로 admin 인증

Admin CRUD 웹페이지 (apps/admin)

  • 구독 상세/수정/삭제: /subscriptions/[id] + SubscriptionForm + MemoDialog
  • 구독 생성 (VIP 부여): /subscriptions/new
  • 유저 상세: role 변경 버튼 (super_admin만) + RoleChangeDialog
  • 감사로그: /audit-log 목록 + /audit-log/[id] 상세 diff (AuditDiffViewer)
  • 결제 이력: PaymentHistory 컴포넌트 (DB + Payple 병행)
  • 사이드바: Audit Log 메뉴 + API Docs 외부 링크

recharts 차트 3종

  • FeatureUsageChart: feature별 일간 StackedBar
  • DauChart: DAU 추이 Line
  • TopUsersChart: 유저별 사용량 Top 20 HorizontalBar
  • Usage 페이지: 차트 + 기간 필터(7/14/30일) + 테이블

권한 체계

  • super_admin: 구독 CRUD, role 변경, 구독 삭제 (위험 작업)
  • admin: 전체 조회 전용
  • admin-guard.ts: requireAdmin(), requireSuperAdmin(), isSuperAdmin()

Admin CRM 독립 프로젝트 (2026-04-12 22:00) ✅

  • apps/admin Next.js 독립 앱 (포트 3001) — apps/web에서 완전 분리
  • OAuth 로그인 + app_metadata 기반 admin 가드
  • RLS: 재귀 참조 수정 → auth.jwt() -> app_metadata 기반
  • middleware: Supabase 세션 갱신
  • 4개 페이지: Overview / Users / Subscriptions / Usage

마스터플랜 4개 작업 (2026-04-12 21:00) ✅

작업 1: LemonSqueezy 코드 정리 (36031ac)

  • LicenseService.ts ~150줄 삭제 (API 상수/인터페이스/메서드 4개)
  • activate/deactivate 로컬 키 검증 전용으로 단순화
  • site/ taxNote: LemonSqueezy → Payple (10 locale)

작업 2: Payple 정기 갱신 크론 (d3c2a43)

  • payple-renew Edge Function: 만료 구독 자동 재결제 + 3회 실패 시 다운그레이드
  • migration: subscriptions.renewal_failures 컬럼 추가
  • GitHub Actions: 매일 01:00 UTC 스케줄
  • payple-webhook: 결제 완료 시 renewal_failures=0 리셋

작업 3: 관리자 CRM 웹페이지 (667c092)

  • migration: profiles.role 컬럼 + admin RLS 정책 3개
  • /admin 대시보드: 총 유저, 유료 구독자, API 호출, 만료 예정
  • /admin/users: 검색/필터/페이지네이션 + 유저 상세
  • /admin/subscriptions: 상태 필터
  • /admin/usage: feature별 집계 (7/14/30일)
  • Sidebar에 Admin 네비게이션 추가

작업 4: SSE 스트리밍 Premium (7467bd6)

  • sse-parser.ts: Anthropic SSE 파서 (content_block_delta.text 추출)
  • CloudSyncService.invokeFunctionStream(): raw fetch + ReadableStream
  • PremiumLLMService.chatStream(): stream=true, 토큰 단위 yield + fallback

라이선스 UI 빅뱅 리라이트 (2026-04-12 20:00) ✅

LemonSqueezy 키 입력 방식 → Supabase 구독 + Payple 결제 기반 UI로 전면 교체

변경 내역

  • LicenseTab.tsx / LicenseModal.tsx: 키 입력 제거, 클라우드 인증 + 업그레이드 버튼
  • useLicenseState.ts 훅: 공용 라이선스+클라우드 상태 관리 (중복 제거)
  • PREMIUM_MODEL_LIMITS: 3곳 중복 → @d3ro/core/constants 단일 소스
  • i18n: LemonSqueezy 전용 키 18개 삭제, 구독 관련 키 13개 추가 (12 locale)
  • DashboardPage: dashboard.model* → license.model* i18n 키 통일

알려진 이슈

  • LicenseService.ts의 activate/deactivate 메서드 + IPC 핸들러: 코드 잔존 (UI 미사용)
  • 다음 사이클에서 LemonSqueezy API 코드 완전 제거 예정

Phase 3.2-B 배포/확인/버그수정 (2026-04-12 19:30) ✅

배포 완료

  • DB migration 20260412000002_payple_billing.sql → Supabase 리모트 적용
  • Payple secrets (테스트 키) → Supabase Secrets 설정
  • Edge Functions 3개 (payple-checkout, payple-webhook, payple-manage) 배포 ACTIVE
  • Electron OPEN_BILLING: 개발 환경 localhost:3000 / 프로덕션 d3ro.dev 분기 추가

대시보드 시각 확인 완료

  • Premium LLM LED (CLAUDE) 초록색 표시 ✓
  • 티어 카드 (FREE) 앰버 LED ✓
  • 사용량 바 + Premium 쿼터 (Haiku 268/주간) ✓
  • i18n 키 14개 전부 존재 확인 ✓

세션 만료 버그 수정

  • CloudSyncService.ts: stored token 없을 때 auth-changed { user: null } emit 추가
  • 렌더러 CloudSyncSection이 초기 상태를 올바르게 수신하게 됨

Phase 3.2-B Payple 결제 연동 + 대시보드 Premium 상태 (코드 구현, 2026-04-12) ✅

A. Payple 결제 연동

Stripe 기반 결제를 Payple(한국 PG)로 전면 교체. 웹(apps/web) 결제 페이지가 핵심.

신규 파일:

  • server/supabase/functions/_shared/payple.ts — Payple API 래퍼 (auth, billing, cancel, deleteBillingKey)
  • server/supabase/functions/payple-checkout/index.ts — 빌링키로 첫 결제 + 구독 활성화
  • server/supabase/functions/payple-webhook/index.ts — 결제완료/취소 이벤트 처리
  • server/supabase/functions/payple-manage/index.ts — 구독 취소 (빌링키 해지)
  • server/supabase/migrations/20260412000002_payple_billing.sql — payment_provider, payple_payer_id, payple_pay_oid 컬럼
  • apps/web/src/components/billing/payple-checkout-button.tsx — Payple JS SDK 결제창 호출
  • apps/web/src/components/billing/payple-manage-button.tsx — 구독 관리/취소 다이얼로그

수정 파일:

  • apps/web/src/app/(app)/billing/page.tsx — 티어 통일(team→pro_plus) + Payple 컴포넌트로 교체
  • apps/desktop/src/renderer/components/LicenseModal.tsx — 업그레이드 → shell.openExternal(웹 결제)
  • packages/core/src/ipc-channels.ts — LICENSE.OPEN_BILLING 추가
  • apps/desktop/src/preload/index.ts — license.openBilling() API
  • apps/desktop/src/main/ipc/license-handlers.ts — OPEN_BILLING 핸들러
  • server/supabase/config.toml — payple-* verify_jwt=false

B. 대시보드 Premium 상태 표시

  • DashboardPage services 배열에 PREMIUM LLM LED 추가 → CrtDisplay 자동 렌더
  • 백엔드 인디케이터 카드(Local/Premium) + 티어 카드
  • 사용량 바: 모든 티어에서 표시 + Premium 모델별(Haiku/Sonnet/Opus) 쿼터 섹션

i18n

12개 locale에 14개 키 추가 (service.premiumLlm, dashboard.currentBackend 등)

알려진 이슈 (이월)

  1. 세션 만료 렌더러 미동기화 — ✅ 수정 완료 (stored token 없을 때 auth-changed null emit)
  2. LemonSqueezy 코드 잔존 — LicenseService.ts에 @deprecated 코드. 다음 사이클에서 정리
  3. Payple 테스트 키 — 라이브 전환 시 PAYPLE_CST_ID/CUST_KEY/REFUND_KEY/CLIENT_KEY Supabase Secrets 등록 필요
  4. Payple 정기 갱신 크론 — payple-renew 미구현. pg_cron 또는 외부 스케줄러 필요

백로그

  • 관리자 백엔드 서비스 웹페이지: 사용자 관리 CRM, 구독 현황 대시보드, 쿼터 관리, 결제 이력 조회. 별도 admin/ 앱 또는 apps/web 내 admin 라우트 그룹으로 구현 예정.
  • Payple 정기 갱신 크론 (payple-renew)
  • SSE 스트리밍 Premium

Phase 3.2 Premium LLM 완결 (2026-04-12) — 빅뱅 8/8 마지막 성공 기준 ✅

Supabase Edge Function(llm-proxy)을 통해 Anthropic Claude를 호출하는 PremiumLLMService 구현. 사용자가 Settings에서 Local/Premium 백엔드를 선택하면 VoiceModeService가 자동 분기하고, Premium 실패 시 Local로 silent fallback + 상단 중앙 배너 알림.

커밋 3개 (이번 세션)

SHA 내용
a744551 fix: Bug 13 전면 해소 — Conversation 에러 배너 + Dictation 빈 STT 가드 + recording tip 타이머 leak
d397bcb feat: LLM 기본 모델 qwen3:4b → gemma4:e4b 전면 전환 + think:false 안전장치
6e52c18 feat: Phase 3.2 Premium LLM — Anthropic Claude 파이프라인 + 모델별 쿼터 + SaaS UI

실측 결과

경로 LLM refine 시간
이전 qwen3:4b (local, reasoning) 42.9초
gemma4:e4b (local, non-reasoning) 3.9초
Claude Haiku (premium) 1.6~3.2초

주요 신규/변경 파일 (23개)

신규: PremiumLLMService.ts, llm-prompts.ts, migration 20260412000001 서버: quota.ts (모델별 쿼터), llm-proxy/index.ts, config.toml (verify_jwt=false) 데스크톱: VoiceModeService (router+fallback), CloudSyncService (invokeFunction, getAccessToken async), ConfigService (llmBackend), LicenseService (쿼터 250/1500), llm-handlers (safeSendToRenderer+premium IPC), preload (premium API) UI: SettingsModal (backend 드롭다운+conditional), LicenseModal (SaaS 전환), AppLayout (fallback 배너) 타입: AppConfig.llmBackend, SubscriptionTier pro_plus, IPC_CHANNELS.LLM.PREMIUM_*

모델별 쿼터 정책

Free Pro ₩9,900 Pro+ ₩29,900
로컬 ∞ ∞ ∞
Haiku 250/주간 1,500/일 ∞
Sonnet ❌ 300/일 1,500/일
Opus ❌ 50/일 300/일

알려진 이슈 (이월)

  1. 세션 만료 시 렌더러 미동기화 — CloudSyncService.init()에서 stored token 없을 때 auth-changed null 미emit → UI 불일치 (memory/feedback_session_expiry_bug.md)
  2. Payple 결제 연동 미완 — 업그레이드 버튼 클릭 시 "결제 연동 준비 중 (Payple)" alert 스텁. Phase 3.2-B에서 실제 연결
  3. 대시보드 Premium 상태 표시 — 현재 대시보드에 Premium LLM 쿼터/모델 정보 미노출
  4. Supabase 2026 verify_jwt 비호환 — sb_publishable_ 키와 Gateway JWT 검증 비호환으로 verify_jwt=false 설정. 함수 내부 requireUser()에서 직접 인증 처리 중

빅뱅 Phase 5 Part 7 (2026-04-11) — Bug 13 전면 해소 + Gemma4 기본 모델 전환 ✅

Part 6에서 Voice Conversation 몰입 패널까지 가고 남은 숙제였던 Bug 13 전면 해소와 사용자 실측에서 발견된 LLM refine 42.9초 병목(qwen3 reasoning 모델 strip empty fallback)을 한 사이클에 정리. 받아쓰기 한 바퀴 51초 → 기대 7~10초로 대폭 단축 예상.

7-A. Voice Conversation 에러 배너 UI (Part 6 후속)

VoiceConversationPage.tsx의 onError 콜백이 기존에는 주석만 있고 UI 피드백이 없었음. MUI Snackbar + Alert(severity="warning" variant="filled") 하단 중앙 4초 auto-hide 배너 추가. 에러 매핑 헬퍼 formatErrorMessage:

  • phase === 'stt' && message.startsWith('no speech') → i18n conversation.error.noSpeech
  • 그 외 → ${phaseLabel}: ${rawMessage} (phaseLabel 3종 i18n)

i18n 키 4개 추가 (ko/en): conversation.error.phase.{stt,llm,tts} + conversation.error.noSpeech

7-B. Dictation Bug 13 가드 (VoiceModeService)

_transcribe()에 2중 가드 추가 — VoiceConversationService와 동일 패턴.

  1. minBytes 가드 (16kHz 16bit mono 기준 0.5초 = 16000 bytes 미만) → _handleError(STTAudioTooShort, 'No speech detected...') → recording-tip popup이 'error' 상태로 전환 → 3초 자동 숨김
  2. 빈 result.text 가드 → _handleError(STTNoAudioData, 'No speech detected...') (동일 경로)

실측 로그에서 확인된 기존 버그: 1.5초 무음 오디오 → VAD 전체 filter → 빈 전사 → LLM 5초 대기 → 빈 session completed → 빈 history entry 생성 시퀀스가 모두 차단됨.

7-C. Recording Tip hide 타이머 leak 수정 (VoiceModeService)

사용자 실측에서 발견된 추가 버그 — "받아쓰기 꾹 누르고 있는데 2초 뒤에 popup이 사라짐". 원인은 _handleError의 setTimeout(() => hideRecordingTip(), 3000)이 핸들로 보관되지 않아 다음 세션 시작 후에도 예약대로 발사되어 현재 진행 중인 recording tip을 hide 시킴. Bug 13 가드 추가로 에러 경로가 자주 타면서 드러난 기존 잠재 버그.

수정: _errorHideTimer: NodeJS.Timeout | null 필드 추가 → _handleError에서 핸들 보관 + 기존 타이머 clearTimeout 후 재예약, _startSession 초기화 블록에서 _errorEmitted = false와 함께 clearTimeout, dispose()에서도 leak 방지용 cleanup. 실측 확인: 4.5초 hold 성공, 전사 "Hey, yeah, yeah..." 정상 완료.

7-D. LLM 기본 모델 전면 교체 — qwen3:4b → gemma4:e4b ⭐

발견한 병목: 받아쓰기 한 사이클 51.4초 = STT 7.4초(PyTorch 미설치, CPU int8) + LLM 43초(!) + insert 1초. LLM 43초의 원인은 qwen3:4b가 reasoning 모델이라 <think>...</think> 블록을 길게 생성 → stripReasoningBlocks 후 빈 문자열 → 원본 transcript fallback. 즉 43초를 기다렸는데 결과는 원본 그대로.

해결: 2026-04-02 릴리스된 Google Gemma 4 e4b (4.5B effective params)를 기본으로 교체. non-reasoning 기본 동작 + Ollama v0.20+ think: false 파라미터로 2중 방어.

변경된 위치 (한 번에):

  • LocalLLMService.ts — 3개 default fallback (generate/streamGenerate/chatStream) + Ollama 요청 body에 think: false 명시 추가 + NO_THINK 주석 업데이트(legacy 표기)
  • OnboardingModal.tsx:246 + OllamaGuideModal.tsx:133 — pull 명령어 변경
  • packages/i18n/src/locales/{12개}.json — settings.ollamaHint, ollama.step2.alt 2개 키 (qwen3:4b → gemma4:e4b, qwen3:8b → gemma4:26b)
  • site/src/i18n/locales/{10개}.ts — 마케팅 페이지 detail 문자열 qwen3 / llama3 / gemma3 → gemma4 / llama3.2 / phi4
  • site/src/sections/HowItWorks.tsx:47 — 파이프라인 시각화 qwen3:4b @ localhost → gemma4:e4b @ localhost
  • apps/desktop/tests/main/services/VoiceModeService.test.ts:58 — test fixture llmModelId
  • docs/design/00-master-architecture.md — LLMConfig.model 주석 + CONFIG_DEFAULTS.llm.model
  • docs/design/05-external-engine-integration.md — 6개 API 스키마 예시 + 2개 OllamaClient 코드 예시 + LLM 모델 추천 표(gemma4:e4b를 최상위로 재정렬, qwen3를 reasoning 경고와 함께 후순위로) + 권장 JSON 설정(think:false 추가)
  • docs/phases/phase-14-meeting-mode.md — 컨텍스트 윈도우 표 갱신

리서치 경로: Ollama 라이브러리 확인 → gemma4는 E2B(2.3B), E4B(4.5B), 26B MoE, 31B Dense 4종 + 31b-cloud. Thinking은 <|think|> 토큰을 system prompt에 넣어야 켜지는 opt-in 방식. 기본 추천 E4B. 사용자는 이미 Ollama 0.20.5 사용 중(gemma4 요구 0.20+).

변경 파일 (대규모) — 40+ 파일

영역 파일 수 성격
코드 (메인) 1 LocalLLMService.ts
UI 컴포넌트 3 OnboardingModal, OllamaGuideModal, VoiceConversationPage
서비스 (버그 수정) 1 VoiceModeService
테스트 1 VoiceModeService.test
i18n (desktop) 12 ko/en/ja/zh/zh-TW/es/fr/de/pt/ru/vi/th
i18n (site) 10 ko/en/ja/zh/es/fr/de/pt/ru/vi
마케팅 컴포넌트 1 HowItWorks.tsx
설계 문서 3 00-master, 05-external-engine, phase-14

검증 게이트

게이트 결과
desktop tsc --noEmit ✅ EXIT 0
Bug 13 Dictation minBytes 가드 ✅ 로그 확인 (전에 빈 history entry 생성되던 경로 차단)
Bug 13 Conversation 배너 UI ✅ Snackbar severity=warning filled 노출
Recording tip hide 타이머 leak ✅ 4.5초 hold 성공, 전사 정상
기본 모델 gemma4:e4b 전환 ⏳ 사용자 수동 pull + 실측 대기

다음 세션 액션

  1. 사용자 ollama pull gemma4:e4b (~4.5GB)
  2. 받아쓰기 한 사이클 실측 → refine 지연 측정 → 43초 → 몇 초 기대
  3. 결과 컨펌되면 커밋 정리 + 빅뱅 8/8 마지막 1개(Phase 3.2 PREMIUM_LLM)로 전환
  4. Phase 3.2 선행 조건: 사용자가 supabase secrets set ANTHROPIC_API_KEY=... OPENAI_API_KEY=...

빅뱅 Phase 5 Part 6++ (2026-04-11) — U1/U3/U4 남은 이슈 정리 ✅

U1 — Realtime TIMED_OUT 자동 재구독 (CloudSyncService.ts)

Publication/RLS/setAuth 모두 설정 완료 상태에서도 매 기동 시 TIMED_OUT이 찍히는 증상. 근본 원인은 Supabase 서버 측 transient(네트워크/WebSocket 타임아웃) 의심으로 판정. Auto push(Phase 3.3) fallback이 완벽히 동작해 블로커는 아니지만, 다른 기기에서의 원격 변경 pull을 놓치는 window가 생김.

변경: subscribe() 콜백에 지수 백오프 자동 재구독 추가.

  • SUBSCRIBED → retry count reset + 타이머 클리어
  • TIMED_OUT / CHANNEL_ERROR / CLOSED → _scheduleRealtimeRetry()
  • 백오프: 1s → 3s → 10s, 3회 초과 시 포기하고 경고 로그(Auto push로만 동작)
  • stopRealtime()에서 타이머 cleanup + count reset

U3 — Refresh token 실패 시 재인증 이벤트 emit (CloudSyncService.ts)

init()의 refreshSession 실패 브랜치에서 기존에는 warn 로깅 + 토큰 삭제만 수행 → 사용자는 "왜 로그아웃됐지?" 상태로 방치. 재현이 쉽지 않지만 장시간 idle 후 발생 가능성 있어 예방적 개선.

변경:

  • logger.warn → logger.error(에러 레벨 승격)
  • sync-error 이벤트 emit ("Sign-in session expired. Please sign in again.")
  • auth-changed { user: null } emit (renderer 게이트 재잠금)
  • try/catch의 catch 브랜치도 동일 처리 (기존에는 로깅만)

U4 — package-lock optional dep (확인 후 유지 결정)

package-lock.json에 rollup의 전 플랫폼 optionalDependencies(darwin/win32/linux/freebsd/android)가 기록돼 있음. Mac npm install --force 시 win32 관련 경고가 나왔지만 런타임 영향 없음. apps/desktop/package.json:56에 @rollup/rollup-win32-x64-msvc가 명시적 의존으로 선언되어 있는 이유는 Windows 빌드자가 사전에 binding을 확보하기 위함.

결정: 유지. 크로스플랫폼 개발/CI 효율 + Win 빌드자 경험을 우선. 향후 lock 재생성 시에도 선택적 제거 불필요. 이 이슈는 close.

변경 파일 (2)

  • apps/desktop/src/main/services/CloudSyncService.ts — U1 재구독 + U3 재인증 이벤트
  • memory/project_status.md — 본 섹션

빅뱅 Phase 5 Part 6+ (2026-04-11) — U7 AudioCaptureService Mac 분기 ✅

AudioCaptureService.getDevices()가 powershell -NoProfile만 호출하던 것을 process.platform 분기로 교체. Mac에서 system_profiler SPAudioDataType -json으로 CoreAudio input 디바이스 열거(coreaudio_device_input > 0 필터). macOS SoX는 실제 캡처 경로에서 -d(default) 고정이므로 deviceId='default', label만 사용자 구분용으로 노출. 기존 "Failed to enumerate audio devices via PowerShell" 경고

  • 1 device fallback 해소.

변경 파일 (1): apps/desktop/src/main/services/AudioCaptureService.ts — _getDevicesWindows / _getDevicesMac helper 추출 + 분기 디스패처.

검증: Electron 재기동 후 로그 Found 2 audio device(s) on darwin 확인 (경고 없음).

빅뱅 Phase 5 Part 6 (2026-04-11) — Voice Conversation UX 몰입 패널 ✅

Part 5-C 설계를 구현으로 완결. 사용자가 listening 상태에서 풀 몰입 계측기 모드 + 사운드 피드백 + 상태별 UI 분기를 실제로 경험할 수 있는 상태.

변경 파일 (6)

  • packages/core/src/ipc-channels.ts — VOICE_CONVERSATION.AUDIO_LEVEL 채널 추가
  • apps/desktop/src/main/services/SoundEffectService.ts — SoundName에 'chime' 추가 (recording-stop.wav 재사용)
  • apps/desktop/src/main/services/VoiceConversationService.ts — _onAudioLevel forwarding + 사운드 훅 4개(start/stop/chime/error) + Bug 13(빈 STT 피드백)
  • apps/desktop/src/preload/index.ts — voiceConversation.onAudioLevel 구독 API
  • apps/desktop/src/renderer/components/voice-conversation/VoiceRecordingPanel.tsx — 신규 9바 phosphor waveform(BAR_COUNT=9, cos 분포, 100ms, SMOOTHING=0.5, RANDOM_FACTOR=0.35) + REC LED + elapsed 타이머 + "SPEAK NOW" 힌트
  • apps/desktop/src/renderer/pages/VoiceConversationPage.tsx — state 분기(listening=VoiceRecordingPanel / 그 외=메시지 리스트) + thinking 시 typing indicator 버블 + TextField listening 시 숨김·thinking/speaking 시 disabled
  • packages/i18n/src/locales/{ko,en}.json — conversation.recording.hint, conversation.thinking.placeholder, conversation.state.recording 3개 키

해소 이슈

  • U6 Voice Conversation UX 몰입 패널 구현
  • U8 Bug 13 빈 STT 피드백 부재 (minBytes 미달 + VAD 무음 판정 양쪽 모두 _emitError('stt', ...) 훅 추가)

검증

  • desktop tsc --noEmit EXIT 0
  • Vite renderer HMR 자동 반영, Electron kill + nohup 재기동(PID 9635/9641)
  • 사용자 녹음 테스트: 한국어 5.1초 오디오 전사 성공("에이에이에이에이 또 검중이래요") → 파이프라인 살아있음 확인

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 주석처리)

주입 예:

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이 자동으로 새 포트 사용.
  • [K] 🐛 fix(desktop): macOS 텍스트 붙여넣기 — TextInsertService._insertViaClipboard가 nut.Key.LeftControl + V 하드코딩이라 macOS에서는 paste 안 됨 (Cmd+V여야). pasteModKey = process.platform === 'darwin' ? LeftSuper : LeftControl 분기. 잘못된 클립보드 검증("text 그대로면 정상") 로직 제거 (macOS는 Ctrl+V가 noop이라 항상 거짓 양성). macOS는 paste 후 sleep 250ms.
  • [L] 🐛 fix(desktop): qwen3 reasoning 39초 + 빈 응답 — qwen3:4b는 reasoning model이라 응답에 <think>...</think> 블록을 길게 출력, 본문이 비어 _completeSession("")로 paste 호출 안 됨. SYSTEM_PROMPTS 전체에 /no_think 토큰 추가 (qwen3 비활성화, 다른 모델은 무시). stripReasoningBlocks() 추가 — <think>/<thinking> 태그 안전망. processText가 stripped 결과 길이 0이면 원본 transcript fallback + warn 로그.

SaaS 데스크톱 클라이언트 1차 (2026-04-11)

사용자 표현: "설계가 잘못된 것 같다" 실제 상황: 백엔드/웹/모바일은 100% multi-tenant SaaS, 데스크톱만 V1 single-user 잔재로 사용자가 Settings에서 Supabase URL/Key를 직접 입력해야 하는 UX였음. Notion/Linear/Slack 데스크톱 패턴으로 정정.

SaaS [1] Supabase 연결 — 소스 하드코딩 (2026-04-13 리팩토링)

  • 이전: .env.local + BUILD_TIME_SUPABASE_* + saasMode 분기 → Mac/Windows UI 불일치
  • 현재: packages/core/src/supabase-config.ts에 URL/Anon Key 하드코딩 (SSOT)
    • Anon key는 클라이언트용 공개 키(RLS 보호)이므로 소스 포함 안전
    • saasMode 분기 전체 제거, 항상 OAuth(Google/GitHub) only
    • env.example 삭제, loadSupabaseEnv() 삭제, isSupabaseBuildTimeConfigured() 삭제
    • CLOUD_SYNC.CONFIGURE IPC 채널 삭제
    • CloudSyncSection: 개발자 모드 UI(URL/Key 입력) 완전 제거

SaaS [2] OAuth 로그인 전략

  • 로그인 없으면 로컬 Ollama로 동작 (무료)
  • 로그인은 Google/GitHub OAuth only (Supabase Auth)
  • LoginScreen.tsx 강제 게이트 → 제거
  • App.tsx AuthGate 상태머신 → 제거 (항상 메인 UI 렌더)

철회 이유 (2026-04-11 빅뱅 Phase 1.5): 사용자 피드백 "무료 로컬은 사용자 엔트리 포인트가 될 거구, 수익화는 클라우드 SaaS 서비스임". 로컬 STT/LLM이 이 앱의 킬러 피처 → 회원가입 없이 즉시 체험 가능해야 함 (onboarding funnel 입구). OAuth provider도 아직 Supabase에 설정 안 된 상태. 강제 게이트는 철학 + 인프라 양쪽 모두 위배.

SaaS [3] 사용자별 SQLite 격리 — ✅ 완료 (2026-04-11, 빅뱅 Phase 1)

  • apps/desktop/src/main/db/index.ts 전면 재설계
    • 구 initDatabase() / closeDatabase() 폐기 (이름 차원에서 제거)
    • 새 API: openForUser(userId), closeCurrent(), getDatabase(), getCurrentUserId(), isDatabaseOpen(), getCurrentDbPath()
    • 경로: userData/users/${userId}/d3ro.db (userId path-separator/.. 가드)
    • applySchema(s)로 테이블 + 인라인 ALTER 마이그레이션 추출 — 새 DB 생성 시마다 재실행
    • 모듈 상태: currentUserId | null, sqlite, db — closeCurrent 후 모두 null
    • 같은 userId 재호출 시 no-op, 다른 userId면 먼저 close 후 재오픈
    • openForUser 실패 시 부분 상태 정리 + D3ROError(DBOpenFailed) throw
  • legacy DB 정책
    • V1 userData/d3ro-voice.db는 한 번도 정식 서비스 안 됨 → archiveLegacyDbIfExists()로 첫 openForUser 직전 silent rename (d3ro-voice.legacy-${ISO timestamp}.db)
    • 파일 삭제 없음, dialog 없음, 데이터 이전 없음 ("클린하게" 요청)
  • CloudSyncService 인증 lifecycle 단일 진입점 (SSOT)
    • _onAuthenticated(session, { reason: 'restore' | 'signin' }):
      1. this._session = session
      2. openForUser(userId) — 실패 시 세션 무효화 + auth-changed(null) + sync-error emit 후 return
      3. auth-changed(user) emit (DB가 열린 뒤에)
      4. startRealtime() 자동 시작
    • init() 세션 복원, handleAuthCallback(code) 신규 로그인 모두 이 함수로 수렴
    • _onSignOut():
      1. VoiceMode active → cancelSession() (녹음 진행 중이면 강제 취소)
      2. MeetingMode active → stopRecording() (await)
      3. Caption → stop() (await)
      4. stopRealtime()
      5. Supabase auth.signOut()
      6. _session = null + _clearStoredRefreshToken()
      7. auth-changed(null) emit — 렌더러 LoginScreen 복귀
      8. closeCurrent() — 이 시점 이후 getDatabase()는 throw
    • signOut() public 메서드는 _onSignOut() 위임
    • import 동적 변경: VoiceMode/MeetingMode/Caption 모두 dynamic import로 순환 의존 회피
  • bootstrap.ts 순서 변경
    • database step 완전 제거 (critical 2번째였던 자리 삭제)
    • cloud-sync step은 마지막 유지 — 세션 복원 성공 시 DB가 여기서 열림
    • initDatabase import 삭제
  • 검증
    • desktop tsc --noEmit ✅
    • desktop npm run build ✅ (main bundle 997kB, vite 경고는 기존 동적 import 관련 pre-existing)
    • runtime (dev 띄워서 LoginScreen → OAuth → user DB 생성 확인) — 사용자 수동 검증 대기
  • 남은 작업 (Phase 1 follow-up)
    • 사용자 A → signOut → 사용자 B 로그인 시 A 데이터 안 보이는지 실측
    • ~/Library/Application Support/d3ro-voice/users/${uuid}/d3ro.db 파일 생성 확인
    • 기존 d3ro-voice.db가 있는 환경에서 archive rename 동작 확인

SaaS [13] Voice Conversation 파이프라인 복구 + UX 개선 설계 (Phase 5 Part 5, 2026-04-11)

Voice Conversation이 Mac에서 한 번도 end-to-end 동작한 적 없었음을 발견. 세 개의 독립된 버그가 합쳐져 있었고 진단 로그를 추가해 하나씩 분해해 해결. UX 개선은 사용자와 인터뷰로 설계서만 확정하고 구현은 다음 세션으로 이월.

Bug 10.5 — TTS spawn powershell ENOENT (Mac)

  • 원인: TTSPlaybackService.ts:105가 spawn('powershell', ...) 하드코딩. 파일 주석부터 "Windows SAPI (PowerShell) 기반" — V2-5 플랫폼 감사 때 이 파일 누락.
  • Fix: _speakOne를 dispatcher로 바꾸고 _speakOneMac(macOS say -r <wpm>) / _speakOneWindows(기존 PowerShell) 분리. 공통 close/error 핸들러는 _bindProcessHandlers로 추출. _getMacRate(WPM 기준 180×speed) / _getWindowsRate(-10~10 SAPI) 분리.
  • 파급: 음성 대화뿐 아니라 TTS를 쓰는 모든 경로가 Mac에서 벙어리였음. 이제 /usr/bin/say로 한/영/자동 판별 voice.

Bug 11 — finishListening 오디오 버퍼 스냅샷 순서 (진짜 root cause, OS 무관)

  • 증상: 녹음 후 "생각 중" 진입은커녕 조용히 listening으로 복귀, STT 한 번도 안 탐. 진단 로그로 추적 시 finishListening called: bufferCount=311, totalBytes=597120 직후 1ms 뒤 audio too short: 0 bytes로 찍힘.
  • 원인: _stopListening() 내부에 this._audioBuffers = [] 리셋이 있는데 finishListening이 concat 이후에 stop을 부르는 게 아니라 이전에 불러서, Buffer.concat 시점엔 이미 빈 배열. 19초짜리 오디오가 매번 날아감. Windows에서도 동일 버그였을 가능성 높음.
  • Fix (VoiceConversationService.ts:193): _stopListening() 전에 const audioBuffer = Buffer.concat(this._audioBuffers) 스냅샷 저장. this._audioBuffers = [] 재설정도 제거(이미 stop에서 하므로 중복).

Bug 12 — Whisper 모델 사전 로드 누락

  • 증상: Bug 11 fix 후 finishListening이 STT로 진입은 하지만 "모델 로딩 중, 오디오 버퍼에 적재"에서 영원히 pending. state가 'thinking'에 고정.
  • 원인: LocalSTTService.initialize()가 모델을 로드하는데, MeetingMode/CaptionService는 startRecording 시 이걸 명시적으로 호출하지만 VoiceConversationService는 호출 안 함. 첫 transcribe가 내부 pending 큐에 쌓이지만 아무도 로드를 트리거하지 않아 deadlock.
  • Fix (VoiceConversationService.ts:84): startSession에서 void getLocalSTTService().initialize().catch(...) fire-and-forget. initialize는 이미 Ready 같은 모델이면 즉시 return하므로 idempotent.

검증

  • desktop tsc --noEmit EXIT=0
  • Mac /usr/bin/say -r 180 직접 호출 시 스피커로 소리 확인 (사용자 증언 "오 들린다!")
  • 사용자 재시연에서 Mic 클릭 → 말함 → ⏹ Stop → 전사 → LLM 스트리밍 → TTS 소리 → 자동 listening 복귀 연속 대화 성공

Bug 13 (신규 발견, 미수정) — 빈 STT 결과 시 사용자 피드백 부재

  • 증상: 재시연 중 사용자가 ⏹ Stop 버튼 눌러도 "반응 없음"으로 느낌. 로그 추적 결과 finishListening → transcribe → faster_whisper VAD filter removed 00:22.740 of audio → 전사 '' 빈 텍스트 → 조용히 listening 복귀 (에러 없음). 사용자 관점에선 버튼 먹통.
  • 원인 조합:
    1. Mac 시스템 마이크 입력 볼륨 너무 낮거나 coreaudio input clipped 왜곡으로 VAD가 전체 오디오를 무음 판정
    2. VoiceConversationService.finishListening에서 빈 STT 결과를 조용히 삼키고 listening으로 복귀 — 사용자 피드백 없음
  • Fix 방향 (다음 세션): _emitError('stt', 'No speech detected...') 토스트 + 몰입 패널 내 "음성이 감지되지 않았습니다. 마이크 볼륨을 확인하세요." 힌트. 근본적으로는 UX 개선(몰입 패널)과 함께 처리.
  • 워크어라운드: Mac 시스템 설정 → 사운드 → 입력 볼륨 조정 (중간 이상).

UX 개선 설계 (구현 미완, 다음 세션 이월) 사용자 피드백: 녹음 상태 불명확 / 텍스트 입력 방치 / 종료·응답 완료 피드백 부재. 인터뷰로 확정한 결정:

  1. 녹음 모드 = 풀 몰입 계측기 패널 — 메시지 영역을 REC LED + 9바 phosphor waveform + 0:04 타이머 + "SPEAK NOW · PRESS ⏹ TO SEND" 힌트로 완전 대체 (listening 상태에서만)
  2. thinking/speaking 상태 = 메시지 리스트로 복귀 — 대화 맥락 유지. typing indicator 버블 추가. TextField는 disabled "응답 대기 중…"
  3. 연속 대화 — 응답 후 자동 listening 복귀 (기존 코드 동작 유지)
  4. 사운드 4개 전부 — recording-start/recording-stop/chime(recording-stop 재사용)/error
  5. Audio level IPC 신규 채널 — VOICE_CONVERSATION.AUDIO_LEVEL로 AudioCaptureService 'audio-level' 이벤트 listening 상태에서만 forward
  6. SoundEffectService SoundName에 'chime' 추가 — 파일은 recording-stop.wav 재사용 (cancel=error 패턴과 동일). 나중에 전용 WAV로 교체 예정

설계 ASCII 와이어프레임 + 작업 리스트는 이번 세션 대화 말미에 존재. 다음 세션 우선 구현 대상.

남은 미해결 이슈

  • U1 — Realtime TIMED_OUT (블로커 아님)
  • U3 — Refresh token 간헐적 소진 (블로커 아님)
  • U4 — package-lock.json optional dep 정리
  • Phase 3.2 — PREMIUM_LLM quota 게이트 (Anthropic/OpenAI key 주입 대기, 빅뱅 성공 기준 8번 중 마지막 1개)
  • Voice Conversation UX — 위 설계 대로 다음 세션 구현
  • AudioCaptureService.getDevices() — Mac에서 PowerShell 호출 실패 경고 로그 (1개 device fallback 동작, 블로커 아님)

SaaS [12] /simplify 패스 — IME 가드 helper 추출 + 8곳 일괄 적용 (Phase 5 Part 5, 2026-04-11)

Bug 10의 한글 IME Enter 중복 가드를 isImeComposingEvent helper로 추출. MeetingModePage 기존 인라인 가드 교체 + 7개 사이트 신규 적용 → 총 8곳 통합. Phase 3.3 훅 8곳은 품질 리뷰 결과 이미 최적 상태로 판정(수정 없음).

신규 helper — apps/desktop/src/renderer/utils/keyboard.ts

export function isImeComposingEvent(e: KeyboardEvent): boolean {
  return e.nativeEvent.isComposing || e.keyCode === 229
}

JSDoc에 Bug 10 원리(Chromium이 IME 조합 중 Enter를 2번 발화) + 권장 사용 패턴 포함.

적용 8곳 (isImeComposingEvent(e) return 1줄 삽입)

파일 라인 용도 변경 전
pages/MeetingModePage.tsx 164 회의 메모 입력 인라인 가드 4줄 (Bug 10 fix)
pages/KnowledgeBasePage.tsx 84 RAG 쿼리 가드 없음
pages/VoiceConversationPage.tsx 113 텍스트 채팅 가드 없음
components/meeting/MeetingChatPanel.tsx 111 미팅 챗 가드 없음
components/meeting/EditableSegment.tsx 70 전사 편집 (Enter+Esc) 가드 없음
components/meeting/MeetingDetailTabs.tsx 242 미팅 타이틀 (인라인 arrow) 인라인 onKeyDown={(e)=>e.key==='Enter'&&...} 1줄 → 블록 확장
components/shared/HistoryEntryCard.tsx 64 태그 추가 (Enter+Esc) 가드 없음
pages/CommandsPage.tsx 336 키워드 추가 (Enter+Esc) 가드 없음

Phase 3.3 훅 품질 리뷰 결론 — 수정 없음

  • HistoryService.create/updateTitle, DictionaryService.add/update, MeetingModeService.startRecording/addMemo/_runPostProcessing/generateDocument, MeetingSummaryService.generateAndSave 8곳 전부 void getCloudSyncService().pushOne(table, id) + // Phase 3.3: fire-and-forget 주석 일관.
  • CloudSyncService.pushOne 내부가 try-catch로 warn만 찍고 삼킴 → 호출자는 무조건 안전, 로컬 write 차단 금지 철학 준수.
  • helper 추출 후보 없음(이미 1줄). 일관성 OK, DRY OK, 에러 흡수 OK → 현 상태가 최적, 리팩터 불필요.

검증

  • desktop tsc --noEmit ✅ EXIT=0
  • Vite HMR 자동 반영(기존 dev 프로세스 살아있음) — 별도 재기동 없이 적용
  • 대표 2곳 한글 Enter 실측 (VoiceConversationPage 채팅 / MeetingDetailTabs 타이틀) — 사용자 수동 검증 대기

U5 해소, 남은 미해결 이슈

  • U1 — Realtime TIMED_OUT (블로커 아님)
  • U3 — Refresh token 간헐적 소진 (블로커 아님)
  • U4 — package-lock.json optional dep 정리
  • Phase 3.2 — PREMIUM_LLM quota 게이트 (Anthropic/OpenAI key 주입 대기)

SaaS [11] Meeting pre-push 직접 실증 + IME Enter 중복 addMemo 픽스 (Phase 5 Part 4, 2026-04-11)

U2 해소. Meeting 모드 녹음을 직접 돌려 Fix 1(startRecording pre-push) + addMemo hook을 실시간 로그로 증명. 부수적으로 발견된 한글 IME Enter 중복 addMemo 버그(Bug 10)를 isComposing 가드로 픽스.

직접 실증 타임라인 (session 78421e75, 21:51:24~21:51:50)

21:51:24.937  pushOne meetings/78421e75 ok          ← Fix 1 pre-push (813ms after startRecording)
21:51:30.499  pushOne meeting_memos/4ed7e59a ok     ← addMemo #1 "메모가"
21:51:32.202  pushOne meeting_memos/de011c15 ok     ← addMemo #2 "두번씩"
21:51:35.893  pushOne meeting_memos/3184fe5e ok     ← addMemo #3 "입력되는걸 막아야합니다."
21:51:38.263  pushOne meeting_memos/00f041ef ok     ← addMemo #4 "잘되는지 봅시다."
21:51:40.740  pushOne meeting_memos/0808c4d2 ok     ← addMemo #5 "잘되는 것 같네요."
21:51:50.681  pushOne meetings/78421e75 ok          ← _runPostProcessing hook
21:51:50.709  pushOne history/70388090 ok           ← CaptionService → HistoryService.create
  • meetings/78421e75가 녹음 시작 + 종료 두 번 push → Fix 1 완전 증명 (부모 row가 메모 push 시점에 이미 Supabase에 존재해서 자식 RLS/FK 통과)
  • 메모 5건 전부 100ms 내 1:1 push → addMemo hook 정상, RLS 에러 0
  • history push 출처 확인: CaptionService.ts:459에서 회의 전사가 완료되면 전체 텍스트를 HistoryService에도 저장하는 기존 경로. Phase 3.3 훅이 HistoryService.create() 끝에 걸려 있어 자동으로 따라옴. 의도대로 동작.

발견+픽스한 버그 — Bug 10: 한글 IME Enter 중복 addMemo

  • 증상: 첫 번째 시연 녹음(bd7f2367)에서 "메모 기능이 잘 작동하는지 봅니다"가 2ms 간격으로 2번 저장됨 (UUID c7f0db45 + a6e442cc, 로그 21:47:57.410/.412). 다른 메모들은 1회.
  • 원인: 한글 IME 조합 중 Enter 키는 Chromium이 keydown을 두 번 발화 — (1) IME 조합 확정용(isComposing=true, keyCode=229) + (2) 실제 Enter 액션. 기존 handleMemoKeyDown(MeetingModePage.tsx:164-174)은 e.key === 'Enter' && !e.shiftKey만 검사하고 isComposing을 무시해 두 이벤트 모두에서 handleAddMemo() 발화 → IPC 2회 → MeetingModeService.addMemo() 2회 → DB insert 2건 → pushOne meeting_memos 2회. 중복이 다른 메모에서 안 난 이유는 해당 메모들은 IME 조합이 이미 확정된 상태(공백/한자 변환 등)에서 Enter를 쳤기 때문.
  • Fix: MeetingModePage.tsx:168에 이중 가드 추가. if (e.nativeEvent.isComposing || e.keyCode === 229) return — isComposing은 모던 표준, keyCode === 229는 Chromium 엣지 케이스(조합 확정 시점에 isComposing이 false로 내려가면서 keyCode만 229로 주는 경우) 호환.
  • 검증: 두 번째 녹음(78421e75)에서 한글 메모 5건 연속 입력 → 전원 1:1 매핑, 중복 0. Vite HMR로 즉시 반영, 별도 재기동 없이 fix 유효.

잠재 IME 버그 — 다른 Enter 핸들러 7곳 (다음 /simplify 때 일괄 처리 예정)

파일 라인 용도 한글 위험도
KnowledgeBasePage.tsx 85 RAG 쿼리 높음
VoiceConversationPage.tsx 114 텍스트 채팅 높음
components/meeting/MeetingChatPanel.tsx 113 미팅 챗 높음
components/meeting/EditableSegment.tsx 72 전사 세그먼트 편집 높음
components/meeting/MeetingDetailTabs.tsx 242 미팅 타이틀 중간
components/shared/HistoryEntryCard.tsx 65 태그 추가 중간
CommandsPage.tsx 337 키워드 추가 중간

(LicenseModal.tsx:196은 ASCII 전용이라 제외)

검증

  • desktop tsc --noEmit ✅ (EXIT=0)
  • Meeting 모드 녹음 1회차(bd7f2367): Fix 1 + addMemo hook 증명, IME 중복 1건 발견
  • Meeting 모드 녹음 2회차(78421e75): IME fix 적용 후 메모 5건 전부 1:1 ✅
  • Realtime 채널 상태: 여전히 U1 TIMED_OUT (블로커 아님, Phase 3.3 auto push가 대체 경로)

빅뱅 성공 기준 상태 — 이번 세션 갱신 없음 (8개 중 7개 완료, Phase 3.2 PREMIUM_LLM만 잔존)

다음 세션 권장 액션

  • /simplify 실행 — Phase 3.3 + Bug 10 fix 코드 품질 리뷰 + 위 7개 IME Enter 핸들러 일괄 가드 추가
  • Phase 3.2 PREMIUM_LLM Edge Function 경로 (Anthropic/OpenAI key 주입 필요, 사용자 의사결정 대기)
  • U1 Realtime TIMED_OUT 원인 조사 (선택)
  • package-lock.json optional dep 정리 (Phase 5 Part 2부터 이월)

수동 Push 버튼이 사라졌다. 로컬 SQLite write가 일어나는 모든 경로(8곳)에서 CloudSyncService.pushOne이 fire-and-forget으로 즉시 실행되어 Supabase에 반영. 웹 Dashboard 새로고침 한 번이면 방금 녹음한 미팅/메모/전사까지 전부 보인다. 이번 세션 한 번에 end-to-end 실증까지 통과.

실증 시나리오

  1. 데스크톱 dev 재기동 → Restored session for user: yunchan8804@gmail.com (cloud-sync.token 자동 복원) → 사용자 DB 재오픈
  2. 신규 Meeting 모드 녹음 1건 (약 33초, 중간에 메모 2건 추가)
  3. 로그에 즉시 나타난 push 이벤트:
    • pushOne history/4c860fe2-... ok
    • pushOne meetings/6bb51eca-... ok
  4. 재기동 시 pushAll 캐치업으로 직전 실패한 메모 2건이 자동 재시도되어 성공 (Sync complete: pushed=4 errors=0)
  5. 웹 http://localhost:3000/meetings/6bb51eca-... 새로고침 → 전사 + 메모 둘 다 렌더 확인 (사용자 "잘 전사와 메모가 올라왔어")

구현 — Phase 3.3 Auto push on write

  1. CloudSyncService.pushOne(table, id) 신규 — 제네릭 단건 push. 로그인 상태 아니면 silent noop(로컬 write 절대 차단 금지), 실패 시 logger.warn만(throw 금지). pushAll과 공통 매퍼 공유 (_mapHistoryPayload / _mapDictionaryPayload / _mapMeetingPayload / _mapMemoPayload / _mapDocumentPayload 5개 private 메서드 추출, DRY).
  2. Hook 삽입 위치 8곳 (전부 void getCloudSyncService().pushOne(...), fire-and-forget):
    • HistoryService.create() 끝 → history
    • HistoryService.generateTitle() DB update 직후 → history (LLM 자동 타이틀)
    • MeetingSummaryService.generateSummary() summaryText 저장 후 → history ("updatePolishedText" 상응)
    • DictionaryService.add() 끝 → dictionary
    • DictionaryService.update() 끝 → dictionary
    • MeetingModeService.startRecording() 끝 → meetings (부모 pre-push, Fix 1 참조)
    • MeetingModeService.addMemo() 끝 → meeting_memos
    • MeetingModeService._runPostProcessing() 완료 상태 저장 직후 → meetings (raw_transcript 포함)
    • MeetingModeService.generateDocument() DB insert 직후 → meeting_documents

발견+픽스한 구조 버그 3건 (이번 세션)

Bug 7: meeting_memos RLS violation — 부모 meeting row 미존재

  • 증상: 녹음 중 메모 추가 → pushOne meeting_memos/4a733d76-... failed: new row violates row-level security policy for table "meeting_memos" (21:19:14에 2건 실패)
  • 원인: RLS 정책이 meeting_id에 해당하는 부모 meetings row의 존재/소유권을 요구하는 것으로 추정 (정확한 정책 확인은 안 함). 기존 구현에서는 _runPostProcessing()에서 "completed" 상태 저장 시점에만 meeting이 push되므로, 녹음 중 addMemo → pushOne 시점에는 부모 row가 Supabase에 아직 없음 → 자식 insert RLS 거부.
  • Fix 1: MeetingModeService.startRecording() 끝에 void getCloudSyncService().pushOne('meetings', sessionId) 추가 — 녹음 시작 즉시 부모 row(status='recording', raw_transcript=null)를 Supabase에 upsert. 이후 addMemo/generateDocument의 자식 push가 FK/RLS를 통과.
  • 검증: 재기동 직후 pushAll 캐치업이 이전 세션의 실패 메모 2건을 재시도 → Sync complete: pushed=4 errors=0 (meeting 1 + memos 2 + history 1 추정) → 웹에서 메모 렌더 확인.
  • 부수 효과(의도적): fire-and-forget 실패는 pushAll 캐치업으로 자동 복구. 실패가 영구 손실로 이어지지 않음.

Bug 8: 웹 meeting detail 페이지가 transcripts 테이블만 읽음 (meetings.raw_transcript 무시)

  • 증상: 녹음 종료 후 웹 /meetings/[id] 상세 페이지의 TRANSCRIPT 섹션이 빈 상태로 표시.
  • 원인: apps/web/src/app/(app)/meetings/[id]/page.tsx가 supabase.from('transcripts').select(...).eq('meeting_id', id)만 읽고 LiveTranscriptList에 넘김. 데스크톱은 CaptionService 세그먼트를 인메모리에 버퍼링했다가 meeting_sessions.rawTranscript 단일 텍스트 필드에만 저장 — transcripts 테이블에는 아무것도 쓰지 않음. Realtime 스트림 설계(웹 실시간 캡션 뷰어용)와 데스크톱 오프라인 전사 플로우의 접점이 없었음.
  • Fix 2 (웹 fallback): transcripts가 비어있고 미팅 상태가 recording이 아닐 때 meeting.edited_transcript ?? meeting.raw_transcript를 whiteSpace: 'pre-wrap'의 Box로 렌더. 둘 다 없으면 "전사가 없습니다." placeholder. recording 상태거나 transcripts 행이 있으면 기존 LiveTranscriptList 유지(향후 실시간 캡션 뷰어 경로 보존).
  • 왜 데스크톱을 고치지 않았나: MVP 스코프 고려. 세그먼트 per-row push는 쓰기 빈도가 폭증하고 RLS 정책을 별도로 다듬어야 함. Phase 3.3의 목표는 "완료된 미팅이 웹에서 읽혀야 한다"이고, 그건 meetings.raw_transcript 경로 하나로 충분. 실시간 캡션 동기화가 정식 기능으로 들어올 때 desktop → transcripts 쓰기를 추가.

Bug 9: Dashboard RECENT MEETINGS 카드 클릭 무반응 (U2)

  • 증상: 지난 세션 말미 사용자 피드백 "웹에서 미팅 아이템이 들어가지질 않는다" 재확인 → Dashboard 카드 클릭 시 /meetings/[id] 이동 안 됨.
  • 원인: apps/web/src/app/(app)/dashboard/page.tsx:156의 MetalCard가 <Link>로 감싸져 있지 않아 클릭 인터랙션 자체가 없었음.
  • Fix 3: next/link의 Link로 MetalCard를 감싸고 href={\/meetings/${meeting.id}`}, cursor: 'pointer'+&:hover { transform: translateY(-1px) }hover 효과 추가,textDecoration: 'none'`으로 링크 기본 스타일 억제.
  • 검증: 새 기동 후 실증 녹음 전에 이미 웹에서 GET /meetings/cd4df4f1-... 200 확인됨.

검증

  • desktop tsc --noEmit ✅
  • web tsc --noEmit ✅
  • dev 런타임 재기동 ✅ (Restored session → 직전 Sync complete: pushed=4 errors=0 캐치업 → Main window)
  • 녹음 1건 fire-and-forget push ✅ (pushOne history/... ok + pushOne meetings/... ok, 21:19:38)
  • 웹 Dashboard 새 미팅 렌더 ✅ (카드 + 상세 페이지 + 전사 + 메모 전부)
  • 사용자 확인: "잘 전사와 메모가 올라왔어"
  • Realtime 채널 TIMED_OUT ❌ (알려진 U1, 블로커 아님 — Phase 3.3 auto push가 대체 일관성 경로)

다음 세션 권장 액션

  • Fix 1의 재현 실증 (녹음 → 중간 메모 추가 시점마다 pushOne meeting_memos/... ok 실시간 발화 — 이번 세션은 pushAll 캐치업으로 통과해서 pre-push 효과를 직접 본 건 아님)
  • U1 Realtime TIMED_OUT 원인 조사 (선택)
  • Phase 3.2 PREMIUM_LLM Edge Function 경로 (Anthropic/OpenAI key 주입 필요)
  • 24 → 25+ commits → git push 컨펌 (사용자 결정 시점)
  • package-lock.json 정리 (rollup-win32-x64-msvc optional dep 이슈)

SaaS [9] 로컬 ID UUID 통일 + Realtime publication 픽스 + 실증 A~E 전원 통과 (Phase 5 Part 2, 2026-04-11)

3 클라이언트 데이터 일원화 end-to-end 실증 완료. Phase 5 Part 1에서 못 잡은 3개 구조 버그를 한 세션에 픽스하고 실증 A/B/C/D/E 전원 통과. 로컬→클라우드 push가 처음으로 성공 (pushed=1, pushed=5), 데스크톱 녹음 데이터가 웹 Dashboard에 그대로 반영되는 것을 스크린샷 캡처까지 확인.

실증 결과 (18:36~19:11)

# 실증 결과 증거
A 세션 자동 복원 + RLS 픽스 검증 ✅ Restored session for user: yunchan8804@gmail.com + Pull complete applied=0 errors=0
B 사용자별 DB + push 경로 (history 단건) ✅ HistoryService created 56a767ac-0fbc-45c8-98f2-2e2d4afa70f9 + Sync complete: pushed=1 errors=0
C 로그아웃 → 로컬 복귀 ✅ Realtime 종료 → Database closed (7da3dd02-...) → Reverted to local DB: users/_local/d3ro.db → continuing in local mode
D 재로그인 데이터 복원 ✅ 19:09:00 재로그인 → 같은 uuid DB가 created=false로 재오픈 (파일 보존 확인)
E 웹 크로스 디바이스 (Meeting 경로) ✅ Meeting Mode 녹음 → Sync complete: pushed=5 errors=0 → 웹 localhost:3000/dashboard 새로고침 → RECENT MEETINGS에 "(제목 없음) 2026. 4. 11. 오후 7:09:21" 표시, "총 회의 1 / 이번 주 1 / ACTIVITY TREND 04/11 스파이크" 전부 일치

실증 E 세부 플로우 (end-to-end 전체 데이터 경로 검증)

데스크톱 Meeting Mode (19:09:53 History created: 093d9d5d-...)
  → MeetingModeService가 meeting_sessions / meeting_memos / meeting_documents 로컬 write
  → users/7da3dd02-.../d3ro.db (SQLite, UUID PK)
  → 수동 Push 버튼 (19:11:21)
  → CloudSyncService.pushAll (history, dictionary, meetings, meeting_memos, meeting_documents 5개 upsert)
  → "Sync complete: pushed=5 errors=0"
  → Supabase `meetings` 테이블 upsert (RLS user_id=auth.uid() 통과)
  → 웹 Next.js dashboard getSupabaseServerClient().from('meetings').select(...)
  → 브라우저 렌더: "(제목 없음) 2026. 4. 11. 오후 7:09:21"

발견+픽스한 구조 버그 3건

Bug 4: 로컬 nanoid PK vs Supabase UUID PK 불일치 (실증 B 첫 시도에서 발견)

  • 증상: Push history failed: invalid input syntax for type uuid: "fvy6bIzrEtZ0vo96Dd-WZ" → Sync complete: pushed=0 errors=1
  • 원인: apps/desktop/src/main/db/schema.ts의 모든 PK는 text('id').primaryKey() + 서비스 코드는 nanoid() 생성 (예: fvy6bIzr..., 21자). 반면 Supabase initial_schema.sql은 id uuid PRIMARY KEY DEFAULT gen_random_uuid(). Phase V2-4 설계 단계에서 "V2-4 MVP는 uuid로 새로 발급, 매핑 테이블 필요"라고 적혀 있었으나 실제 구현에선 매핑이 없어 push 경로 전체가 깨져 있었음. 결과적으로 빅뱅 사이클 내내 push가 한 번도 성공한 적 없음 (지난 세션 pushed=0은 로컬 데이터 0건이라 에러가 안 난 거지 버그가 없어서가 아님).
  • 사용자 결정: 로컬을 UUID로 통일 (방향 B). 이유 — 3 클라이언트(desktop/web/mobile) id 스펙 일관 + PostgreSQL UUID PK 관용 유지 + crypto.randomUUID() 표준 API + nanoid 외부 의존성 자체 제거. "땜질 아닌 근본 해결" 사용자 요청.
  • 픽스:
    • 14개 서비스 파일에서 import { nanoid } from 'nanoid' 제거 + nanoid() → crypto.randomUUID() 일괄 교체 (총 20곳): HistoryService / MeetingModeService / MemoService / DictionaryService / CaptionService / VoiceConversationService / VoiceCommandService / VoiceActionService / FileTranscriptionService / CustomInstructionService / MeetingDocTemplateService / DictationTemplateService / RAGService / ChainService
    • apps/desktop/package.json dependencies에서 nanoid: ^5.1.7 제거
    • apps/desktop/electron.vite.config.ts externalizeDepsPlugin exclude에서 'nanoid' 제거
    • 기존 로컬 DB 2개(users/_local, users/7da3dd02-...) drop — dev 초기 nanoid row만 있었고 의미 있는 데이터 0건이라 cascade FK 마이그레이션 대신 재생성 선택
    • drizzle schema(text('id').primaryKey())는 건드릴 필요 없음 — SQLite text PK는 UUID 36자 문자열을 그대로 저장
  • 검증: 재기동 후 신규 녹음 → HistoryService created 56a767ac-0fbc-45c8-98f2-2e2d4afa70f9 (UUID v4) → Push → pushed=1 errors=0 최초 성공

Bug 5: Supabase supabase_realtime publication 누락

  • 증상: 로그인 직후 Realtime 채널 상태: TIMED_OUT (실증 A에서 재현, 재기동 후에도 재현)
  • 원인: 20260409000001_initial_schema.sql:134이 ALTER PUBLICATION supabase_realtime ADD TABLE public.transcripts 한 줄만 실행. 데스크톱 CloudSyncService.startRealtime()이 실제 구독하는 meetings / history / dictionary는 publication에 포함되지 않아 postgres_changes 스트림이 물리적으로 흐를 수 없었음.
  • 픽스: 20260411000002_realtime_publication.sql 신규 — DO $$ ... LOOP ... ALTER PUBLICATION supabase_realtime ADD TABLE public.%I ... END $$; 패턴으로 meetings / meeting_memos / meeting_documents / history / dictionary 5개 테이블을 pg_publication_tables 카탈로그 체크 후 조건부 추가. supabase db push 적용 완료.

Bug 6: persistSession:false 에서 realtime.setAuth 자동 전파 안 됨 (가설 — 부분 검증)

  • 증상: Bug 5 migration 적용 후에도 TIMED_OUT 재발.
  • 원인 (가설): createClient(url, anonKey, { auth: { persistSession: false, ... } }) 구성에서 setSession() 호출이 onAuthStateChange를 통한 realtime.setAuth() 자동 전파 경로를 깨뜨리는 것으로 추정. 즉 realtime WebSocket이 anon key JWT로 join을 시도하고 RLS 필터(user_id=eq.${userId})를 통과 못해 TIMED_OUT.
  • 픽스: CloudSyncService.startRealtime()에 this._client.realtime.setAuth(this._session.access_token)을 채널 구성 이전에 명시 호출 (try/catch로 안전하게). 에러 시 warn만.
  • 현재 상태: ⚠️ 여전히 TIMED_OUT 재현. Bug 5 migration은 확실히 적용됐고 setAuth 명시 호출도 들어갔으나 해결 안 됨. 후속 조사 필요 (task 8로 분리). 가능 가설 — (a) Supabase 프로젝트 Realtime 서비스 자체 disabled, (b) setAuth 타이밍 문제(WebSocket 이미 connect된 상태에서 setAuth 무효), (c) RLS 정책이 user_id = auth.uid() 외 다른 조건을 요구, (d) realtime 전용 RLS policy(realtime.messages 구독 권한)가 별도로 필요.
  • 블로커 여부: 아님. 주기적 pull + Phase 3.3 Auto push 경로로 최종 일관성 유지. Realtime은 "웹→데스크톱 5초 이내 반영" 편의 기능.

픽스로 해결된 부수 이슈

  • cloud-sync.token 기반 자동 세션 복원 경로 ✅ (지난 세션 Invalid Refresh Token: Already Used는 당시 토큰이 소진된 일회성 이슈, 새 로그인 후 재시작하니 Restored session for user: yunchan8804@gmail.com 성공)
  • users/${uuid}/d3ro.db 파일이 로그아웃 시 close만 되고 보존되어 재로그인 시 created=false로 복원되는 경로 ✅
  • LicenseService 로컬 모드 리셋 경로 ✅ (로그아웃 후 익명 로컬 모드 정상 동작)

검증

  • desktop tsc --noEmit ✅
  • dev 런타임 재기동 ✅ (Main window shown → Restored session → DB opened for 7da3dd02 → Initial sync pushed=0 pulled=0)
  • 핫키 녹음 → UUID row 생성 → Push → pushed=1 errors=0 ✅ (최초 성공)
  • 로그아웃 → Realtime 종료 → _local DB 복귀 ✅

이 섹션에서 의도적으로 뺀 것

  • package-lock.json 정리: npm install이 기존부터 rollup-win32-x64-msvc optional dep로 실패. 당장 nanoid import는 이미 제거되어 런타임 영향 없음. lockfile은 후속 세션에서 --omit=optional로 정리.
  • Realtime 근본 원인 — task 8 별건으로 분리
  • 실증 E(웹 크로스 디바이스) — Phase 3.3 이후 수행

SaaS [8] OAuth 로그인 완주 + RLS 재귀 픽스 (Phase 5 Part 1, 2026-04-11)

로그인 성공! 빅뱅 Phase 5 실증 시작. Google OAuth 토큰 교환 성공, 사용자별 DB 생성 확인. 그러나 첫 pull에서 Supabase RLS 무한 재귀 + Realtime TIMED_OUT 발생 → 같은 세션에서 즉시 픽스.

성공 이벤트 로그 (11:46:52 ~ 55)

[Opening OAuth URL in external browser: google]
[deep-link] Implicit flow tokens detected, calling handleAuthTokens()
[CloudSyncService] Signed in (implicit): yunchan8804@gmail.com
[auth:signin] DB opened for 7da3dd02-9f2f-4ee9-a9b3-1c2c24875a93:
  .../users/7da3dd02-.../d3ro.db (created=true)
[auth:signin] Initial sync starting — push then pull
[deep-link] handleAuthTokens completed successfully
[auth:signin] Initial push done: pushed=0, errors=0

발견된 버그 + 픽스

Bug 1: Supabase implicit flow 응답을 데스크톱이 무시

  • 증상: 로그인 4회 시도, 매번 auth-callback deep link had no 'code' parameter
  • 원인: supabase-js가 persistSession: false일 때 PKCE code_verifier 저장소가 없어 implicit flow로 fallback. Supabase가 fragment(#access_token=...&refresh_token=...)로 토큰 직접 전달. 우리 handleDeepLink는 ?code=...(PKCE)만 파싱.
  • 픽스:
    • apps/desktop/src/main/index.ts: handleDeepLink가 query(?code=)와 fragment(#access_token=) 둘 다 파싱. fragment에 토큰 있으면 CloudSyncService.handleAuthTokens() 호출. 에러 파라미터(error=, error_description=) 상세 로깅.
    • CloudSyncService.handleAuthTokens({accessToken, refreshToken}) 신규 — client.auth.setSession()으로 세션 복원 후 기존 _onAuthenticated(session, {reason: 'signin'}) SSOT 진입점으로 수렴.
    • 토큰 로깅은 host+path만, 토큰 값은 로그에 안 찍히도록 수정 (query=yes/no, fragment=yes/no)

Bug 2: Supabase RLS 무한 재귀

  • 증상: Pull meetings failed: infinite recursion detected in policy for relation "team_members" × 3 테이블 + Realtime 채널 상태: TIMED_OUT
  • 원인 (20260409000002_rls_policies.sql): team_members SELECT 정책이 자기 테이블을 재조회:
    CREATE POLICY "team_members_read_same_team" ON team_members
      FOR SELECT USING (
        team_id IN (SELECT team_id FROM team_members WHERE user_id = auth.uid())
      );
    
    이 정책이 Postgres RLS 엔진에서 무한 재귀 탐지로 에러. meetings / meeting_memos / meeting_documents / team_invites 정책이 모두 team_members 서브쿼리를 경유해서 전부 같이 터짐. Realtime 구독도 같은 이유로 TIMED_OUT.
  • 픽스 (server/supabase/migrations/20260411000001_fix_team_members_rls_recursion.sql):
    • SECURITY DEFINER 함수 2개 신규 — Supabase 공식 권장 패턴으로 재귀 끊기
      • public.user_team_ids(uid uuid) RETURNS SETOF uuid
      • public.user_admin_team_ids(uid uuid) RETURNS SETOF uuid
    • search_path = public, pg_temp 고정 + REVOKE ALL 후 authenticated, service_role에만 GRANT EXECUTE
    • 기존 정책 재작성: team_id IN (SELECT ... FROM team_members) → team_id IN (SELECT public.user_team_ids(auth.uid()))
    • 영향 정책: team_members × 4개, meetings × 2개, meeting_memos × 1개, meeting_documents × 2개, team_invites × 1개
    • supabase db push 적용 완료

Bug 3: LoginScreen stale 번들

  • 증상: Phase 1.5에서 LoginScreen import 제거했는데도 렌더러 화면에 LoginScreen 계속 뜸. HMR/vite cache 혼동 상태.
  • 원인: 불명 (vite HMR이 어딘가에서 stale state 유지한 듯, grep으로는 import 사용처 0개 확인)
  • 픽스: LoginScreen.tsx 파일 자체 삭제. vite가 컴파일할 대상이 아예 사라져서 stale이 불가능해짐.

검증

  • desktop tsc --noEmit ✅
  • desktop dev 런타임 ✅ (로그인 성공 확인)
  • Supabase db push ✅ (migration 적용)
  • pull / Realtime 재테스트 — 사용자가 Settings → Cloud Sync → Pull 눌러서 확인 필요

SaaS [7] 3 클라이언트 일원화 검증 — OAuth provider 연결 대기 (Phase 5)

Phase 1~4+3이 로컬/클라우드 feature gate + sync 인프라를 모두 배선했으나, 실제 로그인을 통해 "데스크톱 ↔ 웹 ↔ 모바일 같은 데이터 관찰"을 검증하려면 Supabase OAuth provider 연결이 선행되어야 함.

사용자가 해야 할 선행 작업 (Supabase 대시보드)

  1. https://supabase.com/dashboard/project/llnocwyqvhgwpdjcqqyw/auth/providers 접속
  2. Google provider 활성화:
    • Google Cloud Console에서 OAuth 2.0 Client ID 발급
      • Application type: Web application
      • Authorized redirect URIs: https://llnocwyqvhgwpdjcqqyw.supabase.co/auth/v1/callback
    • Supabase 대시보드의 Google provider에 Client ID/Secret 입력
  3. (선택) GitHub provider도 동일 패턴으로 활성화
  4. 데스크톱 deep link scheme은 이미 d3ro-voice://auth-callback으로 설정됨

OAuth 연결 후 검증 체크리스트 (다음 세션에서)

  • 데스크톱 Settings → Cloud Sync → Google 클릭 → 브라우저 동의 → 앱으로 deep link 콜백
  • [auth:signin] DB opened for ${uuid} 로그 + users/${uuid}/d3ro.db 파일 생성
  • [auth:signin] Initial push done + Initial pull done 로그
  • subscriptions 테이블에 free 티어 row 자동 생성 (auth trigger)
  • LicenseService tier가 'free'로 sync됨 (tier-changed 이벤트)
  • 웹 앱(apps/web) 에서 같은 계정으로 로그인 → 데스크톱에서 생성한 history/meetings 즉시 조회
  • 웹에서 meeting 편집 → 데스크톱에 Realtime으로 반영 (5초 이내)
  • 모바일 Expo 앱에서 같은 계정 로그인 → 같은 데이터
  • 데스크톱 signOut → LicenseService.resetToFree() + users/_local/d3ro.db로 복귀 → 익명 로컬 모드 계속 동작
  • 로그아웃 상태에서 PREMIUM_LLM 호출 시도 시 UpgradePromptModal (login_required) 표시

SaaS [6] 로컬 우선 sync + login_required UI (Phase 3, 2026-04-11)

빅뱅 Phase 3의 원래 설계("Supabase = source of truth")는 로컬 entry point 철학과 충돌하므로 재정의. 로컬이 source of truth, 클라우드는 로그인 시 mirror + 양방향 sync.

  • CloudSyncService _initialSync(reason)
    • _onAuthenticated 끝에 fire-and-forget으로 호출
    • 로그인 직후 pushAll() 이후 pullAll() 순차 실행
    • signin → 익명 세션 로컬 데이터를 사용자 계정으로 업로드
    • restore → 다른 기기 변경사항 반영
    • 실패해도 warn만, 수동 Sync 버튼으로 재시도 가능
  • FeatureAccess.reason / UpgradePromptEvent.reason 확장
    • 'login_required' 케이스 추가 (types.ts)
    • promptUpgrade(feature, 'login_required') — requiredTier: 'free'로 emit
  • UpgradePromptModal login_required 케이스
    • Title: "X은(는) 로그인이 필요합니다"
    • Desc: "클라우드 기능을 사용하려면 로그인하세요. 로컬 기능은 로그인 없이도 계속 사용할 수 있습니다."
    • Primary button: "로그인하기" → d3ro:open-settings custom event (tab=cloud)
  • LicenseService.consumeQuota() 에도 login_required 케이스 추가 — promptUpgrade + D3ROError(TierRequired)
  • i18n ko/en: license.loginRequired.title/desc/signIn
  • 검증
    • desktop tsc --noEmit ✅
    • desktop npm run build ✅ (renderer 2,130kB)
    • 실증은 OAuth provider 연결 후 Phase 5에서

Phase 3에서 의도적으로 뺀 것 (후속 사이클로)

  • auto push on write — VoiceMode / MeetingMode session 완료 즉시 pushOne(). 현재는 수동 Sync 버튼 + Realtime debounced pull만. 실제 구현은 각 서비스에 hook 추가가 필요하므로 규모 큼.
  • 오프라인 WriteQueue — 네트워크 끊긴 상태에서 write → 디스크 큐 → 복귀 시 flush. 현재는 실패 시 logger.warn만.
  • PREMIUM_LLM 실제 호출 경로 — LocalLLMService에 cloud mode 추가해서 llm-proxy Edge Function 호출 + canUse(PREMIUM_LLM) gate. feature enum/policy는 Phase 4에서 준비 완료.

SaaS [5] 티어 enforcement — 로컬 해방 + 클라우드 gate (Phase 4, 2026-04-11)

사용자 표현: "로컬 사용자는 풀어줘야해! 클라우드 동기화 같은 것과 고급 api가 막히는거지"

기존 LicenseService는 DICTATION 20/일, LLM_PROCESS 10/일 quota + LIVE_CAPTION/VOICE_MEMO/SCREEN_CONTEXT/... 대부분이 pro lock 이었음. 로컬 entry point 철학과 정면 충돌 → 전면 교체.

정책 매트릭스 (Phase 4 확정)

Feature 종류 익명 로컬 _local 로그인 free pro pro_plus
로컬 기능 16종 ✅ 무제한 ✅ 무제한 ✅ 무제한 ✅ 무제한
PREMIUM_LLM (신규) ❌ login_required 🔸 5/일 🔸 500/일 ✅ 무제한
CLOUD_SYNC (신규) ❌ login_required ✅ ✅ ✅
  • packages/core/src/types.ts:
    • Feature enum에 PREMIUM_LLM, CLOUD_SYNC 추가
    • FeatureAccess.reason에 'login_required' 추가 (익명 로컬 모드에서 cloud feature 요청 시)
  • LicenseService.ts:
    • QUOTA_LIMITS.free — DICTATION/LLM_PROCESS 20/10 삭제, PREMIUM_LLM: 5만 남김
    • QUOTA_LIMITS.pro — PREMIUM_LLM: 500 (fair use cap)
    • FEATURE_MIN_TIER — 기존 16개 feature 전부 'free'로 완화. 신규 PREMIUM_LLM/CLOUD_SYNC도 'free' (로그인만 하면 접근 가능, 티어는 quota로만 구분)
    • CLOUD_FEATURES: Set<Feature> 신규 — 익명 로컬 모드 감지용
    • canUse() — CLOUD_FEATURES.has(feature) && isLocalMode() → login_required 반환. 그 외는 기존 tier/quota 로직
    • syncFromCloud(tier) 신규 — Supabase subscriptions 티어를 electron-store에 캐시 + tier-changed emit
    • resetToFree() 신규 — 로그아웃 시 호출. licenseKey/activatedAt/lastVerifiedAt/offlineGraceUntil 모두 null로 리셋
    • db/index.ts의 isLocalMode() import 추가
  • CloudSyncService.ts:
    • _fetchSubscriptionTier(userId) 신규 — subscriptions 테이블에서 {tier, status: 'active'} 조회. 없으면 free, 비정상 티어도 free 폴백
    • _onAuthenticated():
      • DB 오픈 → subscription tier fetch → LicenseService.syncFromCloud(tier) → auth-changed emit → Realtime
      • fetch 실패 시 warn만 찍고 현재 티어 유지 (오프라인 grace)
    • _onSignOut():
      • Voice/Meeting/Caption stop → Realtime stop → auth.signOut → token clear → LicenseService.resetToFree() → closeCurrent → openLocal → auth-changed(null) emit
  • 검증
    • desktop tsc --noEmit ✅
    • desktop npm run build ✅ (main bundle 정상)
    • 익명 로컬 사용자는 이제 기존 기능(LIVE_CAPTION/VOICE_MEMO/MEETING_SUMMARY/FILE_TRANSCRIPTION 등 16개) 전부 사용 가능 — pro lock 완전 제거
    • 실측 검증 대기:
      • 기존 녹음/후처리가 quota 메시지 없이 계속 작동
      • PREMIUM_LLM 호출 경로는 Phase 3에서 추가 예정 → 지금은 feature 정의만

Phase 4에서 의도적으로 뺀 것

  • PREMIUM_LLM 실제 호출 경로 — Phase 3(cloud-first)에서 Edge Function llm-proxy 호출 로직과 함께
  • daily_usage를 Supabase로 동기화 — 현재는 로컬 SQLite daily_usage 테이블로만 추적 (사용자별 격리 DB라 문제 없음)
  • UpgradePromptModal 텍스트 업데이트 — login_required 케이스용 새 메시지는 Phase 3/5에서

SaaS [4] 회원가입 / 온보딩 (Phase 2, 2026-04-11)

기존 OnboardingModal을 로컬 모드 entry point에 맞게 정리 + Cloud Sync 티저 step 추가.

  • AppConfig 타입 정식화
    • packages/core/src/types.ts AppConfig에 onboardingCompleted: boolean 추가
    • ConfigService.CONFIG_DEFAULTS에 onboardingCompleted: false 추가
    • 타입 캐스팅(as keyof AppConfig, as never) 3곳 제거:
      • AppLayout.tsx 첫 실행 감지: cfg.onboardingCompleted 직접 접근
      • OnboardingModal.tsx handleFinish: key: 'onboardingCompleted' 리터럴
      • SettingsModal.tsx "온보딩 다시 보기" 버튼: 동일
  • 온보딩 Step 확장 (5 → 6 step)
    • 기존 flow: 환영 → 마이크 → 핫키 → Ollama → 완료
    • 새 flow: 환영 → 마이크 → 핫키 → Ollama → Cloud Sync(선택) → 완료
    • Cloud Sync step은 saasMode === true일 때만 노출:
      • getState()로 saasMode 조회
      • Ollama "다음" → nextAfterOllama(): saasMode면 step 4, 아니면 step 5로 분기
    • Cloud Sync step 내용: tagline + 3개 benefit (동기화/Premium LLM/팀공유) + "설정에서 언제든지 로그인" 안내. 로그인 버튼 없음 — onboarding scope 밖 (Settings의 CloudSyncSection이 유일한 OAuth 진입점).
  • i18n ko/en 키 추가 (packages/i18n/src/locales/):
    • onboarding.cloud.title / tagline / benefit1~3 / signInLater
    • 나머지 10개 locale은 fallback (Phase 2 scope 밖, 추후 일괄)
  • 검증
    • desktop tsc --noEmit ✅
    • desktop npm run build ✅ (renderer 2,129kB, +3kB)
    • 수동 검증 대기: Settings → About → "온보딩 다시 보기" 눌러서 신규 시퀀스 확인
  • Phase 2에서 의도적으로 뺀 것 (추후 사이클)
    • Supabase profiles.onboarded_at 컬럼 — OAuth provider(Google) 설정 전까진 의미 없음
    • OAuth 로그인 후 "환영" 페이지 — provider 연결 후 Phase 4와 함께
    • STT 모델 선택 step — faster-whisper sidecar가 첫 사용 시 자동 다운로드하므로 불필요
    • 온보딩 키의 나머지 10개 locale 번역

SaaS [3.5] 로컬 모드 = entry point (Phase 1.5, 2026-04-11)

비즈니스 모델: 무료 로컬 모드 = onboarding funnel 입구 (킬러 피처), 유료 클라우드 SaaS = 수익화 (OAuth 로그인 + pro 구독 + premium 모델 + sync). SaaS [2] OAuth 강제 게이트 철회.

  • db/index.ts
    • LOCAL_USER_ID = '_local' 상수 export
    • openLocal() 헬퍼 — openForUser('_local') 래퍼. 경로: users/_local/d3ro.db
    • isLocalMode() 헬퍼 — currentUserId === LOCAL_USER_ID
  • bootstrap.ts
    • database step 복원 (critical). openLocal() 호출로 앱 시작 시 로컬 DB 자동 오픈
    • 순서: logger → config → database (local) → license → windows → tray → ipc → ... → cloud-sync
  • CloudSyncService._onAuthenticated(session)
    • openForUser(userId) — 내부에서 기존 로컬 DB close 후 사용자 DB 재오픈 (자동)
    • auth-changed(user) emit 후 Realtime 시작
  • CloudSyncService._onSignOut()
    • VoiceMode cancel → MeetingMode stopRecording → Caption stop → Realtime stop → auth.signOut → token clear
    • closeCurrent() → openLocal() — 로컬 모드 DB로 복귀 (앱은 계속 로컬 모드로 동작)
    • auth-changed(null) emit
  • App.tsx
    • AuthGate 상태머신 완전 제거. LoginScreen import 제거.
    • 항상 <AppLayout /> <UpgradePromptModal /> 렌더링 (로그인/로그아웃 무관)
  • LoginScreen.tsx
    • 파일 유지 (dead code — 추후 Welcome 화면 재활용 가능)
  • 검증
    • desktop tsc --noEmit ✅
    • desktop npm run build ✅ (main 992kB, renderer 2,126kB)
    • dev 런타임 ✅:
      • [bootstrap] local database opened: .../users/_local/d3ro.db (created=true)
      • [bootstrap] database initialized → 나머지 step 정상
      • Main window shown — LoginScreen 없이 바로 메인 UI
    • 파일시스템: users/_local/d3ro.db + WAL/SHM 정상 생성

빅뱅 다음 세션 목표 (2026-04-11 wrap-up)

데스크톱을 진짜 SaaS 클라이언트로 완성. 자세한 계획 + 단계별 prompt는 memory/handoff-latest.md 참조.

핵심:

  1. 사용자별 SQLite 격리 (userData/users/${userId}/d3ro.db)
  2. 회원가입/온보딩 흐름 — OAuth 첫 가입 시 환영 + 기본 설정
  3. cloud-first 정책 — Supabase가 source of truth, 로컬 cache는 read-through
  4. 멀티 디바이스 sync 강화 (Realtime + last-write-wins → CRDT 검토)
  5. 결제/티어 enforcement (free 일일 quota, 데스크톱에서도 차단)
  6. 회의/지식 web ↔ 데스크톱 일원화

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