Phase 10~11 전체 구현: 킬러 피처 5종 + 수익화 시스템

Phase 10 킬러 피처:
- MemoService: 태그 CRUD + 마크다운 내보내기 (memo_tags DB)
- VoiceCommandService: 키워드→명령어 매칭, 프리셋 4종
- ScreenContextService: PowerShell 활성 윈도우 + Ctrl+C 선택 텍스트
- ChainService: LLM 명령어 순차 실행 파이프라인
- CaptionService: 6초 청크 연속 전사 + 시스템 오디오 루프백

VoiceModeService 파이프라인 통합:
- 녹음 시작 → 컨텍스트 캡처 → STT → 키워드 매칭 → LLM(체인/컨텍스트 주입) → 삽입

시스템 오디오 캡처:
- setDisplayMediaRequestHandler + audio: 'loopback' (IPC 브릿지)
- electron-audio-loopback 패키지 contextIsolation 호환 불가 → 직접 구현

Phase 11 수익화:
- LicenseService: Free/Pro/Pro+ 3티어, LemonSqueezy API
- Feature Gate: requireFeature/checkFeature/consumeFeature
- 일일 쿼터: Free dictation 20/일, LLM 10/일 (SQLite daily_usage)
- LicenseModal, ProBadge, UpgradePromptModal UI

디자인 보강:
- d3roTypo(13종), d3roShadow(10종), d3roRadius(7종) 토큰 시스템
- ScreenPanel, ButtonGroup DS 컴포넌트 신규
- PhosphorText 4→13종 변형, MetalDial conic-gradient 광택
- 공유 컴포넌트: EmptyStateCard, SearchInput, PageHeader, HistoryEntryCard

기타:
- 자막 핫키 SSOT 전체 연동 (Config→Hotkey→VoiceMode→Caption→Settings)
- StatusBar 자막 LED + 효과음, 자막 로딩 UI
- LLM 상태 이벤트 전파 수정 (폴링 제거 → onStatusChanged)
- 커맨드 팝업 "선택 해제" 항목 추가
This commit is contained in:
Yun Chan 2026-04-05 21:36:09 +09:00
parent 36d77ca224
commit a31f96bbb8
97 changed files with 11853 additions and 1143 deletions

199
src/shared/theme-vars.ts Normal file
View file

