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:
2026-03-20 01:12:59 +09:00
parent b306b0e42c
commit 4cacea69c8
12 changed files with 937 additions and 0 deletions

View File

@@ -2,6 +2,9 @@
import { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import ReportTab from './ReportTab';
import PurchaseTab from './PurchaseTab';
import PatternTab from './PatternTab';
import { createClient } from '@/lib/supabase/client';
// ─── 전략 타입 ────────────────────────────────────────────────────────────────
@@ -299,6 +302,8 @@ export default function LottoRecommendPage() {
const [combos, setCombos] = useState<Combo[]>([]);
const [proState, setProState] = useState<'idle' | 'loading' | 'result' | 'error'>('idle');
const [proError, setProError] = useState('');
const [activeTab, setActiveTab] = useState<'generate' | 'report' | 'purchase' | 'pattern'>('generate');
const [perfStats, setPerfStats] = useState<{ total_checked: number; avg_correct: number; rate_3plus: number; vs_random: { improvement_pct: number } } | null>(null);
const idRef = useRef(0);
const MAX_COMBOS = PLAN_MAX_COMBOS[plan] ?? 5;
@@ -318,6 +323,11 @@ export default function LottoRecommendPage() {
}
} catch { /* ignore */ }
}
// 성과 통계 (구독 여부 무관 로드 시도)
try {
const perfRes = await fetch('/api/lotto/stats/performance');
if (perfRes.ok) { const p = await perfRes.json(); setPerfStats(p); }
} catch { /* ignore */ }
setPageReady(true);
}
init();
@@ -525,6 +535,66 @@ export default function LottoRecommendPage() {
</p>
</div>
{/* ── 성과 배너 ── */}
{perfStats && perfStats.total_checked > 0 && (
<div style={{ background: 'linear-gradient(135deg,rgba(74,222,128,.06),rgba(6,182,212,.06))', border: '1px solid rgba(74,222,128,.2)', borderRadius: '1rem', padding: '.85rem 1.25rem', marginBottom: '1.5rem', display: 'flex', flexWrap: 'wrap', gap: '1rem', alignItems: 'center', justifyContent: 'space-between', animation: 'slideIn .5s ease forwards' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '.5rem' }}>
<div style={{ width: 7, height: 7, borderRadius: '50%', background: '#4ade80', boxShadow: '0 0 8px rgba(74,222,128,.8)', animation: 'glowPulse 2s ease-in-out infinite', flexShrink: 0 }} />
<span style={{ color: '#4ade80', fontSize: '.72rem', fontWeight: 700 }}> </span>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1.5rem' }}>
<div style={{ textAlign: 'center' }}>
<div style={{ color: '#4ade80', fontWeight: 900, fontSize: '1.1rem', fontFamily: "'JetBrains Mono',monospace" }}>{(perfStats.rate_3plus * 100).toFixed(1)}%</div>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.6rem' }}>3 </div>
</div>
<div style={{ textAlign: 'center' }}>
<div style={{ color: '#4ade80', fontWeight: 900, fontSize: '1.1rem', fontFamily: "'JetBrains Mono',monospace" }}>{perfStats.avg_correct.toFixed(1)}</div>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.6rem' }}> </div>
</div>
<div style={{ textAlign: 'center' }}>
<div style={{ color: '#06b6d4', fontWeight: 900, fontSize: '1.1rem', fontFamily: "'JetBrains Mono',monospace" }}>+{perfStats.vs_random.improvement_pct.toFixed(0)}%</div>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.6rem' }}> </div>
</div>
<div style={{ textAlign: 'center' }}>
<div style={{ color: 'rgba(255,255,255,.4)', fontWeight: 700, fontSize: '.95rem', fontFamily: "'JetBrains Mono',monospace" }}>{perfStats.total_checked.toLocaleString()}</div>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.6rem' }}> </div>
</div>
</div>
</div>
)}
{/* ── 탭 네비게이션 ── */}
{isSubscribed && (
<div style={{ display: 'flex', gap: '.3rem', marginBottom: '1.5rem', background: 'rgba(255,255,255,.04)', borderRadius: '.75rem', padding: '.3rem', flexWrap: 'wrap' }}>
{([
{ key: 'generate', label: '🎲 번호 생성' },
{ key: 'report', label: '📋 이번 주 공략' },
{ key: 'purchase', label: '💰 구매 기록' },
{ key: 'pattern', label: '🔍 내 패턴' },
] as const).map(tab => (
<button key={tab.key} onClick={() => setActiveTab(tab.key)}
style={{
flex: 1, minWidth: 80,
background: activeTab === tab.key ? 'rgba(251,191,36,.15)' : 'transparent',
border: `1px solid ${activeTab === tab.key ? 'rgba(251,191,36,.35)' : 'transparent'}`,
color: activeTab === tab.key ? '#fbbf24' : 'rgba(255,255,255,.35)',
borderRadius: '.5rem', padding: '.5rem .5rem', fontSize: '.72rem', fontWeight: 700,
cursor: 'pointer', transition: 'all .2s ease', whiteSpace: 'nowrap',
}}>
{tab.label}
</button>
))}
</div>
)}
{/* ── 탭별 컨텐츠: 공략/구매/패턴 ── */}
{isSubscribed && activeTab === 'report' && <ReportTab />}
{isSubscribed && activeTab === 'purchase' && <PurchaseTab />}
{isSubscribed && activeTab === 'pattern' && <PatternTab />}
{/* ── 기존 메인 콘텐츠 (번호 생성 탭 or 비구독) ── */}
<div style={{ display: isSubscribed && activeTab !== 'generate' ? 'none' : 'block' }}>
{/* ── 통계 인디케이터 패널 (전체 공개) ── */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: '.75rem', marginBottom: '2rem' }}>
{[
@@ -1121,6 +1191,8 @@ export default function LottoRecommendPage() {
<p style={{ textAlign: 'center', color: 'rgba(255,255,255,.1)', fontSize: '.65rem', marginTop: '1.75rem', lineHeight: 1.7, fontFamily: "'JetBrains Mono',monospace" }}>
, .
</p>
</div>{/* ── 기존 메인 콘텐츠 래퍼 닫기 ── */}
</div>
</div>
</>