From 9d2dfad512bd305cb08e2fa54c891df098297fa7 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 11 May 2026 08:56:10 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat(api):=20review=20+=20bulkPurchase=20?= =?UTF-8?q?=ED=97=AC=ED=8D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/api.js b/src/api.js index 64c873a..3f01a47 100644 --- a/src/api.js +++ b/src/api.js @@ -680,3 +680,18 @@ export const getBatchJob = (id) => apiGet(`/api/music/generate-b export const listBatchJobs = (status='all') => apiGet(`/api/music/generate-batch?status=${status}`); export const listGenres = () => apiGet('/api/music/genres'); +// === 주간 회고 (weekly_review) === +// apiGet은 비-2xx 응답에서 `HTTP ...` 메시지로 Error를 throw 하므로 +// 404 케이스는 메시지를 파싱하여 null로 변환한다. +export const getLatestReview = () => apiGet('/api/lotto/review/latest').catch(e => { + if (e?.status === 404 || /^HTTP 404\b/.test(e?.message || '')) return null; + throw e; +}); + +export const getReviewHistory = (limit = 4) => + apiGet(`/api/lotto/review/history?limit=${limit}`).then(d => d.reviews || []); + +// === 큐레이터 4계층 원클릭 구매 === +export const bulkPurchase = ({ draw_no, tier_mode, sets, amount }) => + apiPost('/api/lotto/purchase/bulk', { draw_no, tier_mode, sets, amount }); + From cd3c538eb7c26dd5bf527366d14de6f629079171 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 11 May 2026 08:57:14 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat(lotto):=20useReview=20=ED=9B=85=20+=20?= =?UTF-8?q?useBriefing=204=EA=B3=84=EC=B8=B5=20=EC=A0=95=EA=B7=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/lotto/hooks/useBriefing.js | 16 ++++++++++++++-- src/pages/lotto/hooks/useReview.js | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/pages/lotto/hooks/useReview.js diff --git a/src/pages/lotto/hooks/useBriefing.js b/src/pages/lotto/hooks/useBriefing.js index a405623..02394ed 100644 --- a/src/pages/lotto/hooks/useBriefing.js +++ b/src/pages/lotto/hooks/useBriefing.js @@ -1,6 +1,18 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { getLatestBriefing, triggerLottoCurate } from '../../../api'; +const normalizePicks = (picks) => { + if (Array.isArray(picks)) { + return { core: picks, bonus: [], extended: [], pool: [] }; + } + return { + core: picks?.core || [], + bonus: picks?.bonus || [], + extended: picks?.extended || [], + pool: picks?.pool || [], + }; +}; + export default function useBriefing() { const [briefing, setBriefing] = useState(null); const [loading, setLoading] = useState(true); @@ -12,7 +24,7 @@ export default function useBriefing() { setLoading(true); setError(''); try { const data = await getLatestBriefing(); - setBriefing(data); + setBriefing(data ? { ...data, picks: normalizePicks(data.picks) } : data); } catch (e) { setError(e.message); } finally { @@ -33,7 +45,7 @@ export default function useBriefing() { try { const data = await getLatestBriefing(); if (data && data.generated_at !== prevGen) { - setBriefing(data); + setBriefing({ ...data, picks: normalizePicks(data.picks) }); setRegenerating(false); clearInterval(pollingRef.current); } diff --git a/src/pages/lotto/hooks/useReview.js b/src/pages/lotto/hooks/useReview.js new file mode 100644 index 0000000..b7dac64 --- /dev/null +++ b/src/pages/lotto/hooks/useReview.js @@ -0,0 +1,23 @@ +import { useEffect, useState } from 'react'; +import { getLatestReview, getReviewHistory } from '../../../api'; + +export default function useReview() { + const [latest, setLatest] = useState(null); + const [history, setHistory] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancel = false; + Promise.all([getLatestReview(), getReviewHistory(4)]) + .then(([l, h]) => { + if (cancel) return; + setLatest(l); + setHistory(h); + }) + .catch(() => {}) + .finally(() => !cancel && setLoading(false)); + return () => { cancel = true; }; + }, []); + + return { latest, history, loading }; +} From 329141c7325ac1b2afaa7e653d4f9ec191a7c8dc Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 11 May 2026 08:59:00 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat(lotto):=20DecisionCard=20=ED=95=98?= =?UTF-8?q?=EC=9C=84=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8(Pick/Tier/Togg?= =?UTF-8?q?le/Retro)=20+=20=EC=8A=A4=ED=83=80=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lotto/components/decision/PickCard.jsx | 19 ++++++++ .../components/decision/RetrospectiveBox.jsx | 11 +++++ .../components/decision/TierModeToggle.jsx | 28 +++++++++++ .../lotto/components/decision/TierSection.jsx | 25 ++++++++++ .../lotto/components/decision/decision.css | 47 +++++++++++++++++++ 5 files changed, 130 insertions(+) create mode 100644 src/pages/lotto/components/decision/PickCard.jsx create mode 100644 src/pages/lotto/components/decision/RetrospectiveBox.jsx create mode 100644 src/pages/lotto/components/decision/TierModeToggle.jsx create mode 100644 src/pages/lotto/components/decision/TierSection.jsx create mode 100644 src/pages/lotto/components/decision/decision.css diff --git a/src/pages/lotto/components/decision/PickCard.jsx b/src/pages/lotto/components/decision/PickCard.jsx new file mode 100644 index 0000000..430ddc4 --- /dev/null +++ b/src/pages/lotto/components/decision/PickCard.jsx @@ -0,0 +1,19 @@ +const ROLE_COLOR = { '안정': 'stable', '균형': 'balance', '공격': 'aggro' }; + +export default function PickCard({ pick, index, total }) { + const role = pick.risk_tag; + return ( +
+
+ ● {role} + Set {index + 1} / {total} +
+
+ {pick.numbers.map(n => ( + {n} + ))} +
+

{pick.reason}

+
+ ); +} diff --git a/src/pages/lotto/components/decision/RetrospectiveBox.jsx b/src/pages/lotto/components/decision/RetrospectiveBox.jsx new file mode 100644 index 0000000..ac7b6d9 --- /dev/null +++ b/src/pages/lotto/components/decision/RetrospectiveBox.jsx @@ -0,0 +1,11 @@ +export default function RetrospectiveBox({ briefing, review }) { + const retro = briefing?.narrative?.retrospective; + if (!retro) return null; + const drawNo = review?.draw_no ?? (briefing?.draw_no ? briefing.draw_no - 1 : null); + return ( + + ); +} diff --git a/src/pages/lotto/components/decision/TierModeToggle.jsx b/src/pages/lotto/components/decision/TierModeToggle.jsx new file mode 100644 index 0000000..33bb657 --- /dev/null +++ b/src/pages/lotto/components/decision/TierModeToggle.jsx @@ -0,0 +1,28 @@ +const MODES = [ + { key: 'core', label: '코어', sets: 5, amount: 5000 }, + { key: 'core_bonus', label: '+ 보너스', sets: 10, amount: 10000 }, + { key: 'core_bonus_extended', label: '+ 확장', sets: 15, amount: 15000 }, + { key: 'full', label: '+ 풀', sets: 20, amount: 20000 }, +]; + +export default function TierModeToggle({ value, onChange }) { + return ( +
+ {MODES.map((m, i) => ( + + ))} +
+ ); +} + +export { MODES }; diff --git a/src/pages/lotto/components/decision/TierSection.jsx b/src/pages/lotto/components/decision/TierSection.jsx new file mode 100644 index 0000000..67bc90f --- /dev/null +++ b/src/pages/lotto/components/decision/TierSection.jsx @@ -0,0 +1,25 @@ +import PickCard from './PickCard'; + +const TIER_TITLE = { + core: '코어 (필수, 5세트)', + bonus: '보너스 (+5)', + extended: '확장 (+5)', + pool: '풀 (+5)', +}; + +export default function TierSection({ tier, picks, rationale, indexBase = 0, totalSets }) { + if (!picks?.length) return null; + return ( +
+
+

{TIER_TITLE[tier]}

+ {rationale && tier !== 'core' && ( +

{rationale}

+ )} +
+ {picks.map((p, i) => ( + + ))} +
+ ); +} diff --git a/src/pages/lotto/components/decision/decision.css b/src/pages/lotto/components/decision/decision.css new file mode 100644 index 0000000..73eb4c0 --- /dev/null +++ b/src/pages/lotto/components/decision/decision.css @@ -0,0 +1,47 @@ +.lc-card { max-width: 720px; margin: 0 auto; background: linear-gradient(180deg, #161220 0%, #1a1426 100%); + border: 1px solid rgba(255,255,255,0.08); border-radius: 16px; padding: 24px; color: #ece6f7; } +.lc-head { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 14px; } +.lc-eyebrow { font-size: 10px; letter-spacing: 2px; opacity: 0.5; text-transform: uppercase; margin: 0 0 4px; } +.lc-title { font-size: 22px; font-weight: 700; margin: 0; letter-spacing: -0.02em; } +.lc-conf { display: flex; flex-direction: column; align-items: flex-end; } +.lc-conf__num { font-family: 'Courier New', monospace; font-size: 28px; font-weight: 700; color: #b8a8ff; letter-spacing: -0.04em; } +.lc-conf__lbl { font-size: 9px; letter-spacing: 1.5px; opacity: 0.55; } +.lc-retro { background: rgba(184, 168, 255, 0.06); border-left: 2px solid rgba(184, 168, 255, 0.4); + padding: 10px 14px; margin: 14px 0; border-radius: 4px; } +.lc-retro__time { font-size: 9px; letter-spacing: 1.5px; color: #b8a8ff; opacity: 0.7; margin: 0 0 4px; } +.lc-retro__body { font-size: 13px; line-height: 1.55; opacity: 0.85; margin: 0; } +.lc-headline { font-size: 16px; font-weight: 600; line-height: 1.5; margin: 18px 0 4px; } +.lc-headline-3 { font-size: 12px; opacity: 0.65; line-height: 1.55; margin: 0 0 18px; } +.lc-balance { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; + background: rgba(255,255,255,0.03); border-radius: 8px; margin-bottom: 16px; font-size: 11px; } +.lc-balance__chips { display: flex; gap: 8px; } +.lc-chip { padding: 3px 8px; border-radius: 100px; font-weight: 600; font-size: 11px; } +.lc-chip--stable { background: rgba(80, 200, 120, 0.15); color: #76e09a; } +.lc-chip--balance { background: rgba(255, 200, 80, 0.15); color: #ffce6e; } +.lc-chip--aggro { background: rgba(255, 100, 130, 0.15); color: #ff8aa0; } +.lc-toggle { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin: 16px 0; } +.lc-toggle__chip { padding: 10px 8px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); + border-radius: 10px; color: #ece6f7; cursor: pointer; display: flex; flex-direction: column; gap: 4px; align-items: center; } +.lc-toggle__chip.is-active { background: rgba(184, 168, 255, 0.15); border-color: rgba(184, 168, 255, 0.5); } +.lc-toggle__dots { letter-spacing: 2px; font-size: 10px; opacity: 0.7; } +.lc-toggle__lbl { font-size: 12px; font-weight: 600; } +.lc-toggle__sub { font-size: 10px; opacity: 0.55; } +.lc-tier { margin-bottom: 14px; } +.lc-tier__head { padding: 8px 0; border-top: 1px dashed rgba(255,255,255,0.1); margin-bottom: 8px; } +.lc-tier:first-of-type .lc-tier__head { border-top: none; } +.lc-tier__head h4 { font-size: 12px; font-weight: 600; margin: 0 0 4px; opacity: 0.75; letter-spacing: 0.5px; } +.lc-tier__rationale { font-size: 11px; opacity: 0.55; margin: 0; } +.lc-set { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 12px; + padding: 14px; margin-bottom: 10px; } +.lc-set__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } +.lc-set__role { font-size: 11px; font-weight: 600; letter-spacing: 0.5px; } +.lc-set__role--stable { color: #76e09a; } +.lc-set__role--balance { color: #ffce6e; } +.lc-set__role--aggro { color: #ff8aa0; } +.lc-set__idx { font-size: 10px; opacity: 0.4; } +.lc-balls { display: flex; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; } +.lc-set__reason { font-size: 12px; opacity: 0.7; line-height: 1.45; margin: 0; } +.lc-actions { display: flex; gap: 10px; margin-top: 18px; } +@media (max-width: 480px) { + .lc-toggle { grid-template-columns: repeat(2, 1fr); } +} From ff7ac48c6bcebabe9e8049816559d6a4bdf9fdf6 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 11 May 2026 09:00:59 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat(lotto):=20DecisionCard=20+=20BulkPurch?= =?UTF-8?q?aseButton,=20BriefingTab=20=EB=8B=A8=EC=9D=BC=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=9E=AC=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../decision/BulkPurchaseButton.jsx | 33 ++++++ .../components/decision/DecisionCard.jsx | 102 ++++++++++++++++++ .../lotto/components/decision/decision.css | 5 + src/pages/lotto/tabs/BriefingTab.jsx | 15 +-- 4 files changed, 144 insertions(+), 11 deletions(-) create mode 100644 src/pages/lotto/components/decision/BulkPurchaseButton.jsx create mode 100644 src/pages/lotto/components/decision/DecisionCard.jsx diff --git a/src/pages/lotto/components/decision/BulkPurchaseButton.jsx b/src/pages/lotto/components/decision/BulkPurchaseButton.jsx new file mode 100644 index 0000000..afe996e --- /dev/null +++ b/src/pages/lotto/components/decision/BulkPurchaseButton.jsx @@ -0,0 +1,33 @@ +import { useState } from 'react'; +import { bulkPurchase } from '../../../../api'; +import { MODES } from './TierModeToggle'; + +export default function BulkPurchaseButton({ drawNo, tierMode, onSuccess }) { + const [busy, setBusy] = useState(false); + const mode = MODES.find(m => m.key === tierMode) || MODES[0]; + + const onClick = async () => { + if (busy) return; + setBusy(true); + try { + await bulkPurchase({ + draw_no: drawNo, + tier_mode: tierMode, + sets: mode.sets, + amount: mode.amount, + }); + onSuccess?.(); + alert(`${mode.sets}세트 구매 기록 완료!`); + } catch (e) { + alert(`구매 기록 실패: ${e?.message || e}`); + } finally { + setBusy(false); + } + }; + + return ( + + ); +} diff --git a/src/pages/lotto/components/decision/DecisionCard.jsx b/src/pages/lotto/components/decision/DecisionCard.jsx new file mode 100644 index 0000000..492510c --- /dev/null +++ b/src/pages/lotto/components/decision/DecisionCard.jsx @@ -0,0 +1,102 @@ +import { useEffect, useMemo, useState } from 'react'; +import RetrospectiveBox from './RetrospectiveBox'; +import TierModeToggle, { MODES } from './TierModeToggle'; +import TierSection from './TierSection'; +import BulkPurchaseButton from './BulkPurchaseButton'; +import './decision.css'; + +const TIER_CHAIN = { + core: ['core'], + core_bonus: ['core', 'bonus'], + core_bonus_extended: ['core', 'bonus', 'extended'], + full: ['core', 'bonus', 'extended', 'pool'], +}; + +const STORAGE_KEY = 'lotto.tier_mode'; + +export default function DecisionCard({ briefing, review, onPurchaseSuccess }) { + const [tierMode, setTierMode] = useState(() => + localStorage.getItem(STORAGE_KEY) || 'core' + ); + + useEffect(() => { + localStorage.setItem(STORAGE_KEY, tierMode); + }, [tierMode]); + + const visibleTiers = TIER_CHAIN[tierMode]; + + const totalSets = useMemo( + () => visibleTiers.reduce((sum, t) => sum + (briefing?.picks?.[t]?.length || 0), 0), + [briefing, visibleTiers] + ); + + // 분배 칩 — 보이는 계층의 risk_tag 합산 + const balance = useMemo(() => { + const acc = { '안정': 0, '균형': 0, '공격': 0 }; + for (const t of visibleTiers) { + for (const p of (briefing?.picks?.[t] || [])) { + if (acc[p.risk_tag] !== undefined) acc[p.risk_tag]++; + } + } + return acc; + }, [briefing, visibleTiers]); + + if (!briefing) return null; + + let cursor = 0; + + return ( +
+
+
+

Curator Briefing · {briefing.draw_no}회

+

{briefing.narrative.headline}

+
+
+
{briefing.confidence}
+
CONFIDENCE
+
+
+ + + +

+ {(briefing.narrative.summary_3lines || []).join(' · ')} +

+ +
+
+ {balance['안정'] > 0 && 안정 ×{balance['안정']}} + {balance['균형'] > 0 && 균형 ×{balance['균형']}} + {balance['공격'] > 0 && 공격 ×{balance['공격']}} +
+
+ + + + {visibleTiers.map(tier => { + const picks = briefing.picks?.[tier] || []; + const idxBase = cursor; + cursor += picks.length; + return ( + + ); + })} + +
+ +
+
+ ); +} diff --git a/src/pages/lotto/components/decision/decision.css b/src/pages/lotto/components/decision/decision.css index 73eb4c0..ab1721c 100644 --- a/src/pages/lotto/components/decision/decision.css +++ b/src/pages/lotto/components/decision/decision.css @@ -42,6 +42,11 @@ .lc-balls { display: flex; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; } .lc-set__reason { font-size: 12px; opacity: 0.7; line-height: 1.45; margin: 0; } .lc-actions { display: flex; gap: 10px; margin-top: 18px; } +.lc-btn { padding: 12px 16px; border-radius: 10px; border: none; font-weight: 600; cursor: pointer; + font-size: 14px; min-width: 160px; } +.lc-btn--prim { background: linear-gradient(135deg, #b8a8ff, #8a78db); color: #14101e; } +.lc-btn--prim:disabled { opacity: 0.5; cursor: not-allowed; } +.lc-btn--ghost { background: transparent; border: 1px solid rgba(255,255,255,0.15); color: #ece6f7; } @media (max-width: 480px) { .lc-toggle { grid-template-columns: repeat(2, 1fr); } } diff --git a/src/pages/lotto/tabs/BriefingTab.jsx b/src/pages/lotto/tabs/BriefingTab.jsx index 8e2210a..cdbf7d9 100644 --- a/src/pages/lotto/tabs/BriefingTab.jsx +++ b/src/pages/lotto/tabs/BriefingTab.jsx @@ -1,25 +1,18 @@ import useBriefing from '../hooks/useBriefing'; -import BriefingHeader from '../components/briefing/BriefingHeader'; -import BriefingSummary from '../components/briefing/BriefingSummary'; -import PickSetCard from '../components/briefing/PickSetCard'; +import useReview from '../hooks/useReview'; +import DecisionCard from '../components/decision/DecisionCard'; import BriefingEmpty from '../components/briefing/BriefingEmpty'; -import CuratorUsageFooter from '../components/briefing/CuratorUsageFooter'; export default function BriefingTab() { const { briefing, loading, error, regenerating, regenerate } = useBriefing(); + const { latest: review } = useReview(); if (loading) return

로딩 중...

; if (!briefing) return ; return (
- - -
-

이번 주 5세트

- {briefing.picks.map((p, i) => )} -
- +
); } From 0bf1233e96869e197db35e7655d7d0c926f55fb6 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 11 May 2026 09:03:10 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat(lotto):=20=EB=B6=84=EC=84=9D=ED=83=AD?= =?UTF-8?q?=20=E2=86=92=20=EC=9E=90=EB=A3=8C=EC=8B=A4=20=EB=9D=BC=EB=B2=A8?= =?UTF-8?q?=20+=20=EC=B2=AB=20=EC=A7=84=EC=9E=85=20=EB=AA=A8=EB=93=A0=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20=EC=A0=91=ED=9E=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/lotto/Functions.jsx | 2 +- src/pages/lotto/Lotto.css | 5 ++ src/pages/lotto/tabs/AnalysisTab.jsx | 74 ++++++++++++++++++---------- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/pages/lotto/Functions.jsx b/src/pages/lotto/Functions.jsx index 9fd5608..53560d2 100644 --- a/src/pages/lotto/Functions.jsx +++ b/src/pages/lotto/Functions.jsx @@ -7,7 +7,7 @@ import SwipeableView from '../../components/SwipeableView'; const TABS = [ { id: 'briefing', label: '🗓 이번 주 브리핑' }, - { id: 'analysis', label: '📊 분석·통계' }, + { id: 'analysis', label: '📚 자료실 / Deep Dive' }, { id: 'purchase', label: '💰 구매·성과' }, ]; diff --git a/src/pages/lotto/Lotto.css b/src/pages/lotto/Lotto.css index cdb6d45..4c86bd9 100644 --- a/src/pages/lotto/Lotto.css +++ b/src/pages/lotto/Lotto.css @@ -1526,3 +1526,8 @@ font-size: 13px; } } + +.lotto-section-fold { margin-bottom: 14px; } +.lotto-section-fold > summary { cursor: pointer; padding: 12px 16px; background: rgba(255,255,255,0.03); + border-radius: 10px; font-weight: 600; font-size: 14px; opacity: 0.85; } +.lotto-section-fold[open] > summary { margin-bottom: 12px; opacity: 1; } diff --git a/src/pages/lotto/tabs/AnalysisTab.jsx b/src/pages/lotto/tabs/AnalysisTab.jsx index ed98fde..3ef0d5e 100644 --- a/src/pages/lotto/tabs/AnalysisTab.jsx +++ b/src/pages/lotto/tabs/AnalysisTab.jsx @@ -40,18 +40,21 @@ export default function AnalysisTab() { {/* 종합 추론 번호 추천 */} - +
+ 종합 추론 추천 + +
- {/* 최신 회차 + 시뮬레이션 추천 */} -
- {/* Latest Draw */} + {/* 최신 회차 */} +
+ 최신 회차
@@ -87,8 +90,11 @@ export default function AnalysisTab() {

최신 회차 데이터가 없습니다.

)}
+
- {/* Simulation Picks */} + {/* Simulation Picks */} +
+ 시뮬레이션 추천
@@ -163,19 +169,24 @@ export default function AnalysisTab() { )}
-
+ {/* 이번 주 공략 리포트 */} - +
+ 이번 주 공략 리포트 + +
{/* 통계 분석 */} -
+
+ 통계 분석 +

Analysis

@@ -237,9 +248,12 @@ export default function AnalysisTab() {

)}
+
{/* 전체 번호 분포 */} -
+
+ 전체 회차 번호 분포 +

Distribution

@@ -263,12 +277,18 @@ export default function AnalysisTab() {

통계 데이터를 불러오지 못했습니다.

)}
+
{/* 내 번호 패턴 */} - +
+ 내 번호 패턴 + +
{/* 수동 추천 */} -
+
+ 수동 추천 +

Manual Recommendation

@@ -365,9 +385,12 @@ export default function AnalysisTab() {

아직 추천 결과가 없습니다.

)}
+
{/* 추천 히스토리 */} -
+
+ 추천 히스토리 +

History

@@ -423,6 +446,7 @@ export default function AnalysisTab() {
)}
+
); } From 4ef76f6ccea60f5931b101239bfaaa24d50ab11a Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 11 May 2026 09:05:22 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat(lotto):=20=EA=B5=AC=EB=A7=A4=ED=83=AD?= =?UTF-8?q?=EC=97=90=20=EC=9E=90=EB=8F=99=20=EC=B1=84=EC=A0=90=20=EC=9D=BC?= =?UTF-8?q?=EC=B9=98=EC=88=98=20=EB=B0=B0=EC=A7=80=20+=204=EB=93=B1?= =?UTF-8?q?=E2=86=91=20=ED=94=8C=EB=9E=98=EA=B7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/lotto/Lotto.css | 25 ++++++++++++++++---- src/pages/lotto/components/PurchasePanel.jsx | 9 +++++++ src/pages/lotto/hooks/usePurchases.js | 8 +++++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/pages/lotto/Lotto.css b/src/pages/lotto/Lotto.css index 4c86bd9..48d98c7 100644 --- a/src/pages/lotto/Lotto.css +++ b/src/pages/lotto/Lotto.css @@ -1020,7 +1020,7 @@ .lotto-purchase-list__head { display: grid; - grid-template-columns: 60px 100px 100px 100px minmax(0, 1fr) 120px; + grid-template-columns: 60px 100px 100px 100px minmax(0, 160px) minmax(0, 1fr) 120px; gap: 8px; padding: 10px 14px; font-size: 11px; @@ -1033,7 +1033,7 @@ .lotto-purchase-row { display: grid; - grid-template-columns: 60px 100px 100px 100px minmax(0, 1fr) 120px; + grid-template-columns: 60px 100px 100px 100px minmax(0, 160px) minmax(0, 1fr) 120px; gap: 8px; align-items: center; padding: 12px 14px; @@ -1068,6 +1068,21 @@ justify-content: flex-end; } +.lotto-purchase-row__hits { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 2px; + overflow: hidden; +} + +.hit-badge { display: inline-block; min-width: 16px; padding: 1px 4px; margin-right: 2px; + font-size: 10px; border-radius: 4px; background: rgba(255,255,255,0.06); text-align: center; } +.hit-badge.hit-3 { background: rgba(80, 200, 120, 0.2); color: #76e09a; } +.hit-badge.hit-4 { background: rgba(255, 200, 80, 0.25); color: #ffce6e; font-weight: 700; } +.hit-badge.hit-5, .hit-badge.hit-6 { background: rgba(255, 100, 130, 0.3); color: #ff8aa0; font-weight: 700; } +.prize-flag { font-size: 10px; color: #ff8aa0; margin-left: 6px; } + .is-pos { color: #97c9aa; } .is-neg { color: #f7a8a5; } .is-prize { color: #fdd4b1; } @@ -1098,8 +1113,8 @@ gap: 8px; } - .lotto-purchase-list__head span:nth-child(n+3):nth-child(-n+5), - .lotto-purchase-row span:nth-child(n+3):nth-child(-n+5) { + .lotto-purchase-list__head span:nth-child(n+3):nth-child(-n+6), + .lotto-purchase-row span:nth-child(n+3):nth-child(-n+6) { display: none; } @@ -1143,7 +1158,7 @@ .lotto-purchase-list__head, .lotto-purchase-row { - grid-template-columns: 56px 90px 90px minmax(0, 1fr) 100px; + grid-template-columns: 56px 90px 90px minmax(0, 120px) minmax(0, 1fr) 100px; } .lotto-purchase-list__head span:nth-child(4), diff --git a/src/pages/lotto/components/PurchasePanel.jsx b/src/pages/lotto/components/PurchasePanel.jsx index 5c3ccd7..91efe41 100644 --- a/src/pages/lotto/components/PurchasePanel.jsx +++ b/src/pages/lotto/components/PurchasePanel.jsx @@ -137,6 +137,7 @@ const PurchasePanel = ({ 투자금 당첨금 손익 + 채점 메모 @@ -152,6 +153,14 @@ const PurchasePanel = ({ = 0 ? 'is-pos' : 'is-neg'}> {net >= 0 ? '+' : ''}{fmtWon(net)} + + {(rec.results || []).map((r, i) => ( + {r.correct} + ))} + {(rec.results || []).some((r) => r.correct >= 4) && ( + 🚨 4등↑ 확인 필요 + )} + {rec.note || '-'}