Compare commits
2 Commits
ec5fee8429
...
e42b643731
| Author | SHA1 | Date | |
|---|---|---|---|
| e42b643731 | |||
| ee5700dc95 |
@@ -195,6 +195,11 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
|
.ao-sidepanel-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
.ao-sidepanel-close {
|
.ao-sidepanel-close {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -204,6 +209,18 @@
|
|||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
.ao-sidepanel-close:hover { color: #fff; }
|
.ao-sidepanel-close:hover { color: #fff; }
|
||||||
|
/* 전체 화면 토글 — 모바일 전용 (데스크톱에서는 숨김) */
|
||||||
|
.ao-sidepanel-expand {
|
||||||
|
display: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #888;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 4px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.ao-sidepanel-expand:hover { color: #fff; }
|
||||||
|
|
||||||
/* Tabs */
|
/* Tabs */
|
||||||
.ao-sidepanel-tabs {
|
.ao-sidepanel-tabs {
|
||||||
@@ -377,19 +394,31 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
top: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 55vh;
|
||||||
max-height: 55vh;
|
max-height: 55vh;
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-top: 1px solid #333;
|
border-top: 1px solid #333;
|
||||||
border-radius: 16px 16px 0 0;
|
border-radius: 16px 16px 0 0;
|
||||||
animation: slideUp 0.25s ease-out;
|
animation: slideUp 0.25s ease-out;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
transition: height 0.25s ease, max-height 0.25s ease, border-radius 0.25s ease;
|
||||||
|
}
|
||||||
|
/* 전체 화면으로 확장 */
|
||||||
|
.ao-sidepanel.expanded {
|
||||||
|
top: 0;
|
||||||
|
height: 100dvh;
|
||||||
|
max-height: 100dvh;
|
||||||
|
border-radius: 0;
|
||||||
|
border-top: none;
|
||||||
}
|
}
|
||||||
@keyframes slideUp {
|
@keyframes slideUp {
|
||||||
from { transform: translateY(100%); }
|
from { transform: translateY(100%); }
|
||||||
to { transform: translateY(0); }
|
to { transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ao-sidepanel-expand { display: inline-block; }
|
||||||
.ao-sidepanel-header { padding: 8px 12px; }
|
.ao-sidepanel-header { padding: 8px 12px; }
|
||||||
.ao-sidepanel-header::before {
|
.ao-sidepanel-header::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 116 KiB |
@@ -10,6 +10,7 @@ const TABS = ['Commands', 'Tasks', 'Tokens', 'Logs'];
|
|||||||
|
|
||||||
export default function SidePanel({ agentId, agentState, pendingTask, onClose, refreshTrigger }) {
|
export default function SidePanel({ agentId, agentState, pendingTask, onClose, refreshTrigger }) {
|
||||||
const [activeTab, setActiveTab] = useState('Commands');
|
const [activeTab, setActiveTab] = useState('Commands');
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
const meta = AGENT_META[agentId];
|
const meta = AGENT_META[agentId];
|
||||||
if (!meta) return null;
|
if (!meta) return null;
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ export default function SidePanel({ agentId, agentState, pendingTask, onClose, r
|
|||||||
: agentState?.state || 'unknown';
|
: agentState?.state || 'unknown';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ao-sidepanel">
|
<div className={`ao-sidepanel${expanded ? ' expanded' : ''}`}>
|
||||||
<div className="ao-sidepanel-header">
|
<div className="ao-sidepanel-header">
|
||||||
<div className="ao-sidepanel-agent">
|
<div className="ao-sidepanel-agent">
|
||||||
<div className="ao-sidepanel-icon" style={{ borderColor: meta.color }}>
|
<div className="ao-sidepanel-icon" style={{ borderColor: meta.color }}>
|
||||||
@@ -29,8 +30,18 @@ export default function SidePanel({ agentId, agentState, pendingTask, onClose, r
|
|||||||
<div className="ao-sidepanel-state">● {stateText}</div>
|
<div className="ao-sidepanel-state">● {stateText}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="ao-sidepanel-actions">
|
||||||
|
<button
|
||||||
|
className="ao-sidepanel-expand"
|
||||||
|
onClick={() => setExpanded(e => !e)}
|
||||||
|
aria-label={expanded ? '축소' : '전체 화면'}
|
||||||
|
title={expanded ? '축소' : '전체 화면'}
|
||||||
|
>
|
||||||
|
{expanded ? '⤡' : '⤢'}
|
||||||
|
</button>
|
||||||
<button className="ao-sidepanel-close" onClick={onClose}>×</button>
|
<button className="ao-sidepanel-close" onClick={onClose}>×</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="ao-sidepanel-tabs">
|
<div className="ao-sidepanel-tabs">
|
||||||
{TABS.map(tab => (
|
{TABS.map(tab => (
|
||||||
|
|||||||
@@ -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} />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user