feat: 로또 서비스 Phase 1-2 프론트엔드 고도화
- NAS 프록시 공통 헬퍼 (_nas.ts): nasGet/Post/Put/Delete + requireSubscription - API 라우트 7개: stats/performance, report/latest, report/history, analysis/personal, purchase CRUD - ReportTab: 주간 공략 리포트 (신뢰도, 추천 세트, 핫/콜드 번호, 히스토리) - PurchaseTab: 구매 기록 CRUD + 투자 통계 (총구매/당첨금/순손익/최대당첨) - PatternTab: 개인 번호 패턴 분석 (자주 선택/기피 번호, 구간 분포) - 성과 배너: 실제 검증 통계 (3개 이상 일치율, 평균 일치 개수, 무작위 대비 개선율) - 탭 네비게이션: 구독자 전용 (번호 생성/이번 주 공략/구매 기록/내 패턴) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
86
app/api/lotto/_nas.ts
Normal file
86
app/api/lotto/_nas.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
|
||||
const LOTTO_PRODUCT_IDS = ['lotto_gold', 'lotto_platinum', 'lotto_diamond', 'lotto_annual'];
|
||||
|
||||
function nasHeaders() {
|
||||
const h: Record<string, string> = {};
|
||||
if (process.env.NAS_LOTTO_API_KEY) h['Authorization'] = `Bearer ${process.env.NAS_LOTTO_API_KEY}`;
|
||||
return h;
|
||||
}
|
||||
|
||||
function nasBase() {
|
||||
const base = process.env.NAS_LOTTO_API_URL;
|
||||
if (!base) throw new Error('NAS_URL_NOT_CONFIGURED');
|
||||
return base;
|
||||
}
|
||||
|
||||
export async function nasGet(path: string): Promise<unknown> {
|
||||
const res = await fetch(`${nasBase()}${path}`, {
|
||||
headers: nasHeaders(), signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`NAS_${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function nasPost(path: string, body: unknown): Promise<unknown> {
|
||||
const res = await fetch(`${nasBase()}${path}`, {
|
||||
method: 'POST',
|
||||
headers: { ...nasHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`NAS_${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function nasPut(path: string, body: unknown): Promise<unknown> {
|
||||
const res = await fetch(`${nasBase()}${path}`, {
|
||||
method: 'PUT',
|
||||
headers: { ...nasHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`NAS_${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function nasDelete(path: string): Promise<unknown> {
|
||||
const res = await fetch(`${nasBase()}${path}`, {
|
||||
method: 'DELETE', headers: nasHeaders(), signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`NAS_${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface AuthResult { userId: string; plan: string; }
|
||||
|
||||
export async function requireSubscription(): Promise<AuthResult | NextResponse> {
|
||||
const supabase = await createClient();
|
||||
const { data: { user }, error } = await supabase.auth.getUser();
|
||||
if (error || !user) return NextResponse.json({ error: 'UNAUTHORIZED' }, { status: 401 });
|
||||
|
||||
const { data: sub } = await supabase
|
||||
.from('subscriptions').select('product_id')
|
||||
.eq('user_id', user.id).eq('status', 'active')
|
||||
.in('product_id', LOTTO_PRODUCT_IDS).maybeSingle();
|
||||
if (sub) return { userId: user.id, plan: sub.product_id };
|
||||
|
||||
const ago31 = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000).toISOString();
|
||||
const { data: order } = await supabase
|
||||
.from('orders').select('product_id')
|
||||
.eq('user_id', user.id).eq('status', 'paid')
|
||||
.in('product_id', LOTTO_PRODUCT_IDS)
|
||||
.gte('created_at', ago31)
|
||||
.order('created_at', { ascending: false }).limit(1).maybeSingle();
|
||||
if (order) return { userId: user.id, plan: order.product_id };
|
||||
|
||||
return NextResponse.json({ error: 'NOT_SUBSCRIBED' }, { status: 403 });
|
||||
}
|
||||
|
||||
export function handleNasError(err: unknown): NextResponse {
|
||||
const e = err as { name?: string };
|
||||
if (e?.name === 'TimeoutError') return NextResponse.json({ error: 'NAS_TIMEOUT' }, { status: 504 });
|
||||
console.error('[NAS]', err);
|
||||
return NextResponse.json({ error: 'INTERNAL_ERROR' }, { status: 500 });
|
||||
}
|
||||
11
app/api/lotto/analysis/personal/route.ts
Normal file
11
app/api/lotto/analysis/personal/route.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { nasGet, requireSubscription, handleNasError } from '../../_nas';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const data = await nasGet('/api/lotto/analysis/personal');
|
||||
return NextResponse.json(data);
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
23
app/api/lotto/purchase/[id]/route.ts
Normal file
23
app/api/lotto/purchase/[id]/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { nasPut, nasDelete, requireSubscription, handleNasError } from '../../_nas';
|
||||
|
||||
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const data = await nasPut(`/api/lotto/purchase/${id}`, body);
|
||||
return NextResponse.json(data);
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
|
||||
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const { id } = await params;
|
||||
const data = await nasDelete(`/api/lotto/purchase/${id}`);
|
||||
return NextResponse.json(data);
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
26
app/api/lotto/purchase/route.ts
Normal file
26
app/api/lotto/purchase/route.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { nasGet, nasPost, requireSubscription, handleNasError } from '../_nas';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const params = new URLSearchParams();
|
||||
if (searchParams.get('draw_no')) params.set('draw_no', searchParams.get('draw_no')!);
|
||||
if (searchParams.get('days')) params.set('days', searchParams.get('days')!);
|
||||
const qs = params.toString() ? `?${params}` : '';
|
||||
const data = await nasGet(`/api/lotto/purchase${qs}`);
|
||||
return NextResponse.json(data);
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const body = await request.json();
|
||||
const data = await nasPost('/api/lotto/purchase', body);
|
||||
return NextResponse.json(data, { status: 201 });
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
11
app/api/lotto/purchase/stats/route.ts
Normal file
11
app/api/lotto/purchase/stats/route.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { nasGet, requireSubscription, handleNasError } from '../../_nas';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const data = await nasGet('/api/lotto/purchase/stats');
|
||||
return NextResponse.json(data);
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
13
app/api/lotto/report/history/route.ts
Normal file
13
app/api/lotto/report/history/route.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { nasGet, requireSubscription, handleNasError } from '../../_nas';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const limit = searchParams.get('limit') ?? '10';
|
||||
const data = await nasGet(`/api/lotto/report/history?limit=${limit}`);
|
||||
return NextResponse.json(data);
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
11
app/api/lotto/report/latest/route.ts
Normal file
11
app/api/lotto/report/latest/route.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { nasGet, requireSubscription, handleNasError } from '../../_nas';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const data = await nasGet('/api/lotto/report/latest');
|
||||
return NextResponse.json(data);
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
13
app/api/lotto/stats/performance/route.ts
Normal file
13
app/api/lotto/stats/performance/route.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { nasGet, handleNasError } from '../../_nas';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return NextResponse.json({ error: 'UNAUTHORIZED' }, { status: 401 });
|
||||
const data = await nasGet('/api/lotto/stats/performance');
|
||||
return NextResponse.json(data);
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
Reference in New Issue
Block a user