Functions.jsx 컴포넌트 분할: 1,583→460줄 (3훅+8컴포넌트+유틸)

- 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>
This commit is contained in:
2026-04-03 07:53:25 +09:00
parent 2b463682d5
commit 299ce636ff
13 changed files with 1342 additions and 1256 deletions

View File

@@ -0,0 +1,173 @@
import React from 'react';
import { fmtWon } from '../lottoUtils';
const PurchasePanel = ({
records, stats, loading,
formOpen, form, formSaving, formError, editId,
onFormOpen, onFormClose, onFormChange, onFormSubmit,
onEditStart, onDelete,
}) => {
const winRate = stats?.total_records > 0
? ((stats.prize_count / stats.total_records) * 100).toFixed(1)
: '0.0';
const netColor = (stats?.net ?? 0) >= 0 ? 'is-pos' : 'is-neg';
return (
<section className="lotto-panel lotto-panel--wide">
<div className="lotto-panel__head">
<div>
<p className="lotto-panel__eyebrow">Purchase Tracker</p>
<h3>구매 기록</h3>
<p className="lotto-panel__sub">구매 내역 기록 수익률 추적</p>
</div>
<div className="lotto-panel__actions">
{loading && <span className="lotto-chip">로딩 </span>}
<button className="button small" onClick={onFormOpen} disabled={formOpen}>
+ 추가
</button>
</div>
</div>
{/* 통계 바 */}
{stats && stats.total_records > 0 && (
<div className="lotto-purchase-stats">
<div className="lotto-purchase-stat">
<span className="lotto-purchase-stat__val">{fmtWon(stats.total_invested)}</span>
<span className="lotto-purchase-stat__lbl"> 투자</span>
</div>
<div className="lotto-purchase-stat">
<span className="lotto-purchase-stat__val">{fmtWon(stats.total_prize)}</span>
<span className="lotto-purchase-stat__lbl"> 당첨금</span>
</div>
<div className="lotto-purchase-stat">
<span className={`lotto-purchase-stat__val ${netColor}`}>
{(stats.net ?? 0) >= 0 ? '+' : ''}{fmtWon(stats.net)}
</span>
<span className="lotto-purchase-stat__lbl">순손익</span>
</div>
<div className="lotto-purchase-stat">
<span className="lotto-purchase-stat__val">{stats.return_rate?.toFixed(1)}%</span>
<span className="lotto-purchase-stat__lbl">회수율</span>
</div>
<div className="lotto-purchase-stat">
<span className="lotto-purchase-stat__val">{winRate}%</span>
<span className="lotto-purchase-stat__lbl">당첨률</span>
</div>
{stats.max_prize > 0 && (
<div className="lotto-purchase-stat">
<span className="lotto-purchase-stat__val is-prize">{fmtWon(stats.max_prize)}</span>
<span className="lotto-purchase-stat__lbl">최대 당첨금</span>
</div>
)}
</div>
)}
{/* 입력 폼 */}
{formOpen && (
<form className="lotto-purchase-form" onSubmit={onFormSubmit}>
<p className="lotto-purchase-form__title">
{editId != null ? '기록 수정' : '구매 기록 추가'}
</p>
<div className="lotto-purchase-form__grid">
<label className="lotto-field">
회차
<input
type="number" min={1}
value={form.draw_no}
onChange={(e) => onFormChange('draw_no', e.target.value)}
placeholder="예: 1181"
required
/>
</label>
<label className="lotto-field">
구매금액
<input
type="number" step={1000} min={1000}
value={form.amount}
onChange={(e) => onFormChange('amount', Number(e.target.value))}
/>
</label>
<label className="lotto-field">
세트
<input
type="number" min={1} max={20}
value={form.sets}
onChange={(e) => onFormChange('sets', Number(e.target.value))}
/>
</label>
<label className="lotto-field">
당첨금
<input
type="number" min={0}
value={form.prize}
onChange={(e) => onFormChange('prize', Number(e.target.value))}
/>
</label>
<label className="lotto-field lotto-purchase-form__note">
메모
<input
type="text"
value={form.note}
onChange={(e) => onFormChange('note', e.target.value)}
placeholder="예: 5등 1개"
/>
</label>
</div>
{formError && (
<p className="lotto-empty" style={{ color: '#f9b6b1' }}>{formError}</p>
)}
<div className="lotto-purchase-form__actions">
<button type="button" className="button ghost small" onClick={onFormClose}>
취소
</button>
<button type="submit" className="button primary small" disabled={formSaving}>
{formSaving ? '저장 중...' : editId != null ? '수정 완료' : '추가'}
</button>
</div>
</form>
)}
{/* 기록 목록 */}
{records.length === 0 ? (
<p className="lotto-empty">구매 기록이 없습니다.</p>
) : (
<div className="lotto-purchase-list">
<div className="lotto-purchase-list__head">
<span>회차</span>
<span>투자금</span>
<span>당첨금</span>
<span>손익</span>
<span>메모</span>
<span />
</div>
{records.map((rec) => {
const net = (rec.prize ?? 0) - (rec.amount ?? 0);
return (
<div key={rec.id} className="lotto-purchase-row">
<span className="lotto-purchase-row__drw">{rec.draw_no}</span>
<span>{fmtWon(rec.amount)}</span>
<span className={(rec.prize ?? 0) > 0 ? 'is-prize' : ''}>
{fmtWon(rec.prize)}
</span>
<span className={net >= 0 ? 'is-pos' : 'is-neg'}>
{net >= 0 ? '+' : ''}{fmtWon(net)}
</span>
<span className="lotto-purchase-row__note">{rec.note || '-'}</span>
<div className="lotto-purchase-row__actions">
<button className="button ghost small" onClick={() => onEditStart(rec)}>
수정
</button>
<button className="button danger small" onClick={() => onDelete(rec.id)}>
삭제
</button>
</div>
</div>
);
})}
</div>
)}
</section>
);
};
export default PurchasePanel;