Files
web-page/src/pages/stock/components/AiTradeTab.jsx
gahusb 2b463682d5 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>
2026-04-03 07:37:30 +09:00

221 lines
9.8 KiB
JavaScript

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;