diff --git a/app/api/admin/services/route.ts b/app/api/admin/services/route.ts index b81f124..79496d6 100644 --- a/app/api/admin/services/route.ts +++ b/app/api/admin/services/route.ts @@ -51,7 +51,6 @@ export async function PATCH(request: Request) { } const DEFAULT_SERVICES = [ - { id: 'saju', name: 'AI 사주 분석', description: '사주 입력 및 AI 해석 (레거시)', is_active: false, order_index: 101 }, { id: 'music', name: 'AI 음악 팩', description: '음악 가이드 패키지·샘플·스튜디오', is_active: false, order_index: 102 }, { id: 'gyeol', name: 'CONTOUR 설문', description: '/gyeol PMF 설문', is_active: false, order_index: 103 }, { id: 'lotto', name: '로또 추천', description: '로또 번호 추천 노출', is_active: false, order_index: 105 }, diff --git a/app/api/saju/analyze/route.ts b/app/api/saju/analyze/route.ts index 9fb516d..7355338 100644 --- a/app/api/saju/analyze/route.ts +++ b/app/api/saju/analyze/route.ts @@ -64,29 +64,23 @@ const MODELS = [ export async function POST(request: Request) { try { - // ── 결제 사용자 인증 (Gemini API 무단 호출 방지) ────────── + // ── 로그인 인증 + 서버측 일일 사용량 제한 (Gemini API 무단 호출 방지) ────────── const { createClient } = await import('@/lib/supabase/server'); const supabase = await createClient(); const { data: { user } } = await supabase.auth.getUser(); - if (user) { - // 로그인된 경우: saju_detail 결제 여부 확인 - const { data: paidOrder } = await supabase - .from('orders') - .select('id') - .eq('user_id', user.id) - .eq('product_id', 'saju_detail') - .eq('status', 'paid') - .maybeSingle(); - - if (!paidOrder) { - return NextResponse.json({ error: '사주 리포트를 구매한 사용자만 이용할 수 있습니다' }, { status: 403 }); - } - } else { + if (!user) { // 비로그인 사용자는 AI 호출 불가 return NextResponse.json({ error: '로그인이 필요합니다' }, { status: 401 }); } + const { createAdminClient } = await import('@/lib/supabase/admin'); + const { getTodayUsage, recordUsage, SAJU_DAILY_LIMIT } = await import('@/lib/ai-usage'); + const admin = createAdminClient(); + if ((await getTodayUsage(admin, user.id, 'saju')) >= SAJU_DAILY_LIMIT) { + return NextResponse.json({ error: `오늘 AI 사주 해석을 모두 사용했습니다. (${SAJU_DAILY_LIMIT}회/일)` }, { status: 429 }); + } + // ── 입력 길이 검증 (DoS / 프롬프트 인젝션 기초 방어) ────── const raw = await request.json(); if (JSON.stringify(raw).length > 50_000) { @@ -182,6 +176,9 @@ export async function POST(request: Request) { return NextResponse.json({ interpretation: MOCK_INTERPRETATION, analysis }); } + // 실제 Gemini 해석 성공 시에만 일일 사용량 카운트 (MOCK 폴백 경로는 카운트하지 않음) + await recordUsage(admin, user.id, 'saju'); + return NextResponse.json({ interpretation, analysis }); } catch (error: any) { diff --git a/app/work/saju/layout.tsx b/app/work/saju/layout.tsx index 11e1511..6444b00 100644 --- a/app/work/saju/layout.tsx +++ b/app/work/saju/layout.tsx @@ -1,6 +1,4 @@ -import { notFound } from 'next/navigation'; import type { Metadata } from 'next'; -import { isServiceVisible } from '@/lib/service-visibility'; export const metadata: Metadata = { title: 'AI 사주 분석', @@ -24,7 +22,6 @@ export const metadata: Metadata = { }, }; -export default async function SajuLayout({ children }: { children: React.ReactNode }) { - if (!(await isServiceVisible('saju'))) notFound(); +export default function SajuLayout({ children }: { children: React.ReactNode }) { return <>{children}; } diff --git a/lib/service-visibility.ts b/lib/service-visibility.ts index d1ff675..5547478 100644 --- a/lib/service-visibility.ts +++ b/lib/service-visibility.ts @@ -3,7 +3,7 @@ import { createAdminClient } from '@/lib/supabase/admin'; import { verifyAdminTokenNode } from '@/lib/admin-auth'; /** 숨김 가능 서비스 id (service_settings.id와 일치) */ -export type HideableService = 'saju' | 'music' | 'gyeol' | 'lotto'; +export type HideableService = 'music' | 'gyeol' | 'lotto'; /** * 서비스 노출 여부. admin_token 세션이면 항상 true.