diff --git a/app/api/lotto/analysis/personal/route.ts b/app/api/lotto/analysis/personal/route.ts index cc740c9..ecea060 100644 --- a/app/api/lotto/analysis/personal/route.ts +++ b/app/api/lotto/analysis/personal/route.ts @@ -1,11 +1,97 @@ import { NextResponse } from 'next/server'; -import { nasGet, requireSubscription, handleNasError } from '../../_nas'; +import { createClient } from '@/lib/supabase/server'; +import { requireSubscription } from '../../_nas'; + +const DRAW_AVG_ODD = 3.0; // 로또 실제 홀수 평균 +const DRAW_AVG_SUM = 138; // 로또 실제 합계 평균 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); } + + const supabase = await createClient(); + const { data, error } = await supabase + .from('lotto_history') + .select('numbers') + .eq('user_id', auth.userId) + .order('created_at', { ascending: false }) + .limit(500); + + if (error) throw error; + + const rows = data ?? []; + if (rows.length === 0) { + return NextResponse.json({ + total_analyzed: 0, + number_frequency: {}, + top_picks: [], + least_picks: [], + pattern: { avg_odd_count: 0, avg_sum: 0, avg_range: 0, consecutive_rate: 0, zone_avg: {} }, + vs_draw_avg: { odd_diff: 0, sum_diff: 0, odd_tendency: '보통', sum_tendency: '보통' }, + }); + } + + // 번호 빈도 집계 + const freq: Record = {}; + for (let i = 1; i <= 45; i++) freq[i] = 0; + + let totalOdd = 0, totalSum = 0, totalRange = 0, consecutiveCount = 0; + const zoneCounts: Record = { '1-10': 0, '11-20': 0, '21-30': 0, '31-40': 0, '41-45': 0 }; + + for (const row of rows) { + const nums: number[] = Array.isArray(row.numbers) ? row.numbers : []; + const sorted = [...nums].sort((a, b) => a - b); + + for (const n of sorted) { + freq[n] = (freq[n] ?? 0) + 1; + if (n <= 10) zoneCounts['1-10']++; + else if (n <= 20) zoneCounts['11-20']++; + else if (n <= 30) zoneCounts['21-30']++; + else if (n <= 40) zoneCounts['31-40']++; + else zoneCounts['41-45']++; + } + + totalOdd += sorted.filter(n => n % 2 !== 0).length; + totalSum += sorted.reduce((a, b) => a + b, 0); + totalRange += sorted.length >= 2 ? sorted[sorted.length - 1] - sorted[0] : 0; + if (sorted.some((n, i) => i > 0 && n === sorted[i - 1] + 1)) consecutiveCount++; + } + + const n = rows.length; + const avgOdd = totalOdd / n; + const avgSum = totalSum / n; + + const freqEntries = Object.entries(freq) + .map(([num, cnt]) => ({ num: parseInt(num), cnt })) + .sort((a, b) => b.cnt - a.cnt); + + const zoneAvg: Record = {}; + for (const [zone, count] of Object.entries(zoneCounts)) { + zoneAvg[zone] = Math.round((count / n) * 10) / 10; + } + + return NextResponse.json({ + total_analyzed: n, + number_frequency: freq, + top_picks: freqEntries.slice(0, 10).map(e => e.num), + least_picks: freqEntries.slice(-10).map(e => e.num).reverse(), + pattern: { + avg_odd_count: Math.round(avgOdd * 10) / 10, + avg_sum: Math.round(avgSum), + avg_range: Math.round(totalRange / n), + consecutive_rate: Math.round((consecutiveCount / n) * 100) / 100, + zone_avg: zoneAvg, + }, + vs_draw_avg: { + odd_diff: Math.round((avgOdd - DRAW_AVG_ODD) * 10) / 10, + sum_diff: Math.round(avgSum - DRAW_AVG_SUM), + odd_tendency: avgOdd > DRAW_AVG_ODD + 0.3 ? '홀수 선호' : avgOdd < DRAW_AVG_ODD - 0.3 ? '짝수 선호' : '보통', + sum_tendency: avgSum > DRAW_AVG_SUM + 10 ? '고합계 선호' : avgSum < DRAW_AVG_SUM - 10 ? '저합계 선호' : '보통', + }, + }); + } catch (err) { + console.error('[analysis/personal]', err); + return NextResponse.json({ error: 'DB_ERROR' }, { status: 500 }); + } } diff --git a/app/api/lotto/purchase/[id]/route.ts b/app/api/lotto/purchase/[id]/route.ts index 641f2eb..adaf063 100644 --- a/app/api/lotto/purchase/[id]/route.ts +++ b/app/api/lotto/purchase/[id]/route.ts @@ -1,23 +1,50 @@ import { NextResponse } from 'next/server'; -import { nasPut, nasDelete, requireSubscription, handleNasError } from '../../_nas'; +import { createClient } from '@/lib/supabase/server'; +import { requireSubscription } 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 supabase = await createClient(); const { id } = await params; const body = await request.json(); - const data = await nasPut(`/api/lotto/purchase/${id}`, body); + + const { data, error } = await supabase + .from('lotto_purchases') + .update({ prize: body.prize, note: body.note }) + .eq('id', parseInt(id)) + .eq('user_id', auth.userId) // 본인 데이터만 수정 + .select() + .single(); + + if (error) throw error; return NextResponse.json(data); - } catch (err) { return handleNasError(err); } + } catch (err) { + console.error('[purchase PUT]', err); + return NextResponse.json({ error: 'DB_ERROR' }, { status: 500 }); + } } export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) { try { const auth = await requireSubscription(); if (auth instanceof NextResponse) return auth; + + const supabase = await createClient(); const { id } = await params; - const data = await nasDelete(`/api/lotto/purchase/${id}`); - return NextResponse.json(data); - } catch (err) { return handleNasError(err); } + + const { error } = await supabase + .from('lotto_purchases') + .delete() + .eq('id', parseInt(id)) + .eq('user_id', auth.userId); // 본인 데이터만 삭제 + + if (error) throw error; + return NextResponse.json({ ok: true }); + } catch (err) { + console.error('[purchase DELETE]', err); + return NextResponse.json({ error: 'DB_ERROR' }, { status: 500 }); + } } diff --git a/app/api/lotto/purchase/route.ts b/app/api/lotto/purchase/route.ts index bee820c..0a74ccc 100644 --- a/app/api/lotto/purchase/route.ts +++ b/app/api/lotto/purchase/route.ts @@ -1,26 +1,58 @@ import { NextResponse } from 'next/server'; -import { nasGet, nasPost, requireSubscription, handleNasError } from '../_nas'; +import { createClient } from '@/lib/supabase/server'; +import { requireSubscription } from '../_nas'; export async function GET(request: Request) { try { const auth = await requireSubscription(); if (auth instanceof NextResponse) return auth; + + const supabase = await createClient(); 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); } + const drawNo = searchParams.get('draw_no'); + + let query = supabase + .from('lotto_purchases') + .select('id, draw_no, amount, sets, prize, note, created_at') + .eq('user_id', auth.userId) + .order('draw_no', { ascending: false }); + + if (drawNo) query = query.eq('draw_no', parseInt(drawNo)); + + const { data, error } = await query; + if (error) throw error; + return NextResponse.json({ records: data ?? [] }); + } catch (err) { + console.error('[purchase GET]', err); + return NextResponse.json({ error: 'DB_ERROR' }, { status: 500 }); + } } export async function POST(request: Request) { try { const auth = await requireSubscription(); if (auth instanceof NextResponse) return auth; + + const supabase = await createClient(); const body = await request.json(); - const data = await nasPost('/api/lotto/purchase', body); + + const { data, error } = await supabase + .from('lotto_purchases') + .insert({ + user_id: auth.userId, + draw_no: body.draw_no, + amount: body.amount ?? 5000, + sets: body.sets ?? 5, + prize: body.prize ?? 0, + note: body.note ?? '', + }) + .select() + .single(); + + if (error) throw error; return NextResponse.json(data, { status: 201 }); - } catch (err) { return handleNasError(err); } + } catch (err) { + console.error('[purchase POST]', err); + return NextResponse.json({ error: 'DB_ERROR' }, { status: 500 }); + } } diff --git a/app/api/lotto/purchase/stats/route.ts b/app/api/lotto/purchase/stats/route.ts index cc93d55..14dc019 100644 --- a/app/api/lotto/purchase/stats/route.ts +++ b/app/api/lotto/purchase/stats/route.ts @@ -1,11 +1,37 @@ import { NextResponse } from 'next/server'; -import { nasGet, requireSubscription, handleNasError } from '../../_nas'; +import { createClient } from '@/lib/supabase/server'; +import { requireSubscription } 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); } + + const supabase = await createClient(); + const { data, error } = await supabase + .from('lotto_purchases') + .select('amount, prize') + .eq('user_id', auth.userId); + + if (error) throw error; + + const records = data ?? []; + const total_invested = records.reduce((s, r) => s + (r.amount ?? 0), 0); + const total_prize = records.reduce((s, r) => s + (r.prize ?? 0), 0); + const prize_count = records.filter(r => (r.prize ?? 0) > 0).length; + const max_prize = records.reduce((m, r) => Math.max(m, r.prize ?? 0), 0); + + return NextResponse.json({ + total_records: records.length, + total_invested, + total_prize, + net: total_prize - total_invested, + return_rate: total_invested > 0 ? Math.round((total_prize / total_invested) * 1000) / 10 : 0, + prize_count, + max_prize, + }); + } catch (err) { + console.error('[purchase/stats]', err); + return NextResponse.json({ error: 'DB_ERROR' }, { status: 500 }); + } }