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:
162
src/pages/lotto/hooks/useLottoData.js
Normal file
162
src/pages/lotto/hooks/useLottoData.js
Normal file
@@ -0,0 +1,162 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
getLatest, getStats, getBestPicks, getAnalysis,
|
||||
getPerformanceStats, getLatestReport, getReportHistory, getReport,
|
||||
getPersonalAnalysis, getCombinedRecommend, getCombinedHistory,
|
||||
} from '../../../api';
|
||||
import { readStatsCache, writeStatsCache } from '../lottoUtils';
|
||||
|
||||
export default function useLottoData() {
|
||||
const [latest, setLatest] = useState(null);
|
||||
const [stats, setStats] = useState(() => readStatsCache());
|
||||
const [statsLoading, setStatsLoading] = useState(false);
|
||||
const [statsError, setStatsError] = useState('');
|
||||
const [loading, setLoading] = useState({
|
||||
latest: false, bestPicks: false, analysis: false,
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
const [bestPicks, setBestPicks] = useState([]);
|
||||
const [bestPicksExpanded, setBestPicksExpanded] = useState(false);
|
||||
const [analysis, setAnalysis] = useState(null);
|
||||
const [simulating, setSimulating] = useState(false);
|
||||
const [simResult, setSimResult] = useState(null);
|
||||
|
||||
// 종합 추론
|
||||
const [combined, setCombined] = useState(null);
|
||||
const [combinedLoading, setCombinedLoading] = useState(false);
|
||||
const [combinedHistory, setCombinedHistory] = useState([]);
|
||||
const [combinedHistLoading, setCombinedHistLoading] = useState(false);
|
||||
|
||||
// 신뢰도·리포트·개인분석
|
||||
const [perfStats, setPerfStats] = useState(null);
|
||||
const [report, setReport] = useState(null);
|
||||
const [reportHistory, setReportHistory] = useState([]);
|
||||
const [reportLoading, setReportLoading] = useState(false);
|
||||
const [personalAnalysis, setPersonalAnalysis] = useState(null);
|
||||
const [personalLoading, setPersonalLoading] = useState(false);
|
||||
|
||||
const refreshLatest = useCallback(async () => {
|
||||
setLoading((s) => ({ ...s, latest: true }));
|
||||
setError('');
|
||||
try { setLatest(await getLatest()); }
|
||||
catch (e) { setError(e?.message ?? String(e)); }
|
||||
finally { setLoading((s) => ({ ...s, latest: false })); }
|
||||
}, []);
|
||||
|
||||
const refreshStats = useCallback(async () => {
|
||||
setStatsLoading(true); setStatsError('');
|
||||
try {
|
||||
const cached = readStatsCache();
|
||||
if (cached && !stats) setStats(cached);
|
||||
const data = await getStats();
|
||||
if (!cached || cached.total_draws !== data?.total_draws) {
|
||||
setStats(data); writeStatsCache(data);
|
||||
}
|
||||
} catch (e) { setStatsError(e?.message ?? String(e)); }
|
||||
finally { setStatsLoading(false); }
|
||||
}, [stats]);
|
||||
|
||||
const refreshBestPicks = useCallback(async () => {
|
||||
setLoading((s) => ({ ...s, bestPicks: true }));
|
||||
try { setBestPicks((await getBestPicks(20)).items ?? []); }
|
||||
catch {}
|
||||
finally { setLoading((s) => ({ ...s, bestPicks: false })); }
|
||||
}, []);
|
||||
|
||||
const refreshAnalysis = useCallback(async () => {
|
||||
setLoading((s) => ({ ...s, analysis: true }));
|
||||
try { setAnalysis(await getAnalysis()); }
|
||||
catch {}
|
||||
finally { setLoading((s) => ({ ...s, analysis: false })); }
|
||||
}, []);
|
||||
|
||||
const refreshPerfStats = useCallback(async () => {
|
||||
try { setPerfStats(await getPerformanceStats()); } catch {}
|
||||
}, []);
|
||||
|
||||
const refreshReport = useCallback(async () => {
|
||||
setReportLoading(true);
|
||||
try {
|
||||
const [rep, hist] = await Promise.all([
|
||||
getLatestReport(),
|
||||
getReportHistory(10),
|
||||
]);
|
||||
setReport(rep);
|
||||
setReportHistory(hist?.reports ?? []);
|
||||
} catch {}
|
||||
finally { setReportLoading(false); }
|
||||
}, []);
|
||||
|
||||
const loadSpecificReport = useCallback(async (drwNo) => {
|
||||
setReportLoading(true);
|
||||
try { setReport(await getReport(drwNo)); }
|
||||
catch {}
|
||||
finally { setReportLoading(false); }
|
||||
}, []);
|
||||
|
||||
const runCombinedRecommend = useCallback(async () => {
|
||||
setCombinedLoading(true);
|
||||
try {
|
||||
const data = await getCombinedRecommend();
|
||||
setCombined(data);
|
||||
const hist = await getCombinedHistory(30);
|
||||
setCombinedHistory(hist?.items ?? []);
|
||||
} catch (e) { setError(e?.message ?? String(e)); }
|
||||
finally { setCombinedLoading(false); }
|
||||
}, []);
|
||||
|
||||
const loadCombinedHistory = useCallback(async () => {
|
||||
setCombinedHistLoading(true);
|
||||
try {
|
||||
const hist = await getCombinedHistory(30);
|
||||
setCombinedHistory(hist?.items ?? []);
|
||||
} catch {}
|
||||
finally { setCombinedHistLoading(false); }
|
||||
}, []);
|
||||
|
||||
const refreshPersonalAnalysis = useCallback(async () => {
|
||||
setPersonalLoading(true);
|
||||
try { setPersonalAnalysis(await getPersonalAnalysis()); }
|
||||
catch {}
|
||||
finally { setPersonalLoading(false); }
|
||||
}, []);
|
||||
|
||||
const onSimulate = useCallback(async () => {
|
||||
const ok = confirm('시뮬레이션을 즉시 실행할까요?\n20,000개 후보를 분석합니다. (약 1~3분 소요)');
|
||||
if (!ok) return;
|
||||
setSimulating(true); setSimResult(null); setError('');
|
||||
try {
|
||||
const { triggerSimulate } = await import('../../../api');
|
||||
const data = await triggerSimulate();
|
||||
setSimResult(data);
|
||||
await refreshBestPicks();
|
||||
} catch (e) { setError(e?.message ?? String(e)); }
|
||||
finally { setSimulating(false); }
|
||||
}, [refreshBestPicks]);
|
||||
|
||||
// 초기 로드
|
||||
useEffect(() => {
|
||||
refreshLatest();
|
||||
refreshStats();
|
||||
refreshBestPicks();
|
||||
refreshAnalysis();
|
||||
refreshPerfStats();
|
||||
refreshReport();
|
||||
refreshPersonalAnalysis();
|
||||
loadCombinedHistory();
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return {
|
||||
latest, loading, error, setError,
|
||||
stats, statsLoading, statsError, refreshStats,
|
||||
refreshLatest,
|
||||
bestPicks, bestPicksExpanded, setBestPicksExpanded, refreshBestPicks,
|
||||
analysis, refreshAnalysis,
|
||||
simulating, simResult, onSimulate,
|
||||
combined, combinedLoading, combinedHistory, combinedHistLoading,
|
||||
runCombinedRecommend,
|
||||
perfStats,
|
||||
report, reportHistory, reportLoading, refreshReport, loadSpecificReport,
|
||||
personalAnalysis, personalLoading,
|
||||
};
|
||||
}
|
||||
75
src/pages/lotto/hooks/useManualRecommend.js
Normal file
75
src/pages/lotto/hooks/useManualRecommend.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { deleteHistory, getHistory, recommend } from '../../../api';
|
||||
import { buildMetricsFromHistory } from '../lottoUtils';
|
||||
|
||||
export default function useManualRecommend() {
|
||||
const [params, setParams] = useState({
|
||||
recent_window: 200, recent_weight: 2.0, avoid_recent_k: 5,
|
||||
});
|
||||
const presets = useMemo(() => [
|
||||
{ name: '기본', recent_window: 200, recent_weight: 2.0, avoid_recent_k: 5 },
|
||||
{ name: '최근 가중치↑', recent_window: 100, recent_weight: 3.0, avoid_recent_k: 10 },
|
||||
{ name: '안전(분산)', recent_window: 300, recent_weight: 1.6, avoid_recent_k: 8 },
|
||||
{ name: '공격(최근)', recent_window: 80, recent_weight: 3.5, avoid_recent_k: 12 },
|
||||
], []);
|
||||
const [result, setResult] = useState(null);
|
||||
const [history, setHistory] = useState([]);
|
||||
const [historyExpanded, setHistoryExpanded] = useState(false);
|
||||
const historyEndRef = useRef(null);
|
||||
const prevHistoryExpandedRef = useRef(false);
|
||||
const [loading, setLoading] = useState({ recommend: false, history: false });
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const historyMetrics = useMemo(() => buildMetricsFromHistory(history), [history]);
|
||||
const visibleHistory = historyExpanded ? history : history.slice(0, 5);
|
||||
|
||||
const refreshHistory = useCallback(async () => {
|
||||
setLoading((s) => ({ ...s, history: true }));
|
||||
setError('');
|
||||
try {
|
||||
const limit = 100; let offset = 0; const allItems = [];
|
||||
while (true) {
|
||||
const data = await getHistory(limit, offset);
|
||||
const items = data.items ?? [];
|
||||
allItems.push(...items);
|
||||
if (items.length < limit) break;
|
||||
offset += limit;
|
||||
}
|
||||
setHistory(allItems);
|
||||
} catch (e) { setError(e?.message ?? String(e)); }
|
||||
finally { setLoading((s) => ({ ...s, history: false })); }
|
||||
}, []);
|
||||
|
||||
const onRecommend = useCallback(async () => {
|
||||
setLoading((s) => ({ ...s, recommend: true })); setError('');
|
||||
try { const data = await recommend(params); setResult(data); await refreshHistory(); }
|
||||
catch (e) { setError(e?.message ?? String(e)); }
|
||||
finally { setLoading((s) => ({ ...s, recommend: false })); }
|
||||
}, [params, refreshHistory]);
|
||||
|
||||
const onDelete = useCallback(async (id) => {
|
||||
if (!confirm(`히스토리 #${id}를 삭제할까요?`)) return;
|
||||
setError('');
|
||||
try { await deleteHistory(id); setHistory((prev) => prev.filter((item) => item.id !== id)); }
|
||||
catch (e) { setError(e?.message ?? String(e)); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (historyExpanded && !prevHistoryExpandedRef.current) {
|
||||
requestAnimationFrame(() => {
|
||||
historyEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||||
});
|
||||
}
|
||||
prevHistoryExpandedRef.current = historyExpanded;
|
||||
}, [historyExpanded, visibleHistory.length]);
|
||||
|
||||
useEffect(() => { refreshHistory(); }, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return {
|
||||
params, setParams, presets,
|
||||
result, history, historyExpanded, setHistoryExpanded,
|
||||
historyEndRef, loading, error, setError,
|
||||
historyMetrics, visibleHistory,
|
||||
refreshHistory, onRecommend, onDelete,
|
||||
};
|
||||
}
|
||||
105
src/pages/lotto/hooks/usePurchases.js
Normal file
105
src/pages/lotto/hooks/usePurchases.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
getPurchases, getPurchaseStats, addPurchase, updatePurchase, deletePurchase,
|
||||
} from '../../../api';
|
||||
import { emptyPurchaseForm } from '../lottoUtils';
|
||||
|
||||
export default function usePurchases() {
|
||||
const [purchases, setPurchases] = useState([]);
|
||||
const [purchaseStats, setPurchaseStats] = useState(null);
|
||||
const [purchaseLoading, setPurchaseLoading] = useState(false);
|
||||
|
||||
// 폼 상태
|
||||
const [purchaseFormOpen, setPurchaseFormOpen] = useState(false);
|
||||
const [purchaseForm, setPurchaseForm] = useState(emptyPurchaseForm);
|
||||
const [purchaseFormSaving, setPurchaseFormSaving] = useState(false);
|
||||
const [purchaseFormError, setPurchaseFormError] = useState('');
|
||||
const [purchaseEditId, setPurchaseEditId] = useState(null);
|
||||
|
||||
const refreshPurchases = useCallback(async () => {
|
||||
setPurchaseLoading(true);
|
||||
try {
|
||||
const [recs, st] = await Promise.all([getPurchases(), getPurchaseStats()]);
|
||||
setPurchases(recs?.records ?? []);
|
||||
setPurchaseStats(st);
|
||||
} catch {}
|
||||
finally { setPurchaseLoading(false); }
|
||||
}, []);
|
||||
|
||||
const handlePurchaseFormOpen = useCallback(() => {
|
||||
setPurchaseEditId(null);
|
||||
setPurchaseForm(emptyPurchaseForm());
|
||||
setPurchaseFormError('');
|
||||
setPurchaseFormOpen(true);
|
||||
}, []);
|
||||
|
||||
const handlePurchaseFormClose = useCallback(() => {
|
||||
setPurchaseFormOpen(false);
|
||||
setPurchaseEditId(null);
|
||||
setPurchaseFormError('');
|
||||
}, []);
|
||||
|
||||
const handlePurchaseFormChange = useCallback((field, value) => {
|
||||
setPurchaseForm((prev) => ({ ...prev, [field]: value }));
|
||||
}, []);
|
||||
|
||||
const handlePurchaseEditStart = useCallback((rec) => {
|
||||
setPurchaseEditId(rec.id);
|
||||
setPurchaseForm({
|
||||
draw_no: String(rec.draw_no ?? ''),
|
||||
amount: rec.amount ?? 5000,
|
||||
sets: rec.sets ?? 5,
|
||||
prize: rec.prize ?? 0,
|
||||
note: rec.note ?? '',
|
||||
});
|
||||
setPurchaseFormError('');
|
||||
setPurchaseFormOpen(true);
|
||||
}, []);
|
||||
|
||||
const handlePurchaseFormSubmit = useCallback(async (e) => {
|
||||
e.preventDefault();
|
||||
setPurchaseFormSaving(true); setPurchaseFormError('');
|
||||
const payload = {
|
||||
draw_no: Number(purchaseForm.draw_no),
|
||||
amount: Number(purchaseForm.amount),
|
||||
sets: Number(purchaseForm.sets),
|
||||
prize: Number(purchaseForm.prize),
|
||||
note: purchaseForm.note.trim(),
|
||||
};
|
||||
try {
|
||||
if (purchaseEditId != null) {
|
||||
const updated = await updatePurchase(purchaseEditId, payload);
|
||||
setPurchases((prev) =>
|
||||
prev.map((r) => r.id === purchaseEditId ? (updated ?? { ...payload, id: purchaseEditId }) : r)
|
||||
);
|
||||
} else {
|
||||
const saved = await addPurchase(payload);
|
||||
setPurchases((prev) => [saved ?? { ...payload, id: Date.now() }, ...prev]);
|
||||
}
|
||||
try { setPurchaseStats(await getPurchaseStats()); } catch {}
|
||||
handlePurchaseFormClose();
|
||||
} catch (err) {
|
||||
setPurchaseFormError(err?.message ?? String(err));
|
||||
} finally {
|
||||
setPurchaseFormSaving(false);
|
||||
}
|
||||
}, [purchaseForm, purchaseEditId, handlePurchaseFormClose]);
|
||||
|
||||
const handlePurchaseDelete = useCallback(async (id) => {
|
||||
if (!confirm('이 구매 기록을 삭제할까요?')) return;
|
||||
setPurchases((prev) => prev.filter((r) => r.id !== id));
|
||||
try {
|
||||
await deletePurchase(id);
|
||||
try { setPurchaseStats(await getPurchaseStats()); } catch {}
|
||||
} catch { refreshPurchases(); }
|
||||
}, [refreshPurchases]);
|
||||
|
||||
useEffect(() => { refreshPurchases(); }, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return {
|
||||
purchases, purchaseStats, purchaseLoading,
|
||||
purchaseFormOpen, purchaseForm, purchaseFormSaving, purchaseFormError, purchaseEditId,
|
||||
handlePurchaseFormOpen, handlePurchaseFormClose, handlePurchaseFormChange,
|
||||
handlePurchaseFormSubmit, handlePurchaseEditStart, handlePurchaseDelete,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user