- lottoUtils.jsx: 공통 유틸·상수 추출 (Ball, NumberRow, 통계 헬퍼 등) - hooks/useLottoData.js: 핵심 데이터 로드 (최신회차, 통계, 시뮬레이션, 리포트) - hooks/usePurchases.js: 구매 기록 CRUD - hooks/useManualRecommend.js: 수동 추천 + 히스토리 - components/: MetricBlock, FrequencyChart, PerformanceBanner, ConfidenceRing, CombinedRecommendPanel, ReportPanel, PersonalAnalysisPanel, PurchasePanel 분리 - getReport import 누락 버그 수정 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
158 lines
8.6 KiB
JavaScript
158 lines
8.6 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { ballClass, NumberRow, METHOD_META, METHOD_ORDER, SCORE_META, fmtKST } from '../lottoUtils';
|
|
|
|
const CombinedRecommendPanel = ({ combined, history, loading, histLoading, onRun, onCopy }) => {
|
|
const [histExpand, setHistExpand] = useState(false);
|
|
|
|
return (
|
|
<section className="lotto-panel lotto-panel--wide lotto-combined">
|
|
<div className="lotto-panel__head">
|
|
<div>
|
|
<p className="lotto-panel__eyebrow">AI · 종합 추론</p>
|
|
<h3>종합 추론 번호 추천</h3>
|
|
<p className="lotto-panel__sub">
|
|
5가지 통계 기법(빈도·지문·갭·공동출현·다양성)을 가중 투표로 합산해
|
|
최적 6개 번호를 도출합니다.
|
|
</p>
|
|
</div>
|
|
<div className="lotto-panel__actions">
|
|
{loading && <span className="lotto-chip">분석 중…</span>}
|
|
<button className="button primary small" onClick={onRun} disabled={loading}>
|
|
{loading ? '추론 중…' : '🔮 종합 추론 실행'}
|
|
</button>
|
|
{history.length > 0 && (
|
|
<button className="button ghost small" onClick={() => setHistExpand(p => !p)}>
|
|
이력 {history.length}건 {histExpand ? '▲' : '▼'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{!combined && !loading && (
|
|
<p className="lotto-empty">버튼을 눌러 종합 추론을 실행하세요.</p>
|
|
)}
|
|
|
|
{combined && (
|
|
<>
|
|
{/* 기법별 추천 번호 */}
|
|
<div className="lotto-combined__methods">
|
|
{METHOD_ORDER.map((key) => {
|
|
const meta = METHOD_META[key];
|
|
const m = combined.methods?.[key];
|
|
if (!m) return null;
|
|
return (
|
|
<div key={key} className="lotto-combined__method">
|
|
<div className="lotto-combined__method-head">
|
|
<span className="lotto-combined__method-icon">{meta.icon}</span>
|
|
<div>
|
|
<p className="lotto-combined__method-name" style={{ color: meta.color }}>
|
|
{meta.label}
|
|
<span className="lotto-combined__method-weight"> ({m.weight_pct}%)</span>
|
|
</p>
|
|
<p className="lotto-combined__method-desc">{meta.desc}</p>
|
|
</div>
|
|
</div>
|
|
<div className="lotto-combined__method-nums">
|
|
{m.numbers.map((n) => {
|
|
const inFinal = combined.final_numbers.includes(n);
|
|
return (
|
|
<span
|
|
key={n}
|
|
className={`lotto-ball ${ballClass(n).replace('lotto-ball ', '')} ${inFinal ? 'is-final' : 'is-dim'}`}
|
|
>
|
|
{n}
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* 최종 추론 결과 */}
|
|
<div className="lotto-combined__final">
|
|
<div className="lotto-combined__final-head">
|
|
<span className="lotto-combined__final-badge">종합 추론 결과</span>
|
|
{combined.deduped && (
|
|
<span className="lotto-chip lotto-chip--muted">중복 (이미 저장됨)</span>
|
|
)}
|
|
<button className="button ghost small" onClick={() => onCopy(combined.final_numbers)}>
|
|
복사
|
|
</button>
|
|
</div>
|
|
<div className="lotto-combined__final-balls">
|
|
{combined.final_numbers.map((n) => {
|
|
const votes = combined.vote_counts?.[String(n)] ?? 0;
|
|
return (
|
|
<div key={n} className="lotto-combined__final-ball-wrap">
|
|
<span className={ballClass(n)}>{n}</span>
|
|
<span className="lotto-combined__vote-dots">
|
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
<span key={i} className={`lotto-combined__vote-dot ${i < votes ? 'is-on' : ''}`} />
|
|
))}
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<p className="lotto-combined__final-sub">
|
|
● 점은 해당 번호가 채택된 기법 수 (최대 5개)
|
|
</p>
|
|
</div>
|
|
|
|
{/* 점수 바 */}
|
|
<div className="lotto-combined__scores">
|
|
<p className="lotto-combined__scores-title">조합 품질 점수</p>
|
|
{SCORE_META.map(({ key, label, color, weight }) => {
|
|
const val = combined.scores?.[key] ?? 0;
|
|
const pct = Math.round(val * 100);
|
|
return (
|
|
<div key={key} className="lotto-combined__score-row">
|
|
<span className="lotto-combined__score-label">{label}</span>
|
|
<span className="lotto-combined__score-weight">{weight}%</span>
|
|
<div className="lotto-combined__score-bar-wrap">
|
|
<div
|
|
className="lotto-combined__score-bar"
|
|
style={{ width: `${pct}%`, background: color }}
|
|
/>
|
|
</div>
|
|
<span className="lotto-combined__score-val">{pct}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
<div className="lotto-combined__score-total">
|
|
종합 점수 <strong>{Math.round((combined.scores?.score_total ?? 0) * 100)}</strong> / 100
|
|
</div>
|
|
</div>
|
|
|
|
<p className="lotto-combined__disclaimer">
|
|
※ 이 추천은 역대 통계 패턴 기반 참고 자료이며, 당첨을 보장하지 않습니다.
|
|
</p>
|
|
</>
|
|
)}
|
|
|
|
{/* 추천 이력 */}
|
|
{histExpand && (
|
|
<div className="lotto-combined__history">
|
|
<p className="lotto-combined__history-title">종합 추론 이력</p>
|
|
{histLoading && <p className="lotto-empty">로딩 중…</p>}
|
|
{history.map((item) => (
|
|
<div key={item.id} className="lotto-combined__history-item">
|
|
<div className="lotto-combined__history-meta">
|
|
<span>#{item.id}</span>
|
|
<span>{fmtKST(item.created_at)}</span>
|
|
<span>기준 {item.based_on_draw ?? '-'}회</span>
|
|
</div>
|
|
<NumberRow nums={item.numbers} />
|
|
<button className="button ghost small" onClick={() => onCopy(item.numbers)}>복사</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export default CombinedRecommendPanel;
|