@ -0,0 +1,199 @@
// src/shared/theme-vars.ts
// 팝업 윈도우용 CSS 변수 맵.
// main process (WindowManager)에서 insertCSS로 팝업에 테마를 주입할 때 사용.
// renderer의 theme.ts RAW와 동기화 유지 필요.
// Node.js/Electron main 환경에서 import 가능 (DOM API 없음).
export type PopupThemeKey = 'dark' | 'light' | 'nord' | 'solarized' | 'catppuccin' | 'dracula'
interface PopupThemeVars {
/** 카드 배경 (팝업 본체) */
'--d3-bg-card': string
/** 앱 배경 (recording-tip 반투명 배경) */
'--d3-bg-tip': string
/** 주요 텍스트 */
'--d3-text-primary': string
/** 보조 텍스트 */
'--d3-text-secondary': string
/** 비활성 텍스트 (번호, 시간 등) */
'--d3-text-inactive': string
/** 힌트/최약 텍스트 */
'--d3-text-muted': string
/** dimLabel 텍스트 (command-popup 제목) */
'--d3-text-dimLabel': string
/** 기본 테두리 */
'--d3-border-default': string
/** 미묘한 테두리 (구분선, hover) */
'--d3-border-subtle': string
/** 강한 테두리 (progress background) */
'--d3-border-strong': string
/** 악센트 색상 (웨이브바, 선택 바) */
'--d3-accent-main': string
/** 악센트 dim (active 배경) */
'--d3-accent-dim': string
/** 팝업 박스 섀도 */
'--d3-shadow-popup': string
/** 결과 팝업 배경 (불투명) */
'--d3-bg-result': string
/** 결과 팝업 테두리 */
'--d3-border-result': string
/** 결과 팝업 텍스트 */
'--d3-text-result': string
/** 결과 팝업 버튼 색상 */
'--d3-action-btn': string
/** 결과 팝업 버튼 hover 배경 */
'--d3-action-btn-hover-bg': string
/** 결과 팝업 버튼 hover 색상 */
'--d3-action-btn-hover': string
}
const POPUP_THEME_VARS: Record<PopupThemeKey, PopupThemeVars> = {
dark: {
'--d3-bg-card': '#242427',
'--d3-bg-tip': 'rgba(0,0,0,0.85)',
'--d3-text-primary': 'rgba(255,255,255,0.87)',
'--d3-text-secondary': 'rgba(255,255,255,0.6)',
'--d3-text-inactive': 'rgba(255,255,255,0.3)',
'--d3-text-muted': 'rgba(255,255,255,0.25)',
'--d3-text-dimLabel': '#5c2615',
'--d3-border-default': 'rgba(255,255,255,0.08)',
'--d3-border-subtle': 'rgba(255,255,255,0.06)',
'--d3-border-strong': 'rgba(255,255,255,0.15)',
'--d3-accent-main': '#f25b29',
'--d3-accent-dim': 'rgba(242,91,41,0.08)',
'--d3-shadow-popup': '0 8px 32px rgba(0,0,0,0.5)',
'--d3-bg-result': '#1e1e1e',
'--d3-border-result': 'rgba(255,255,255,0.08)',
'--d3-text-result': 'rgba(255,255,255,0.87)',
'--d3-action-btn': 'rgba(255,255,255,0.4)',
'--d3-action-btn-hover-bg': 'rgba(255,255,255,0.08)',
'--d3-action-btn-hover': 'rgba(255,255,255,0.7)',
},
light: {
'--d3-bg-card': '#ffffff',
'--d3-bg-tip': 'rgba(255,255,255,0.92)',
'--d3-text-primary': 'rgba(0,0,0,0.87)',
'--d3-text-secondary': 'rgba(0,0,0,0.6)',
'--d3-text-inactive': 'rgba(0,0,0,0.35)',
'--d3-text-muted': 'rgba(0,0,0,0.25)',
'--d3-text-dimLabel': '#b07040',
'--d3-border-default': 'rgba(0,0,0,0.10)',
'--d3-border-subtle': 'rgba(0,0,0,0.06)',
'--d3-border-strong': 'rgba(0,0,0,0.14)',
'--d3-accent-main': '#f25b29',
'--d3-accent-dim': 'rgba(242,91,41,0.08)',
'--d3-shadow-popup': '0 8px 32px rgba(0,0,0,0.12)',
'--d3-bg-result': '#ffffff',
'--d3-border-result': 'rgba(0,0,0,0.08)',
'--d3-text-result': 'rgba(0,0,0,0.87)',
'--d3-action-btn': 'rgba(0,0,0,0.4)',
'--d3-action-btn-hover-bg': 'rgba(0,0,0,0.06)',
'--d3-action-btn-hover': 'rgba(0,0,0,0.7)',
},
nord: {
'--d3-bg-card': '#3b4252',
'--d3-bg-tip': 'rgba(46,52,64,0.92)',
'--d3-text-primary': 'rgba(236,239,244,0.87)',
'--d3-text-secondary': 'rgba(216,222,233,0.6)',
'--d3-text-inactive': 'rgba(216,222,233,0.35)',
'--d3-text-muted': 'rgba(216,222,233,0.25)',
'--d3-text-dimLabel': '#5e81ac',
'--d3-border-default': 'rgba(216,222,233,0.10)',
'--d3-border-subtle': 'rgba(216,222,233,0.06)',
'--d3-border-strong': 'rgba(216,222,233,0.16)',
'--d3-accent-main': '#88c0d0',
'--d3-accent-dim': 'rgba(136,192,208,0.08)',
'--d3-shadow-popup': '0 8px 32px rgba(0,0,0,0.5)',
'--d3-bg-result': '#434c5e',
'--d3-border-result': 'rgba(216,222,233,0.10)',
'--d3-text-result': 'rgba(236,239,244,0.87)',
'--d3-action-btn': 'rgba(216,222,233,0.4)',
'--d3-action-btn-hover-bg': 'rgba(216,222,233,0.08)',
'--d3-action-btn-hover': 'rgba(216,222,233,0.7)',
},
solarized: {
'--d3-bg-card': '#073642',
'--d3-bg-tip': 'rgba(0,43,54,0.92)',
'--d3-text-primary': 'rgba(238,232,213,0.87)',
'--d3-text-secondary': 'rgba(147,161,161,0.7)',
'--d3-text-inactive': 'rgba(147,161,161,0.4)',
'--d3-text-muted': 'rgba(147,161,161,0.25)',
'--d3-text-dimLabel': '#7a6c2e',
'--d3-border-default': 'rgba(131,148,150,0.13)',
'--d3-border-subtle': 'rgba(131,148,150,0.08)',
'--d3-border-strong': 'rgba(131,148,150,0.20)',
'--d3-accent-main': '#b58900',
'--d3-accent-dim': 'rgba(181,137,0,0.08)',
'--d3-shadow-popup': '0 8px 32px rgba(0,0,0,0.5)',
'--d3-bg-result': '#0d4250',
'--d3-border-result': 'rgba(131,148,150,0.13)',
'--d3-text-result': 'rgba(238,232,213,0.87)',
'--d3-action-btn': 'rgba(147,161,161,0.4)',
'--d3-action-btn-hover-bg': 'rgba(131,148,150,0.10)',
'--d3-action-btn-hover': 'rgba(147,161,161,0.7)',
},
catppuccin: {
'--d3-bg-card': '#313244',
'--d3-bg-tip': 'rgba(30,30,46,0.92)',
'--d3-text-primary': 'rgba(205,214,244,0.87)',
'--d3-text-secondary': 'rgba(186,194,222,0.7)',
'--d3-text-inactive': 'rgba(166,173,200,0.4)',
'--d3-text-muted': 'rgba(166,173,200,0.25)',
'--d3-text-dimLabel': '#585b70',
'--d3-border-default': 'rgba(205,214,244,0.08)',
'--d3-border-subtle': 'rgba(205,214,244,0.04)',
'--d3-border-strong': 'rgba(205,214,244,0.14)',
'--d3-accent-main': '#cba6f7',
'--d3-accent-dim': 'rgba(203,166,247,0.08)',
'--d3-shadow-popup': '0 8px 32px rgba(0,0,0,0.5)',
'--d3-bg-result': '#3a3a54',
'--d3-border-result': 'rgba(205,214,244,0.08)',
'--d3-text-result': 'rgba(205,214,244,0.87)',
'--d3-action-btn': 'rgba(166,173,200,0.4)',
'--d3-action-btn-hover-bg': 'rgba(205,214,244,0.06)',
'--d3-action-btn-hover': 'rgba(205,214,244,0.7)',
},
dracula: {
'--d3-bg-card': '#44475a',
'--d3-bg-tip': 'rgba(40,42,54,0.92)',
'--d3-text-primary': 'rgba(248,248,242,0.87)',
'--d3-text-secondary': 'rgba(169,176,208,0.7)',
'--d3-text-inactive': 'rgba(169,176,208,0.4)',
'--d3-text-muted': 'rgba(169,176,208,0.25)',
'--d3-text-dimLabel': '#6272a4',
'--d3-border-default': 'rgba(248,248,242,0.08)',
'--d3-border-subtle': 'rgba(248,248,242,0.04)',
'--d3-border-strong': 'rgba(248,248,242,0.14)',
'--d3-accent-main': '#bd93f9',
'--d3-accent-dim': 'rgba(189,147,249,0.08)',
'--d3-shadow-popup': '0 8px 32px rgba(0,0,0,0.5)',
'--d3-bg-result': '#4f5266',
'--d3-border-result': 'rgba(248,248,242,0.08)',
'--d3-text-result': 'rgba(248,248,242,0.87)',
'--d3-action-btn': 'rgba(169,176,208,0.4)',
'--d3-action-btn-hover-bg': 'rgba(248,248,242,0.06)',
'--d3-action-btn-hover': 'rgba(248,248,242,0.7)',
},
}
/**
* 팝업용 CSS 변수 맵 조회.
* ThemeMode('system' | 'light' | 'dark') 또는 테마 키를 받아 해당 변수 맵 반환.
* 'system' 또는 알 수 없는 값은 'dark'로 폴백.
*/
export function getPopupThemeVars(themeKey: string): PopupThemeVars {
const key = themeKey as PopupThemeKey
return POPUP_THEME_VARS[key] ?? POPUP_THEME_VARS.dark
}
/**
* 팝업 BrowserWindow에 insertCSS로 주입할 CSS 문자열 반환.
* `:root { --d3-bg-card: #242427; ... }`
*/
export function buildPopupThemeCss(themeKey: string): string {
const vars = getPopupThemeVars(themeKey)
const declarations = Object.entries(vars)
.map(([prop, value]) => ` ${prop}: ${value};`)
.join('\n')
return `:root {\n${declarations}\n}`
}