feat(phase2): 일일 사용량 유틸(KST) + tarot_readings·ai_usage_log 마이그레이션
- kstDayStartISO: KST 자정을 UTC ISO로 변환 - getTodayUsage, recordUsage: AI 사용량 조회·기록 - DB: tarot_readings, ai_usage_log 테이블 생성 - saju service_settings 삭제 (숨김 해제) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AAtcmKKtqDUe4NyVgy1aLQ
This commit is contained in:
17
lib/__tests__/ai-usage.test.ts
Normal file
17
lib/__tests__/ai-usage.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
28
lib/ai-usage.ts
Normal file
28
lib/ai-usage.ts
Normal file
@@ -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<number> {
|
||||||
|
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<void> {
|
||||||
|
await admin.from('ai_usage_log').insert({ user_id: userId, service });
|
||||||
|
}
|
||||||
27
supabase/migrations/2026-07-02-phase2-saju-tarot.sql
Normal file
27
supabase/migrations/2026-07-02-phase2-saju-tarot.sql
Normal file
@@ -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';
|
||||||
Reference in New Issue
Block a user