d3ro-voice/memory/project_status.md
Yun Chan 708e20f747
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
2026-08-20 11:12:05 +09:00

146 KiB
Raw Blame History

D3RO-VOICE 프로젝트 현황

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 모델: baselarge-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.jsonbuild.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/i18nresolveTranslation{{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: NSISdlaka.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.tssetup: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 configureCMakeDebugfix-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_checkmanager 추가, 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.tsxonError 콜백이 기존에는 주석만 있고 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이 사라짐". 원인은 _handleErrorsetTimeout(() => 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개}.jsonsettings.ollamaHint, ollama.step2.alt 2개 키 (qwen3:4b → gemma4:e4b, qwen3:8b → gemma4:26b)
  • site/src/i18n/locales/{10개}.ts — 마케팅 페이지 detail 문자열 qwen3 / llama3 / gemma3gemma4 / llama3.2 / phi4
  • site/src/sections/HowItWorks.tsx:47 — 파이프라인 시각화 qwen3:4b @ localhostgemma4:e4b @ localhost
  • apps/desktop/tests/main/services/VoiceModeService.test.ts:58 — test fixture llmModelId
  • docs/design/00-master-architecture.mdLLMConfig.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.warnlogger.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.tsVOICE_CONVERSATION.AUDIO_LEVEL 채널 추가
  • apps/desktop/src/main/services/SoundEffectService.tsSoundName'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.tsvoiceConversation.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}.jsonconversation.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.tskeyCodeToName/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._insertViaClipboardnut.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:105spawn('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곳은 품질 리뷰 결과 이미 최적 상태로 판정(수정 없음).

신규 helperapps/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) returnisComposing은 모던 표준, 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.tsxsupabase.from('transcripts').select(...).eq('meeting_id', id)만 읽고 LiveTranscriptList에 넘김. 데스크톱은 CaptionService 세그먼트를 인메모리에 버퍼링했다가 meeting_sessions.rawTranscript 단일 텍스트 필드에만 저장 — transcripts 테이블에는 아무것도 쓰지 않음. Realtime 스트림 설계(웹 실시간 캡션 뷰어용)와 데스크톱 오프라인 전사 플로우의 접점이 없었음.
  • Fix 2 (웹 fallback): transcripts가 비어있고 미팅 상태가 recording이 아닐 때 meeting.edited_transcript ?? meeting.raw_transcriptwhiteSpace: '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:156MetalCard<Link>로 감싸져 있지 않아 클릭 인터랙션 자체가 없었음.
  • Fix 3: next/linkLinkMetalCard를 감싸고 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.dbcontinuing 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.sqlid 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:134ALTER 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 shownRestored sessionDB opened for 7da3dd02Initial 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 ALLauthenticated, 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.proPREMIUM_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.tsisLocalMode() 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 AppConfigonboardingCompleted: boolean 추가
    • ConfigService.CONFIG_DEFAULTSonboardingCompleted: 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.tsxelectronI18nStorage 어댑터 구현 후 <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 mvpackages/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.jsonpaths@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.sqlhandle_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.tsEXE_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.pyIS_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.ymlmac: 타겟 추가 (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.tstypoSx(key) 헬퍼 추가 — d3roTypo를 MUI sx 친화 포맷으로 변환
  • theme.tsd3roPalette.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