server/supabase/ 신규 디렉토리:
config.toml — Supabase CLI 설정
- 프로젝트 ID, DB 포트, auth providers (Google/GitHub/Apple),
edge_runtime, functions.*.verify_jwt 설정
migrations/ (PostgreSQL DDL, 시간순):
- 20260409000001_initial_schema.sql
12개 테이블: profiles, teams, team_members, meetings,
meeting_memos, meeting_documents, transcripts, history,
dictionary, memo_tags, daily_usage, subscriptions
+ 인덱스 + FK CASCADE + transcripts Realtime publication
- 20260409000002_rls_policies.sql
개인 전용(history/dictionary): user_id = auth.uid()
팀 공유(meetings 등): 본인 OR team_members 조회 subquery
daily_usage: 읽기만, 쓰기는 service_role RPC
- 20260409000003_auth_triggers.sql
handle_new_user — auth.users INSERT → profiles+subscriptions 자동 생성
moddatetime — 8개 테이블 updated_at 자동 갱신
increment_daily_usage — service_role 전용 쿼터 RPC
- 20260409000004_storage_buckets.sql
audio/exports/avatars 3개 버킷 + 경로 기반 접근 정책
(파일 경로가 {user_id}/...로 시작해야 쓰기 허용)
functions/ (Deno/TypeScript Edge Functions):
- _shared/cors.ts — CORS 헤더 + preflight 핸들러
- _shared/auth.ts — requireUser (JWT 검증 + User 반환)
- _shared/quota.ts — 티어별 쿼터 체크 + consume + service role client
- stt-proxy/index.ts — Google Cloud STT 래퍼 스캐폴딩
placeholder 응답, 실제 API 호출 코드는 주석으로 포함
- llm-proxy/index.ts — Anthropic Messages API 래퍼 스캐폴딩
티어별 허용 모델 정책 (free=Haiku, pro=Sonnet, team=Opus)
설계 문서:
- docs/v2/phase-V2-2.md — 상세 설계 (스키마/RLS/Edge Functions/Realtime)
- docs/v2/phase-V2-2-setup.md — 사용자 액션 가이드 (Supabase 계정/
OAuth 등록/CLI/배포/검증)
apps/desktop typecheck 통과 (server/는 Deno 런타임이라 별도).
실제 Supabase 프로젝트 배포는 사용자가 phase-V2-2-setup.md 따라 수행.
108 lines
2.7 KiB
TypeScript
108 lines
2.7 KiB
TypeScript
// server/supabase/functions/_shared/quota.ts
|
|
// 티어별 기능 쿼터 확인 + 증가
|
|
|
|
// @ts-expect-error — Deno 런타임 import
|
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.7'
|
|
|
|
export type Tier = 'free' | 'pro' | 'team'
|
|
export type Feature = 'stt_transcribe' | 'llm_process'
|
|
|
|
/** 일일 쿼터 정책 (-1 = 무제한) */
|
|
const DAILY_QUOTA: Record<Tier, Record<Feature, number>> = {
|
|
free: {
|
|
stt_transcribe: 50,
|
|
llm_process: 50
|
|
},
|
|
pro: {
|
|
stt_transcribe: -1,
|
|
llm_process: -1
|
|
},
|
|
team: {
|
|
stt_transcribe: -1,
|
|
llm_process: -1
|
|
}
|
|
}
|
|
|
|
export interface QuotaCheck {
|
|
allowed: boolean
|
|
current: number
|
|
limit: number
|
|
tier: Tier
|
|
}
|
|
|
|
/**
|
|
* 유저의 오늘 사용량을 확인하고 쿼터 초과 여부를 반환.
|
|
* 실제 증가는 performQuotaConsume 호출 시 수행.
|
|
*/
|
|
export async function checkQuota(
|
|
userId: string,
|
|
feature: Feature,
|
|
serviceRoleClient: ReturnType<typeof createClient>
|
|
): Promise<QuotaCheck> {
|
|
// 티어 조회
|
|
const { data: sub } = await serviceRoleClient
|
|
.from('subscriptions')
|
|
.select('tier')
|
|
.eq('user_id', userId)
|
|
.single()
|
|
|
|
const tier: Tier = (sub?.tier as Tier) ?? 'free'
|
|
const limit = DAILY_QUOTA[tier][feature]
|
|
|
|
if (limit === -1) {
|
|
return { allowed: true, current: 0, limit, tier }
|
|
}
|
|
|
|
// 오늘 사용량 조회
|
|
const today = new Date().toISOString().slice(0, 10)
|
|
const { data: usage } = await serviceRoleClient
|
|
.from('daily_usage')
|
|
.select('count')
|
|
.eq('user_id', userId)
|
|
.eq('date', today)
|
|
.eq('feature', feature)
|
|
.maybeSingle()
|
|
|
|
const current = (usage?.count as number) ?? 0
|
|
return {
|
|
allowed: current < limit,
|
|
current,
|
|
limit,
|
|
tier
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 쿼터 소비. increment_daily_usage 함수 호출 (service_role 전용).
|
|
*/
|
|
export async function consumeQuota(
|
|
userId: string,
|
|
feature: Feature,
|
|
serviceRoleClient: ReturnType<typeof createClient>,
|
|
amount: number = 1
|
|
): Promise<number> {
|
|
const { data, error } = await serviceRoleClient.rpc('increment_daily_usage', {
|
|
p_user_id: userId,
|
|
p_feature: feature,
|
|
p_amount: amount
|
|
})
|
|
|
|
if (error) {
|
|
throw new Error(`Failed to increment quota: ${error.message}`)
|
|
}
|
|
|
|
return (data as number) ?? 0
|
|
}
|
|
|
|
/**
|
|
* service role 클라이언트 생성 헬퍼
|
|
*/
|
|
export function createServiceRoleClient(): ReturnType<typeof createClient> {
|
|
// @ts-expect-error — Deno.env는 Deno 런타임 전역
|
|
const url = Deno.env.get('SUPABASE_URL') ?? ''
|
|
// @ts-expect-error — Deno.env는 Deno 런타임 전역
|
|
const serviceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
|
return createClient(url, serviceKey, {
|
|
auth: { persistSession: false, autoRefreshToken: false }
|
|
})
|
|
}
|