dashboard 형태의 UI 수정 및 고도화
This commit is contained in:
@@ -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 & 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">
|
||||
|
||||
Reference in New Issue
Block a user