From 12c135ebd83de7f9e4df53192d78f90bf0bd0392 Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 20 Mar 2026 02:13:04 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20purchase/analysis=20API=EB=A5=BC=20NAS?= =?UTF-8?q?=20=E2=86=92=20Supabase=EB=A1=9C=20=EC=9E=AC=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 원인: purchase/personal은 유저별 데이터인데 NAS로 프록시 → NAS가 userId 모름 - ConnectTimeoutError는 NAS에 미구현 엔드포인트로 연결 시도한 결과 purchase/route.ts: nasGet/nasPost → Supabase lotto_purchases CRUD purchase/stats/route.ts: nasGet → Supabase 집계 (총구매/당첨금/순손익 계산) purchase/[id]/route.ts: nasPut/nasDelete → Supabase UPDATE/DELETE (user_id RLS) analysis/personal/route.ts: nasGet → lotto_history 테이블 직접 분석 (번호 빈도, top/least picks, 홀짝패턴, 구간 분포, 당첨평균 대비) Co-Authored-By: Claude Sonnet 4.6 --- app/api/lotto/analysis/personal/route.ts | 94 +++++++++++++++++++++++- app/api/lotto/purchase/[id]/route.ts | 39 ++++++++-- app/api/lotto/purchase/route.ts | 52 ++++++++++--- app/api/lotto/purchase/stats/route.ts | 34 ++++++++- 4 files changed, 195 insertions(+), 24 deletions(-) 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 }); + } }