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

View file

@ -89,7 +89,31 @@ export enum ErrorCode {
DictionaryExportFailed = 723,
DictionaryImportInvalidFormat = 724,
// === Config (800-899) ===
// === Phase 10: Memo Tags (730-739) ===
MemoTagDuplicate = 730,
MemoTagNotFound = 731,
MemoExportFailed = 732,
// === Phase 10: Voice Commands (740-749) ===
VoiceCommandMatchFailed = 740,
VoiceCommandNotFound = 741,
// === Phase 10: Screen Context (750-759) ===
ContextCaptureFailed = 750,
ContextSelectedTextFailed = 751,
// === Phase 10: LLM Chain (760-769) ===
ChainNotFound = 760,
ChainExecutionFailed = 761,
ChainStepFailed = 762,
ChainCancelled = 763,
// === Phase 10: Live Caption (770-779) ===
CaptionStartFailed = 770,
CaptionAlreadyActive = 771,
CaptionSTTFailed = 772,
// === Config (800-849) ===
ConfigReadFailed = 800,
ConfigWriteFailed = 801,
ConfigInvalidValue = 802,
@ -97,6 +121,18 @@ export enum ErrorCode {
ConfigResetFailed = 804,
ConfigMigrationFailed = 810,
// === License (850-869) ===
LicenseKeyInvalid = 850,
LicenseKeyExpired = 851,
LicenseActivationFailed = 852,
LicenseDeactivationFailed = 853,
LicenseMachineIdMismatch = 854,
LicenseOfflineGraceExpired = 855,
LicenseVerificationFailed = 856,
FeatureNotAvailable = 860,
QuotaExceeded = 861,
TierRequired = 862,
// === System / Window (900-999) ===
WindowCreationFailed = 900,
WindowNotFound = 901,

View file

@ -76,6 +76,8 @@ export const IPC_CHANNELS = {
SET_HANDS_FREE_SHORTCUT: 'hotkey:setHandsFreeShortcut',
GET_COMMAND_SHORTCUT: 'hotkey:getCommandShortcut',
SET_COMMAND_SHORTCUT: 'hotkey:setCommandShortcut',
GET_CAPTION_SHORTCUT: 'hotkey:getCaptionShortcut',
SET_CAPTION_SHORTCUT: 'hotkey:setCaptionShortcut',
IS_ENABLED: 'hotkey:isEnabled',
SET_ENABLED: 'hotkey:setEnabled',
START_RECORDING: 'hotkey:startRecording',
@ -159,7 +161,79 @@ export const IPC_CHANNELS = {
GET_WEEKLY: 'stats:getWeekly',
// Main → Renderer events
UPDATED: 'stats:updated'
}
},
// ── Phase 10: Memo Tags (10.3) ──
MEMO: {
GET_TAGS: 'memo:getTags',
ADD_TAG: 'memo:addTag',
REMOVE_TAG: 'memo:removeTag',
GET_ALL_TAGS: 'memo:getAllTags',
SEARCH_BY_TAG: 'memo:searchByTag',
EXPORT: 'memo:export',
},
// ── Phase 10: Voice Commands (10.5) ──
VOICE_COMMAND: {
GET_ALL: 'voiceCommand:getAll',
SET_KEYWORDS: 'voiceCommand:setKeywords',
SET_ENABLED: 'voiceCommand:setEnabled',
IS_ENABLED: 'voiceCommand:isEnabled',
// Main → Renderer events
MATCHED: 'voiceCommand:matched',
},
// ── Phase 10: Screen Context (10.2) ──
CONTEXT: {
CAPTURE: 'context:capture',
GET_CONFIG: 'context:getConfig',
SET_ENABLED: 'context:setEnabled',
IS_ENABLED: 'context:isEnabled',
},
// ── Phase 10: LLM Chain (10.4) ──
CHAIN: {
GET_ALL: 'chain:getAll',
CREATE: 'chain:create',
UPDATE: 'chain:update',
DELETE: 'chain:delete',
EXECUTE: 'chain:execute',
// Main → Renderer events
PROGRESS: 'chain:progress',
},
// ── Phase 10: Live Caption (10.1) ──
CAPTION: {
START: 'caption:start',
STOP: 'caption:stop',
GET_STATE: 'caption:getState',
SET_CONFIG: 'caption:setConfig',
GET_CONFIG: 'caption:getConfig',
/** 렌더러 → 메인: 시스템 오디오 PCM 데이터 전달 */
SYSTEM_AUDIO_DATA: 'caption:systemAudioData',
// Main → Renderer events
SEGMENT: 'caption:segment',
DELTA: 'caption:delta',
STATE_CHANGED: 'caption:stateChanged',
SESSION_SAVED: 'caption:sessionSaved',
/** 메인 → 렌더러: 시스템 오디오 캡처 시작/정지 요청 */
START_SYSTEM_AUDIO: 'caption:startSystemAudio',
STOP_SYSTEM_AUDIO: 'caption:stopSystemAudio',
},
// ── Phase 11: License & Monetization ──
LICENSE: {
GET_INFO: 'license:getInfo',
ACTIVATE: 'license:activate',
DEACTIVATE: 'license:deactivate',
CHECK_FEATURE: 'license:checkFeature',
GET_USAGE: 'license:getUsage',
GET_ALL_USAGE: 'license:getAllUsage',
GET_TIER_COMPARISON: 'license:getTierComparison',
// Main → Renderer events
UPGRADE_PROMPT: 'license:upgradePrompt',
TIER_CHANGED: 'license:tierChanged',
},
} as const
// 타입 유틸리티: 채널명 유니온 추출

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}`
}

View file

@ -5,7 +5,7 @@
// Common
// ============================================================
export type ThemeMode = 'light' | 'dark' | 'auto'
export type ThemeMode = 'light' | 'dark' | 'auto' | 'nord' | 'solarized' | 'catppuccin' | 'dracula'
export type VoiceMode = 'dictation' | 'hands-free'
@ -267,7 +267,7 @@ export interface LLMModel {
modifiedAt: string
}
export type LLMAction = 'refine' | 'translate' | 'summarize' | 'expand' | 'grammar' | 'custom'
export type LLMAction = 'refine' | 'translate' | 'summarize' | 'expand' | 'grammar' | 'custom' | 'chain'
export interface LLMProcessParams {
text: string
@ -336,7 +336,7 @@ export interface SetEnabledParams {
enabled: boolean
}
export type HotkeyAction = 'dictation' | 'hands-free' | 'command'
export type HotkeyAction = 'dictation' | 'hands-free' | 'command' | 'caption'
export interface HotkeyTriggeredEvent {
action: HotkeyAction
@ -370,6 +370,8 @@ export interface AppConfig {
dictationShortcut: HotkeyBinding
handsFreeShortcut: HotkeyBinding
commandShortcut: HotkeyBinding
/** 실시간 자막 토글 핫키 (Phase 10.1) */
captionShortcut: HotkeyBinding
hotkeyEnabled: boolean
insertMethod: 'clipboard' | 'keyboard'
autoInsert: boolean
@ -380,6 +382,8 @@ export interface AppConfig {
agentModeEnabled: boolean
/** 핸즈프리 모드 활성화 (토글) */
handsFreeEnabled: boolean
/** 스크린 컨텍스트 캡처 활성화 (Phase 10.2) */
screenContextEnabled: boolean
}
export interface ConfigGetParams {
@ -662,3 +666,284 @@ export interface WeeklyStats {
wordCount: number
sessionCount: number
}
// ============================================================
// Phase 10: Memo Tags (10.3)
// ============================================================
export interface MemoTag {
id: string
historyId: string
tag: string
createdAt: number
}
export interface AddTagParams {
historyId: string
tag: string
}
export interface RemoveTagParams {
historyId: string
tag: string
}
export interface GetTagsParams {
historyId: string
}
export interface SearchByTagParams {
tag: string
page: number
pageSize: number
}
export interface ExportMemoParams {
format: 'markdown'
tag?: string
from?: string
to?: string
}
export interface TagCount {
tag: string
count: number
}
// ============================================================
// Phase 10: Voice Commands (10.5)
// ============================================================
export type KeywordMatchMode = 'prefix' | 'suffix' | 'contains'
export interface VoiceCommandKeyword {
keyword: string
matchMode: KeywordMatchMode
}
export interface VoiceCommandRule {
id: string
instructionId: string
keywords: VoiceCommandKeyword[]
enabled: boolean
priority: number
}
export interface VoiceCommandMatch {
matched: boolean
ruleId: string | null
instructionId: string | null
/** 키워드 제거 후 남은 텍스트 */
cleanedText: string
/** 매칭된 키워드 */
matchedKeyword: string | null
}
export interface SetVoiceCommandKeywordsParams {
instructionId: string
keywords: VoiceCommandKeyword[]
}
export interface SetVoiceCommandEnabledParams {
enabled: boolean
}
// ============================================================
// Phase 10: Screen Context (10.2)
// ============================================================
export interface ScreenContext {
appName: string | null
windowTitle: string | null
selectedText: string | null
capturedAt: number
}
export interface CaptureContextResult {
context: ScreenContext
/** 선택 텍스트 캡처 시도 여부 */
selectedTextAttempted: boolean
}
// ============================================================
// Phase 10: LLM Chain (10.4)
// ============================================================
export interface ChainStep {
instructionId: string
/** 이전 단계 결과 사용 or 원본 텍스트 사용 */
inputSource: 'previous' | 'original'
}
export interface LLMChain {
id: string
name: string
steps: ChainStep[]
createdAt: number
updatedAt: number
}
export interface CreateChainParams {
name: string
steps: ChainStep[]
}
export interface UpdateChainParams {
id: string
name?: string
steps?: ChainStep[]
}
export interface DeleteChainParams {
id: string
}
export interface ExecuteChainParams {
chainId: string
text: string
}
export interface ChainProgress {
chainId: string
currentStep: number
totalSteps: number
stepName: string
intermediateText: string
}
export interface ChainExecutionResult {
chainId: string
finalText: string
steps: Array<{ instructionId: string; output: string; durationMs: number }>
totalDurationMs: number
}
// ============================================================
// Phase 10: Live Caption (10.1)
// ============================================================
export type CaptionState = 'inactive' | 'starting' | 'active' | 'stopping'
export interface CaptionSegment {
id: string
text: string
timestamp: number
isFinal: boolean
}
export type CaptionAudioSource = 'mic' | 'system' | 'both'
export interface CaptionConfig {
fontSize: number
opacity: number
maxLines: number
autoClearMs: number
/** 오디오 소스: 마이크 / 시스템 오디오 / 둘 다 */
audioSource: CaptionAudioSource
}
export interface CaptionSessionSummary {
sessionId: string
segments: CaptionSegment[]
startedAt: number
endedAt: number
totalDurationMs: number
}
// ============================================================
// Phase 11: License & Monetization
// ============================================================
/** 라이센스 티어 */
export type LicenseTier = 'free' | 'pro' | 'pro_plus'
/** 기능 게이팅 대상 */
export enum Feature {
// 쿼터 제한 기능 (Free에서 횟수 제한)
DICTATION = 'dictation',
LLM_PROCESS = 'llm_process',
// Pro 이상
HISTORY_UNLIMITED = 'history_unlimited',
HISTORY_EXPORT = 'history_export',
CUSTOM_INSTRUCTION_CREATE = 'custom_instruction_create',
LIVE_CAPTION = 'live_caption',
SCREEN_CONTEXT = 'screen_context',
VOICE_MEMO = 'voice_memo',
VOICE_COMMAND = 'voice_command',
LLM_CHAIN = 'llm_chain',
// Pro+ 이상
FILE_TRANSCRIPTION = 'file_transcription',
VOICE_CONVERSATION = 'voice_conversation',
DICTATION_TEMPLATE = 'dictation_template',
MEETING_SUMMARY = 'meeting_summary',
LOCAL_RAG = 'local_rag',
OS_AUTOMATION = 'os_automation',
}
/** 라이센스 정보 (electron-store에 저장) */
export interface LicenseInfo {
tier: LicenseTier
licenseKey: string | null
activatedAt: number | null
machineId: string
/** 마지막 온라인 검증 시각 */
lastVerifiedAt: number | null
/** 오프라인 유예 만료 (lastVerifiedAt + 30일) */
offlineGraceUntil: number | null
}
/** 일일 사용량 */
export interface DailyUsage {
date: string // 'YYYY-MM-DD'
feature: string
count: number
}
/** 쿼터 정보 */
export interface UsageQuota {
feature: Feature
used: number
limit: number // -1 = 무제한
remaining: number // -1 = 무제한
resetAt: string // 다음 리셋 시각 (내일 00:00) ISO 8601
}
/** 기능 접근 결과 */
export interface FeatureAccess {
allowed: boolean
reason: 'ok' | 'quota_exceeded' | 'tier_required' | 'license_expired'
requiredTier?: LicenseTier
quota?: UsageQuota
}
/** 업그레이드 유도 이벤트 */
export interface UpgradePromptEvent {
feature: Feature
reason: 'quota_exceeded' | 'tier_required'
currentTier: LicenseTier
requiredTier: LicenseTier
quota?: UsageQuota
}
/** 라이센스 활성화 파라미터 */
export interface ActivateLicenseParams {
licenseKey: string
}
/** 라이센스 활성화 결과 */
export interface ActivateLicenseResult {
success: boolean
tier: LicenseTier
message: string
}
/** 티어별 기능 비교 항목 */
export interface TierComparison {
feature: Feature
featureLabel: string
free: boolean | string
pro: boolean | string
proPlus: boolean | string
}