빅뱅 8/8 마지막 성공 기준 달성. Supabase Edge Function(llm-proxy)을 통해 Anthropic Claude를 호출하는 PremiumLLMService 신규 구현. 사용자가 Settings에서 Local/Premium 백엔드를 선택하면 VoiceModeService가 자동 분기하고, Premium 실패 시 Local로 silent fallback + 상단 중앙 배너 알림. 실측: Claude Haiku refine 1.6~3.2초 (이전 qwen3 42.9초 → 13~27배 빠름). 주요 변경: - PremiumLLMService 신규 (싱글톤+EventEmitter, processText/chatStream, Supabase functions.invoke 기반, _ensureAuth 가드) - llm-prompts.ts: SYSTEM_PROMPTS를 Local/Premium 공유 모듈로 추출 (resolveSystemPrompt 헬퍼) - VoiceModeService: _getLLMProcessor → _runProcessorWithFallback 라우터 + premium-llm-fallback 이벤트 - CloudSyncService: getAccessToken(async), getAnonKey, invokeFunction(auth 헤더 자동 처리, 에러 body 파싱) - IPC: LLM.PREMIUM_* 채널 6개 + preload API + llm-handlers 이벤트 전달 (safeSendToRenderer 헬퍼) - AppConfig.llmBackend: 'local' | 'premium' (기본 'local') - Settings UI: Backend 드롭다운 + Premium 선택 시 Ollama UI 숨김 + 라이선스 모달 자동 오픈 - AppLayout: 상단 중앙 Snackbar fallback 배너 (8초, warning filled) - LicenseModal: 라이선스 키 입력 제거 → SaaS 구독 관리 UI 전환 (Free/Pro/Pro+ 업그레이드 버튼, Payple 준비 중 스텁) - 등급 비교 표: featureLabel i18n 번역 수정 서버 (Supabase Edge Functions): - quota.ts: 모델별 쿼터 구조 (llm_haiku/sonnet/opus × free/pro/pro_plus), 주간/일간 기간 분리, modelToQuotaKey 매핑, consumeQuota baseLimit 파라미터화 - llm-proxy: 모델별 쿼터 체크 + 소비 (checkQuota → consumeQuota 원자적), verify_jwt=false (2026 sb_publishable_ 키 호환) - config.toml: llm-proxy verify_jwt = false - migration 20260412000001: tier team→pro_plus 통일, subscriptions.overage_credits 컬럼, consume_quota RPC (원자적 base→overage fallback) Tier/쿼터: - free: Haiku 250/주간, Sonnet/Opus 불가 - pro ₩9,900: Haiku 1500/일, Sonnet 300/일, Opus 50/일 - pro_plus ₩29,900: Haiku 무제한, Sonnet 1500/일, Opus 300/일 - api-client SubscriptionTier: team→pro_plus, overage_credits 필드 추가
150 lines
5.6 KiB
PL/PgSQL
150 lines
5.6 KiB
PL/PgSQL
-- ============================================================================
|
|
-- Phase 3.2: Tier 통일 (team → pro_plus) + 오버리지 크레딧 데이터 모델
|
|
-- ============================================================================
|
|
-- 데스크톱 코드가 이미 'pro_plus'를 사용 중이고 서버만 'team'이 남아있어
|
|
-- 발생한 불일치를 해소. 팀 협업 feature(teams/team_members 테이블)와
|
|
-- 가격제(tier) 개념은 분리 — 이 마이그레이션은 가격제만 건드린다.
|
|
--
|
|
-- 추가로 SaaS 오버리지 구매 모델을 위한 subscriptions.overage_credits 컬럼
|
|
-- 도입. 실제 Stripe 연결은 Phase 3.3 이월, 이번 마이그레이션은 데이터 모델과
|
|
-- 읽기 경로만 준비.
|
|
|
|
-- ----------------------------------------------------------------------------
|
|
-- 1. profiles.tier: 기존 CHECK 제약 제거 → 값 마이그레이션 → 새 CHECK
|
|
-- ----------------------------------------------------------------------------
|
|
ALTER TABLE public.profiles DROP CONSTRAINT IF EXISTS profiles_tier_check;
|
|
|
|
UPDATE public.profiles SET tier = 'pro_plus' WHERE tier = 'team';
|
|
|
|
ALTER TABLE public.profiles
|
|
ADD CONSTRAINT profiles_tier_check
|
|
CHECK (tier IN ('free', 'pro', 'pro_plus'));
|
|
|
|
-- ----------------------------------------------------------------------------
|
|
-- 2. subscriptions.tier: 동일 처리
|
|
-- ----------------------------------------------------------------------------
|
|
ALTER TABLE public.subscriptions DROP CONSTRAINT IF EXISTS subscriptions_tier_check;
|
|
|
|
UPDATE public.subscriptions SET tier = 'pro_plus' WHERE tier = 'team';
|
|
|
|
ALTER TABLE public.subscriptions
|
|
ADD CONSTRAINT subscriptions_tier_check
|
|
CHECK (tier IN ('free', 'pro', 'pro_plus'));
|
|
|
|
-- ----------------------------------------------------------------------------
|
|
-- 3. subscriptions.overage_credits: SaaS 오버리지 구매 모델
|
|
-- ----------------------------------------------------------------------------
|
|
ALTER TABLE public.subscriptions
|
|
ADD COLUMN IF NOT EXISTS overage_credits integer NOT NULL DEFAULT 0;
|
|
|
|
COMMENT ON COLUMN public.subscriptions.overage_credits IS
|
|
'추가 크레딧 (베이스 일일 쿼터 소진 시 차감). Phase 3.2는 데이터 모델만, 실제 Stripe 구매 경로는 Phase 3.3.';
|
|
|
|
-- ----------------------------------------------------------------------------
|
|
-- 4. consume_quota RPC: 원자적 base → overage fallback 소비
|
|
-- ----------------------------------------------------------------------------
|
|
-- 기존 increment_daily_usage는 유지 (다른 경로에서 쓰일 수 있음).
|
|
-- llm-proxy는 이 새 RPC를 사용해 원자적으로 base → overage 순차 소비.
|
|
|
|
CREATE OR REPLACE FUNCTION public.consume_quota(
|
|
p_user_id uuid,
|
|
p_feature text,
|
|
p_base_limit integer
|
|
) RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
v_current integer;
|
|
v_overage integer;
|
|
v_new_count integer;
|
|
v_new_overage integer;
|
|
BEGIN
|
|
-- 오늘 사용량 조회 (없으면 0)
|
|
SELECT count INTO v_current
|
|
FROM public.daily_usage
|
|
WHERE user_id = p_user_id
|
|
AND feature = p_feature
|
|
AND date = CURRENT_DATE;
|
|
v_current := COALESCE(v_current, 0);
|
|
|
|
-- 오버리지 크레딧 조회 (없으면 0)
|
|
SELECT overage_credits INTO v_overage
|
|
FROM public.subscriptions
|
|
WHERE user_id = p_user_id;
|
|
v_overage := COALESCE(v_overage, 0);
|
|
|
|
-- 무제한(-1): 그냥 카운터만 증가
|
|
IF p_base_limit = -1 THEN
|
|
INSERT INTO public.daily_usage (user_id, date, feature, count)
|
|
VALUES (p_user_id, CURRENT_DATE, p_feature, 1)
|
|
ON CONFLICT (user_id, date, feature) DO UPDATE
|
|
SET count = public.daily_usage.count + 1
|
|
RETURNING count INTO v_new_count;
|
|
RETURN jsonb_build_object(
|
|
'allowed', true,
|
|
'current', v_new_count,
|
|
'limit', -1,
|
|
'overage_credits', v_overage,
|
|
'consumed_from', 'unlimited'
|
|
);
|
|
END IF;
|
|
|
|
-- base 잔여 여부 체크
|
|
IF v_current < p_base_limit THEN
|
|
-- base 소비
|
|
INSERT INTO public.daily_usage (user_id, date, feature, count)
|
|
VALUES (p_user_id, CURRENT_DATE, p_feature, 1)
|
|
ON CONFLICT (user_id, date, feature) DO UPDATE
|
|
SET count = public.daily_usage.count + 1
|
|
RETURNING count INTO v_new_count;
|
|
RETURN jsonb_build_object(
|
|
'allowed', true,
|
|
'current', v_new_count,
|
|
'limit', p_base_limit,
|
|
'overage_credits', v_overage,
|
|
'consumed_from', 'base'
|
|
);
|
|
END IF;
|
|
|
|
-- base 소진 → 오버리지 체크
|
|
IF v_overage <= 0 THEN
|
|
RETURN jsonb_build_object(
|
|
'allowed', false,
|
|
'current', v_current,
|
|
'limit', p_base_limit,
|
|
'overage_credits', 0,
|
|
'consumed_from', 'none'
|
|
);
|
|
END IF;
|
|
|
|
-- 오버리지 소비 (daily_usage 증가 + overage_credits 감소)
|
|
UPDATE public.subscriptions
|
|
SET overage_credits = overage_credits - 1,
|
|
updated_at = now()
|
|
WHERE user_id = p_user_id
|
|
RETURNING overage_credits INTO v_new_overage;
|
|
|
|
INSERT INTO public.daily_usage (user_id, date, feature, count)
|
|
VALUES (p_user_id, CURRENT_DATE, p_feature, 1)
|
|
ON CONFLICT (user_id, date, feature) DO UPDATE
|
|
SET count = public.daily_usage.count + 1
|
|
RETURNING count INTO v_new_count;
|
|
|
|
RETURN jsonb_build_object(
|
|
'allowed', true,
|
|
'current', v_new_count,
|
|
'limit', p_base_limit,
|
|
'overage_credits', v_new_overage,
|
|
'consumed_from', 'overage'
|
|
);
|
|
END;
|
|
$$;
|
|
|
|
COMMENT ON FUNCTION public.consume_quota(uuid, text, integer) IS
|
|
'원자적 쿼터 소비: base 먼저 → 소진 시 overage. Phase 3.2 llm-proxy 전용.';
|
|
|
|
-- RPC는 service_role만 호출 가능
|
|
REVOKE ALL ON FUNCTION public.consume_quota(uuid, text, integer) FROM public;
|
|
GRANT EXECUTE ON FUNCTION public.consume_quota(uuid, text, integer) TO service_role;
|