From 84b36267bf6a52cb6f46a841a89f5db4662ea487 Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 2 Jul 2026 20:46:35 +0900 Subject: [PATCH] =?UTF-8?q?feat(phase2):=20=EC=9D=BC=EC=9D=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=9F=89=20=EC=9C=A0=ED=8B=B8(KST)=20+=20tarot=5Fread?= =?UTF-8?q?ings=C2=B7ai=5Fusage=5Flog=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - kstDayStartISO: KST 자정을 UTC ISO로 변환 - getTodayUsage, recordUsage: AI 사용량 조회·기록 - DB: tarot_readings, ai_usage_log 테이블 생성 - saju service_settings 삭제 (숨김 해제) Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01AAtcmKKtqDUe4NyVgy1aLQ --- lib/__tests__/ai-usage.test.ts | 17 +++++++++++ lib/ai-usage.ts | 28 +++++++++++++++++++ .../2026-07-02-phase2-saju-tarot.sql | 27 ++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 lib/__tests__/ai-usage.test.ts create mode 100644 lib/ai-usage.ts create mode 100644 supabase/migrations/2026-07-02-phase2-saju-tarot.sql diff --git a/lib/__tests__/ai-usage.test.ts b/lib/__tests__/ai-usage.test.ts new file mode 100644 index 0000000..0e1419f --- /dev/null +++ b/lib/__tests__/ai-usage.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; +import { kstDayStartISO, SAJU_DAILY_LIMIT, TAROT_DAILY_LIMIT } from '../ai-usage'; + +describe('kstDayStartISO', () => { + it('KST 자정을 UTC로 환산한다 (KST 15:00 UTC = 당일 00:00 KST)', () => { + // 2026-07-02T05:00:00Z = 2026-07-02 14:00 KST → 그날 KST 자정 = 2026-07-01T15:00:00Z + expect(kstDayStartISO(new Date('2026-07-02T05:00:00Z'))).toBe('2026-07-01T15:00:00.000Z'); + }); + it('KST 자정 직후도 같은 날로 계산한다', () => { + // 2026-07-01T15:30:00Z = 2026-07-02 00:30 KST → KST 자정 = 2026-07-01T15:00:00Z + expect(kstDayStartISO(new Date('2026-07-01T15:30:00Z'))).toBe('2026-07-01T15:00:00.000Z'); + }); + it('제한 상수', () => { + expect(SAJU_DAILY_LIMIT).toBe(1); + expect(TAROT_DAILY_LIMIT).toBe(3); + }); +}); diff --git a/lib/ai-usage.ts b/lib/ai-usage.ts new file mode 100644 index 0000000..cba8c1b --- /dev/null +++ b/lib/ai-usage.ts @@ -0,0 +1,28 @@ +import type { SupabaseClient } from '@supabase/supabase-js'; + +export const SAJU_DAILY_LIMIT = 1; +export const TAROT_DAILY_LIMIT = 3; +export type AiService = 'saju' | 'tarot'; + +/** KST(UTC+9) 자정을 UTC ISO로. 오늘 사용량 집계 하한. */ +export function kstDayStartISO(now: Date): string { + const kstMs = now.getTime() + 9 * 60 * 60 * 1000; + const kst = new Date(kstMs); + const kstMidnightUtcMs = Date.UTC(kst.getUTCFullYear(), kst.getUTCMonth(), kst.getUTCDate()) - 9 * 60 * 60 * 1000; + return new Date(kstMidnightUtcMs).toISOString(); +} + +export async function getTodayUsage(admin: SupabaseClient, userId: string, service: AiService): Promise { + const since = kstDayStartISO(new Date()); + const { count } = await admin + .from('ai_usage_log') + .select('id', { count: 'exact', head: true }) + .eq('user_id', userId) + .eq('service', service) + .gte('created_at', since); + return count ?? 0; +} + +export async function recordUsage(admin: SupabaseClient, userId: string, service: AiService): Promise { + await admin.from('ai_usage_log').insert({ user_id: userId, service }); +} diff --git a/supabase/migrations/2026-07-02-phase2-saju-tarot.sql b/supabase/migrations/2026-07-02-phase2-saju-tarot.sql new file mode 100644 index 0000000..b398834 --- /dev/null +++ b/supabase/migrations/2026-07-02-phase2-saju-tarot.sql @@ -0,0 +1,27 @@ +-- Phase 2 (2026-07-02): 타로 저장·AI 사용량 로그 + 사주 숨김 해제 +-- 적용: 클라우드 Supabase + NAS self-host 양쪽 + +CREATE TABLE IF NOT EXISTS tarot_readings ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + spread_type text NOT NULL DEFAULT 'three_card', + category text, + question text, + cards jsonb NOT NULL, + interpretation jsonb NOT NULL, + summary text, + created_at timestamptz NOT NULL DEFAULT now() +); +ALTER TABLE tarot_readings ENABLE ROW LEVEL SECURITY; +CREATE POLICY tarot_select_own ON tarot_readings FOR SELECT USING (auth.uid() = user_id); + +CREATE TABLE IF NOT EXISTS ai_usage_log ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL, + service text NOT NULL CHECK (service IN ('saju','tarot')), + created_at timestamptz NOT NULL DEFAULT now() +); +ALTER TABLE ai_usage_log ENABLE ROW LEVEL SECURITY; +CREATE INDEX IF NOT EXISTS idx_ai_usage_user_day ON ai_usage_log (user_id, service, created_at); + +DELETE FROM service_settings WHERE id = 'saju';