refactor(stock): 거래 데스크에서 AI 투자 탭 제거

TAB_AI 탭과 관련 컴포넌트(AiTradeTab)·훅(useAiBalance) 삭제. 헤더 카드는
aib 모의투자 요약 분기를 제거하고 항상 포트폴리오 요약을 표시.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 08:30:44 +09:00
parent ee5700dc95
commit e42b643731
4 changed files with 29 additions and 356 deletions

View File

@@ -6,7 +6,7 @@ import SwipeableView from '../../components/SwipeableView';
import { import {
formatNumber, formatPercent, formatNumber, formatPercent,
toNumeric, profitColorClass, toNumeric, profitColorClass,
TAB_PORTFOLIO, TAB_AI, TAB_REPORT, TAB_ADVISOR, TAB_PORTFOLIO, TAB_REPORT, TAB_ADVISOR,
} from './stockUtils'; } from './stockUtils';
/* ── hooks ──────────────────────────────────────────────────────── */ /* ── hooks ──────────────────────────────────────────────────────── */
@@ -15,13 +15,11 @@ import useSellHistory from './hooks/useSellHistory';
import useAiCoach from './hooks/useAiCoach'; import useAiCoach from './hooks/useAiCoach';
import useAssetHistory from './hooks/useAssetHistory'; import useAssetHistory from './hooks/useAssetHistory';
import useMarketContext from './hooks/useMarketContext'; import useMarketContext from './hooks/useMarketContext';
import useAiBalance from './hooks/useAiBalance';
import useReportData from './hooks/useReportData'; import useReportData from './hooks/useReportData';
import useAdvisor from './hooks/useAdvisor'; import useAdvisor from './hooks/useAdvisor';
/* ── tab components ─────────────────────────────────────────────── */ /* ── tab components ─────────────────────────────────────────────── */
import PortfolioTab from './components/PortfolioTab'; import PortfolioTab from './components/PortfolioTab';
import AiTradeTab from './components/AiTradeTab';
import ReportTab from './components/ReportTab'; import ReportTab from './components/ReportTab';
import AdvisorTab from './components/AdvisorTab'; import AdvisorTab from './components/AdvisorTab';
import SellHistoryDrawer from './components/SellHistoryDrawer'; import SellHistoryDrawer from './components/SellHistoryDrawer';
@@ -32,8 +30,8 @@ const StockTrade = () => {
const [activeTab, setActiveTab] = React.useState(TAB_REPORT); const [activeTab, setActiveTab] = React.useState(TAB_REPORT);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const TAB_ORDER = [TAB_PORTFOLIO, TAB_AI, TAB_REPORT, TAB_ADVISOR]; const TAB_ORDER = [TAB_PORTFOLIO, TAB_REPORT, TAB_ADVISOR];
const tabLabels = ['포트폴리오', 'AI 트레이드', '리포트', '어드바이저']; const tabLabels = ['포트폴리오', '리포트', '어드바이저'];
const tabIndex = TAB_ORDER.indexOf(activeTab); const tabIndex = TAB_ORDER.indexOf(activeTab);
const handleTabChange = useCallback((idx) => setActiveTab(TAB_ORDER[idx]), []); // eslint-disable-line react-hooks/exhaustive-deps const handleTabChange = useCallback((idx) => setActiveTab(TAB_ORDER[idx]), []); // eslint-disable-line react-hooks/exhaustive-deps
@@ -49,7 +47,6 @@ const StockTrade = () => {
totalAssets: pf.totalAssets, totalAssets: pf.totalAssets,
marketCtx, marketCtx,
}); });
const aib = useAiBalance();
const report = useReportData({ const report = useReportData({
portfolioHoldings: pf.portfolioHoldings, portfolioHoldings: pf.portfolioHoldings,
portfolioSummary: pf.portfolioSummary, portfolioSummary: pf.portfolioSummary,
@@ -97,12 +94,10 @@ const StockTrade = () => {
if (activeTab === TAB_PORTFOLIO && !pf.portfolioLoaded) { if (activeTab === TAB_PORTFOLIO && !pf.portfolioLoaded) {
pf.loadPortfolio(); pf.loadPortfolio();
sell.loadSellHistory(); sell.loadSellHistory();
} else if (activeTab === TAB_AI && !aib.balanceLoaded) {
aib.loadBalance();
} else if ((activeTab === TAB_REPORT || activeTab === TAB_ADVISOR) && !pf.portfolioLoaded) { } else if ((activeTab === TAB_REPORT || activeTab === TAB_ADVISOR) && !pf.portfolioLoaded) {
pf.loadPortfolio(); pf.loadPortfolio();
} }
}, [activeTab, pf.portfolioLoaded, aib.balanceLoaded]); // eslint-disable-line react-hooks/exhaustive-deps }, [activeTab, pf.portfolioLoaded]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => { useEffect(() => {
if (activeTab === TAB_PORTFOLIO) asset.loadAssetHistory(asset.assetHistoryDays); if (activeTab === TAB_PORTFOLIO) asset.loadAssetHistory(asset.assetHistoryDays);
@@ -135,10 +130,7 @@ const StockTrade = () => {
</div> </div>
</div> </div>
<div className="stock-card"> <div className="stock-card">
<p className="stock-card__title"> <p className="stock-card__title">쟁승토리 계좌 요약</p>
{activeTab === TAB_AI ? 'AI 투자 요약' : '쟁승토리 계좌 요약'}
</p>
{activeTab === TAB_PORTFOLIO || activeTab === TAB_REPORT ? (
<div className="stock-status"> <div className="stock-status">
<div><span> 매입</span><strong>{formatNumber(pf.portfolioSummary.total_buy)}</strong></div> <div><span> 매입</span><strong>{formatNumber(pf.portfolioSummary.total_buy)}</strong></div>
<div><span> 평가</span><strong>{formatNumber(pf.portfolioSummary.total_eval)}</strong></div> <div><span> 평가</span><strong>{formatNumber(pf.portfolioSummary.total_eval)}</strong></div>
@@ -161,16 +153,6 @@ const StockTrade = () => {
<div><span> 자산</span><strong style={{ fontWeight: 700 }}>{formatNumber(pf.totalAssets)}</strong></div> <div><span> 자산</span><strong style={{ fontWeight: 700 }}>{formatNumber(pf.totalAssets)}</strong></div>
)} )}
</div> </div>
) : (
<div className="stock-status">
<div><span> 평가금액</span><strong>{formatNumber(aib.totalEval)}</strong></div>
<div><span>예수금</span><strong>{formatNumber(aib.deposit)}</strong></div>
<div><span>보유 종목</span><strong>{aib.holdings.length}</strong></div>
</div>
)}
{activeTab === TAB_AI && aib.summary.note ? (
<p className="stock-status__note">{aib.summary.note}</p>
) : null}
</div> </div>
</header> </header>
@@ -182,8 +164,6 @@ const StockTrade = () => {
label: tabLabels[i], label: tabLabels[i],
content: tabId === TAB_PORTFOLIO content: tabId === TAB_PORTFOLIO
? <PortfolioTab pf={pf} asset={asset} handleSell={handleSell} handleSaveSnapshot={handleSaveSnapshot} /> ? <PortfolioTab pf={pf} asset={asset} handleSell={handleSell} handleSaveSnapshot={handleSaveSnapshot} />
: tabId === TAB_AI
? <AiTradeTab aib={aib} />
: tabId === TAB_REPORT : tabId === TAB_REPORT
? <ReportTab pf={pf} report={report} ai={ai} marketCtx={marketCtx} /> ? <ReportTab pf={pf} report={report} ai={ai} marketCtx={marketCtx} />
: <AdvisorTab pf={pf} advisor={advisor} />, : <AdvisorTab pf={pf} advisor={advisor} />,
@@ -196,7 +176,6 @@ const StockTrade = () => {
<div className="stock-main-tabs"> <div className="stock-main-tabs">
{[ {[
{ id: TAB_PORTFOLIO, icon: '💼', label: '쟁승토리 계좌', badge: pf.portfolioHoldings.length || null }, { id: TAB_PORTFOLIO, icon: '💼', label: '쟁승토리 계좌', badge: pf.portfolioHoldings.length || null },
{ id: TAB_AI, icon: '🤖', label: 'AI 투자', sub: '모의투자' },
{ id: TAB_REPORT, icon: '📊', label: '리포트', sub: '분석·AI코치' }, { id: TAB_REPORT, icon: '📊', label: '리포트', sub: '분석·AI코치' },
{ id: TAB_ADVISOR, icon: '🧠', label: 'AI 어드바이저', sub: 'Gemini Pro', className: 'stock-main-tab--advisor' }, { id: TAB_ADVISOR, icon: '🧠', label: 'AI 어드바이저', sub: 'Gemini Pro', className: 'stock-main-tab--advisor' },
].map(({ id, icon, label, sub, badge, className: cls }) => ( ].map(({ id, icon, label, sub, badge, className: cls }) => (
@@ -217,7 +196,6 @@ const StockTrade = () => {
{activeTab === TAB_PORTFOLIO && ( {activeTab === TAB_PORTFOLIO && (
<PortfolioTab pf={pf} asset={asset} handleSell={handleSell} handleSaveSnapshot={handleSaveSnapshot} /> <PortfolioTab pf={pf} asset={asset} handleSell={handleSell} handleSaveSnapshot={handleSaveSnapshot} />
)} )}
{activeTab === TAB_AI && <AiTradeTab aib={aib} />}
{activeTab === TAB_REPORT && <ReportTab pf={pf} report={report} ai={ai} marketCtx={marketCtx} />} {activeTab === TAB_REPORT && <ReportTab pf={pf} report={report} ai={ai} marketCtx={marketCtx} />}
{activeTab === TAB_ADVISOR && <AdvisorTab pf={pf} advisor={advisor} />} {activeTab === TAB_ADVISOR && <AdvisorTab pf={pf} advisor={advisor} />}
</> </>

View File

@@ -1,220 +0,0 @@
import React from 'react';
import {
formatNumber, formatPercent,
getQty, getBuyPrice, getCurrentPrice, getProfitRate, getProfitLoss,
toNumeric, profitColorClass,
} from '../stockUtils';
const AiTradeTab = ({ aib }) => (
<>
{aib.balanceError ? <p className="stock-error">{aib.balanceError}</p> : null}
{/* AI Balance section */}
<section className="stock-panel stock-panel--wide">
<div className="stock-panel__head">
<div>
<p className="stock-panel__eyebrow">AI 모의투자</p>
<h3>보유 현황</h3>
<p className="stock-panel__sub">
AI가 운용 중인 모의투자 계좌의 잔고와 보유 종목을 확인합니다.
</p>
</div>
<div className="stock-panel__actions">
{aib.balanceLoading ? (
<span className="stock-chip">조회 </span>
) : null}
<button
className="button ghost small"
onClick={aib.loadBalance}
disabled={aib.balanceLoading}
>
새로고침
</button>
</div>
</div>
<div className="stock-balance">
<div className="stock-balance__summary">
{[
{ label: '총 평가', value: aib.totalEval },
{ label: '예수금', value: aib.deposit },
].map((item) => (
<div key={item.label} className="stock-balance__card">
<span>{item.label}</span>
<strong>{formatNumber(item.value)}</strong>
</div>
))}
</div>
{aib.holdings.length ? (
<div className="stock-holdings">
{aib.holdings.map((item, idx) => {
const profitLoss = getProfitLoss(item);
const profitLossNumeric = toNumeric(profitLoss);
const profitClass = profitColorClass(profitLossNumeric);
const profitRate = getProfitRate(item);
const profitRateNumeric = toNumeric(profitRate);
const profitRateClass = profitColorClass(profitRateNumeric);
return (
<div
key={item.code ?? `${item.name}-${idx}`}
className="stock-holdings__item"
>
<div>
<p className="stock-holdings__name">
{item.name ?? item.code ?? 'N/A'}
</p>
<span className="stock-holdings__code">
{item.code ?? ''}
</span>
</div>
<div className="stock-holdings__metric">
<span>수량</span>
<strong>{formatNumber(getQty(item))}</strong>
</div>
<div className="stock-holdings__metric">
<span>매입가</span>
<strong>{formatNumber(getBuyPrice(item))}</strong>
</div>
<div className="stock-holdings__metric">
<span>현재가</span>
<strong>{formatNumber(getCurrentPrice(item))}</strong>
</div>
<div className="stock-holdings__metric">
<span>평가금액</span>
<strong>
{getCurrentPrice(item) != null && getQty(item) != null
? formatNumber(toNumeric(getCurrentPrice(item)) * toNumeric(getQty(item)))
: '-'}
</strong>
</div>
<div className="stock-holdings__metric">
<span>수익률</span>
<strong className={`stock-profit ${profitRateClass}`}>
{formatPercent(profitRate)}
</strong>
</div>
<div className="stock-holdings__metric">
<span>평가손익</span>
<strong className={`stock-profit ${profitClass}`}>
{formatNumber(profitLoss)}
</strong>
</div>
</div>
);
})}
</div>
) : (
<p className="stock-empty">보유 종목이 없습니다.</p>
)}
</div>
</section>
{/* Manual order section */}
<section className="stock-panel stock-panel--wide">
<div className="stock-panel__head">
<div>
<p className="stock-panel__eyebrow">수동 주문</p>
<h3>직접 매수/매도</h3>
<p className="stock-panel__sub">
종목명 또는 종목코드를 입력하고 매수/매도 주문을 요청합니다.
</p>
</div>
</div>
<form className="stock-order" onSubmit={aib.submitManualOrder}>
<label>
종목명/코드
<input
type="text"
value={aib.manualForm.code}
onChange={(e) =>
aib.setManualForm((prev) => ({ ...prev, code: e.target.value }))
}
placeholder="005930 또는 삼성전자"
required
/>
</label>
<label>
매수/매도
<select
value={aib.manualForm.type}
onChange={(e) =>
aib.setManualForm((prev) => ({ ...prev, type: e.target.value }))
}
>
<option value="buy">매수</option>
<option value="sell">매도</option>
</select>
</label>
<label>
수량
<input
type="number"
min={1}
step={1}
value={aib.manualForm.qty}
onChange={(e) =>
aib.setManualForm((prev) => ({ ...prev, qty: Number(e.target.value) }))
}
required
/>
</label>
<label>
금액()
<input
type="number"
min={0}
step={1}
value={aib.manualForm.price}
onChange={(e) =>
aib.setManualForm((prev) => ({ ...prev, price: Number(e.target.value) }))
}
/>
</label>
<button
className="button primary"
type="submit"
disabled={aib.manualLoading}
>
{aib.manualLoading ? '요청 중...' : '주문 요청'}
</button>
{aib.manualError ? (
<p className="stock-error">{aib.manualError}</p>
) : null}
{aib.manualResult ? (
<div className="stock-result">
<p className="stock-result__title">요청 결과</p>
<pre>
{typeof aib.manualResult === 'string'
? aib.manualResult
: JSON.stringify(aib.manualResult, null, 2)}
</pre>
</div>
) : null}
</form>
</section>
{/* KIS modal */}
{aib.kisModal ? (
<div className="stock-modal" role="dialog" aria-modal="true">
<div
className="stock-modal__backdrop"
onClick={() => aib.setKisModal('')}
/>
<div className="stock-modal__card">
<div className="stock-modal__head">
<h4>주문 결과</h4>
<button
type="button"
className="button ghost small"
onClick={() => aib.setKisModal('')}
>
닫기
</button>
</div>
<pre>{aib.kisModal}</pre>
</div>
</div>
) : null}
</>
);
export default AiTradeTab;

View File

@@ -1,84 +0,0 @@
import { useState, useCallback, useMemo } from 'react';
import { getTradeBalance, createTradeOrder } from '../../../api';
import { getQty, getBuyPrice, getCurrentPrice, getProfitRate, getProfitLoss } from '../stockUtils';
export default function useAiBalance() {
const [balance, setBalance] = useState(null);
const [balanceLoading, setBalanceLoading] = useState(false);
const [balanceError, setBalanceError] = useState('');
const [balanceLoaded, setBalanceLoaded] = useState(false);
const [manualForm, setManualForm] = useState({
code: '',
qty: 1,
price: 0,
type: 'buy',
});
const [manualLoading, setManualLoading] = useState(false);
const [manualError, setManualError] = useState('');
const [manualResult, setManualResult] = useState(null);
const [kisModal, setKisModal] = useState('');
const loadBalance = useCallback(async () => {
setBalanceLoading(true);
setBalanceError('');
try {
const data = await getTradeBalance();
setBalance(data);
setBalanceLoaded(true);
} catch (err) {
setBalanceError(err?.message ?? String(err));
} finally {
setBalanceLoading(false);
}
}, []);
const submitManualOrder = async (event) => {
event.preventDefault();
setManualLoading(true);
setManualError('');
setManualResult(null);
try {
const payload = {
ticker: manualForm.code.trim(),
action: manualForm.type === 'sell' ? 'SELL' : 'BUY',
quantity: Number(manualForm.qty),
price: Number(manualForm.price),
};
const result = await createTradeOrder(payload);
setManualResult(result ?? { ok: true });
if (result?.kis_result !== undefined) {
const message =
typeof result.kis_result === 'string'
? result.kis_result
: JSON.stringify(result.kis_result, null, 2);
setKisModal(message);
}
await loadBalance();
} catch (err) {
setManualError(err?.message ?? String(err));
} finally {
setManualLoading(false);
}
};
/* derived */
const holdings = useMemo(() => {
if (!balance) return [];
if (Array.isArray(balance.holdings)) return balance.holdings;
if (Array.isArray(balance.positions)) return balance.positions;
if (Array.isArray(balance.items)) return balance.items;
return [];
}, [balance]);
const summary = balance?.summary ?? {};
const totalEval = summary.total_eval ?? balance?.total_eval ?? balance?.total_value;
const deposit = summary.deposit ?? balance?.deposit ?? balance?.available_cash;
return {
balance, balanceLoading, balanceError, balanceLoaded, loadBalance,
holdings, summary, totalEval, deposit,
manualForm, setManualForm, manualLoading, manualError, manualResult,
kisModal, setKisModal, submitManualOrder,
};
}

View File

@@ -128,6 +128,5 @@ export const emptySellForm = () => ({
/* ── TAB IDs ─────────────────────────────────────────────────────── */ /* ── TAB IDs ─────────────────────────────────────────────────────── */
export const TAB_PORTFOLIO = 'portfolio'; export const TAB_PORTFOLIO = 'portfolio';
export const TAB_AI = 'ai';
export const TAB_REPORT = 'report'; export const TAB_REPORT = 'report';
export const TAB_ADVISOR = 'advisor'; export const TAB_ADVISOR = 'advisor';