StockTrade 탭 컴포넌트 분리 (Phase 5+6): 1,932→210줄
5개 탭/드로어 컴포넌트를 components/ 디렉토리로 추출: - PortfolioTab: 포트폴리오 관리, 예수금, 자산추이 차트 - AiTradeTab: AI 모의투자 잔고, 수동주문, KIS 모달 - ReportTab: 차트, 리스크 분석, 수익률 랭킹, AI 코치 - AdvisorTab: 프롬프트 빌더, 클립보드 복사 - SellHistoryDrawer: 실현손익 드로어, 필터, 폼 StockTrade.jsx는 210줄 오케스트레이터로 축소 (hooks 호출 + lazy load + 헤더 + 탭 바 + 탭 렌더) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
220
src/pages/stock/components/AiTradeTab.jsx
Normal file
220
src/pages/stock/components/AiTradeTab.jsx
Normal file
@@ -0,0 +1,220 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user