dashboard 형태의 UI 수정 및 고도화

This commit is contained in:
2026-03-04 08:29:39 +09:00
parent 618d5f8e6f
commit ccc9f7c634
17 changed files with 1296 additions and 224 deletions

View File

@@ -9,7 +9,6 @@ import {
deletePortfolio,
upsertCash,
deleteCash,
getFearAndGreed,
} from '../../api';
import Loading from '../../components/Loading';
import './Stock.css';
@@ -81,24 +80,6 @@ const toNumeric = (value) => {
return Number.isNaN(numeric) ? null : numeric;
};
/* ── Fear & Greed helpers ──────────────────────────────────────── */
const getFgColor = (score) => {
if (score <= 25) return '#ef4444';
if (score <= 45) return '#f97316';
if (score <= 55) return '#eab308';
if (score <= 75) return '#84cc16';
return '#22c55e';
};
const getFgLabel = (score) => {
if (score <= 25) return '극단적 공포';
if (score <= 45) return '공포';
if (score <= 55) return '중립';
if (score <= 75) return '탐욕';
return '극단적 탐욕';
};
/* ── Chart colors ──────────────────────────────────────────────── */
const CHART_COLORS = ['#818cf8', '#fbbf24', '#34d399', '#f472b6', '#fb923c', '#a78bfa', '#38bdf8', '#4ade80'];
@@ -130,7 +111,7 @@ const TAB_REPORT = 'report';
const StockTrade = () => {
/* Active tab */
const [activeTab, setActiveTab] = useState(TAB_PORTFOLIO);
const [activeTab, setActiveTab] = useState(TAB_REPORT);
/* ────────────────────────────────────────────────────────────── */
/* 쟁승토리 계좌 (Portfolio) state */
@@ -166,12 +147,6 @@ const StockTrade = () => {
const [reportSortField, setReportSortField] = useState('profit_rate');
const [reportSortDir, setReportSortDir] = useState('desc');
/* Fear & Greed */
const [fgData, setFgData] = useState(null);
const [fgLoading, setFgLoading] = useState(false);
const [fgError, setFgError] = useState('');
const [fgLoaded, setFgLoaded] = useState(false);
/* AI Coach */
const [aiApiKey, setAiApiKey] = useState('');
const [aiModel, setAiModel] = useState('claude-haiku-4-5-20251001');
@@ -215,23 +190,6 @@ const StockTrade = () => {
}
}, []);
const loadFearAndGreed = useCallback(async () => {
setFgLoading(true);
setFgError('');
try {
const data = await getFearAndGreed();
const fg = data?.fear_and_greed ?? data;
const score = typeof fg?.score === 'number' ? fg.score : parseFloat(fg?.score);
if (isNaN(score)) throw new Error('지수 데이터 형식 오류');
setFgData({ score, rating: fg.rating ?? '', timestamp: fg.timestamp ?? null });
setFgLoaded(true);
} catch (err) {
setFgError('F&G 지수 조회 실패: ' + (err?.message ?? String(err)));
} finally {
setFgLoading(false);
}
}, []);
const loadBalance = useCallback(async () => {
setBalanceLoading(true);
setBalanceError('');
@@ -257,13 +215,6 @@ const StockTrade = () => {
}
}, [activeTab, portfolioLoaded, balanceLoaded, loadPortfolio, loadBalance]);
/* Fear & Greed: 리포트 탭 첫 진입 시 자동 로드 */
useEffect(() => {
if (activeTab === TAB_REPORT && !fgLoaded) {
loadFearAndGreed();
}
}, [activeTab, fgLoaded, loadFearAndGreed]);
/* AI Coach: 마운트 시 localStorage에서 API Key + 오늘 캐시 복원 */
useEffect(() => {
const savedKey = localStorage.getItem('ai_coach_key') ?? '';
@@ -1454,63 +1405,6 @@ ${holdingsText}
)}
{portfolioError && <p className="stock-error">{portfolioError}</p>}
{/* ── Fear & Greed Index ─────────────────────────── */}
<section className="stock-panel stock-panel--wide">
<div className="stock-panel__head">
<div>
<p className="stock-panel__eyebrow">시장 심리 지표</p>
<h3>Fear & Greed Index</h3>
<p className="stock-panel__sub">
CNN Fear &amp; Greed 지수로 현재 시장 심리를 파악합니다.
</p>
</div>
<div className="stock-panel__actions">
<button
className="button ghost small"
onClick={loadFearAndGreed}
disabled={fgLoading}
>
{fgLoading ? '조회 중...' : '새로고침'}
</button>
</div>
</div>
{fgError && <p className="stock-error">{fgError}</p>}
{fgData ? (
<div className="fg-panel">
<div className="fg-gauge">
<div className="fg-gauge__track">
<div
className="fg-gauge__needle"
style={{ left: `${Math.min(100, Math.max(0, fgData.score))}%` }}
/>
</div>
<div className="fg-gauge__labels">
<span>극단적 공포</span>
<span>공포</span>
<span>중립</span>
<span>탐욕</span>
<span>극단적 탐욕</span>
</div>
</div>
<div className="fg-score-display">
<span className="fg-score-number" style={{ color: getFgColor(fgData.score) }}>
{Math.round(fgData.score)}
</span>
<span className="fg-score-label" style={{ color: getFgColor(fgData.score) }}>
{getFgLabel(fgData.score)}
</span>
{fgData.timestamp && (
<span className="fg-score-date">
{new Date(fgData.timestamp).toLocaleDateString('ko-KR')}
</span>
)}
</div>
</div>
) : !fgError ? (
<p className="stock-empty">지수 데이터를 불러오는 ...</p>
) : null}
</section>
{/* ── 자산 배분 + 수익률 차트 ────────────────────── */}
{portfolioHoldings.length > 0 && (
<section className="stock-panel stock-panel--wide">