업비트 스타일 매수/매도 탭 주문 패널 적용
- TradeTabOrderPanel 신규 생성: 매수|매도|간편주문|거래내역 탭 - 매수 탭: 빨간 하단 인디케이터, 매도 탭: 파란 하단 인디케이터 - 간편주문: 시장가 즉시체결 (KRW 금액 입력 + % 버튼) - 거래내역: historySlot prop으로 외부 콘텐츠 주입 - VirtualTradingPage: 외부 '거래' 탭 제거 → 거래내역 내부 탭으로 이동 - PaperTradingPage: 미체결/체결/원장 거래내역 내부 탭으로 이동 - TradeNotificationListPage·RightSidePanel: TradeTabOrderPanel 적용 - tradeRightPanel.css: ttop-* · qop-* 스타일 추가 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -23,7 +23,7 @@ import PaperPendingOrdersList from './paper/PaperPendingOrdersList';
|
||||
import PaperLedgerTab from './paper/PaperLedgerTab';
|
||||
import PaperAssetTrendChart from './paper/PaperAssetTrendChart';
|
||||
import { computePaperMetrics } from '../utils/paperMetrics';
|
||||
import { TradeSplitOrderPanel, TradeOrderbookPanelContent, TradeRightPanelBody } from './trade';
|
||||
import { TradeTabOrderPanel, TradeOrderbookPanelContent, TradeRightPanelBody } from './trade';
|
||||
import type { Theme, TradeOrderFillRequest } from '../types';
|
||||
import PaperAnalysisChart from './paper/PaperAnalysisChart';
|
||||
import PaperLeftStrategyTab from './paper/PaperLeftStrategyTab';
|
||||
@@ -38,7 +38,7 @@ import { formatVirtualTargetOptionLabel } from '../utils/virtualTargetNames';
|
||||
|
||||
type HistoryTab = 'open' | 'recent' | 'ledger';
|
||||
type CenterTab = 'invest' | 'chart';
|
||||
type RightTab = 'trade' | 'orderbook' | 'history';
|
||||
type RightTab = 'trade' | 'orderbook';
|
||||
type LeftTab = 'strategy' | 'settings';
|
||||
|
||||
interface Props {
|
||||
@@ -140,7 +140,7 @@ const PaperTradingPage: React.FC<Props> = ({
|
||||
} finally {
|
||||
setInitialLoading(false);
|
||||
}
|
||||
if (opts?.focusHistory) setRightTab('history');
|
||||
if (opts?.focusHistory) setRightTab('trade');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -293,9 +293,33 @@ const PaperTradingPage: React.FC<Props> = ({
|
||||
<div className="bps-right-tabs">
|
||||
<button type="button" className={`bps-right-tab${rightTab === 'trade' ? ' bps-right-tab--on' : ''}`} onClick={() => setRightTab('trade')}>매매</button>
|
||||
<button type="button" className={`bps-right-tab${rightTab === 'orderbook' ? ' bps-right-tab--on' : ''}`} onClick={() => setRightTab('orderbook')}>호가</button>
|
||||
<button type="button" className={`bps-right-tab${rightTab === 'history' ? ' bps-right-tab--on' : ''}`} onClick={() => setRightTab('history')}>
|
||||
거래{trades.length > 0 ? ` (${trades.length})` : ''}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const tradeHistorySlot = (
|
||||
<div className="ptd-right-history">
|
||||
<div className="ptd-tabs ptd-tabs--history">
|
||||
<button type="button" className={`ptd-tab${historyTab === 'open' ? ' active' : ''}`} onClick={() => setHistoryTab('open')}>미체결</button>
|
||||
<button type="button" className={`ptd-tab${historyTab === 'recent' ? ' active' : ''}`} onClick={() => setHistoryTab('recent')}>체결{trades.length > 0 ? ` (${trades.length})` : ''}</button>
|
||||
<button type="button" className={`ptd-tab${historyTab === 'ledger' ? ' active' : ''}`} onClick={() => setHistoryTab('ledger')}>원장</button>
|
||||
</div>
|
||||
<div className="ptd-right-history-body">
|
||||
{historyTab === 'open' ? (
|
||||
<PaperPendingOrdersList orders={pendingOrders} onChanged={() => void loadData()} />
|
||||
) : historyTab === 'ledger' ? (
|
||||
<PaperLedgerTab refreshKey={dataRefreshKey} />
|
||||
) : (
|
||||
<PaperTradeHistoryList
|
||||
trades={trades}
|
||||
strategyNames={strategyNames}
|
||||
tickers={tickers}
|
||||
selectedTradeId={selectedTradeId}
|
||||
onSelectTrade={handleTradeHistorySelect}
|
||||
emptyText="체결 내역이 없습니다."
|
||||
className="ptd-trade-history--fill"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -421,7 +445,7 @@ const PaperTradingPage: React.FC<Props> = ({
|
||||
right={(
|
||||
<TradeRightPanelBody anchorRef={orderAnchorRef} embeddedInShell>
|
||||
{rightTab === 'trade' ? (
|
||||
<TradeSplitOrderPanel
|
||||
<TradeTabOrderPanel
|
||||
market={selectedMarket}
|
||||
tradePrice={tradePrice}
|
||||
availableKrw={orderableCash}
|
||||
@@ -433,8 +457,9 @@ const PaperTradingPage: React.FC<Props> = ({
|
||||
paperTradingEnabled={paperTradingEnabled}
|
||||
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
||||
onPaperOrderFilled={() => void loadData({ focusHistory: true })}
|
||||
historySlot={tradeHistorySlot}
|
||||
/>
|
||||
) : rightTab === 'orderbook' ? (
|
||||
) : (
|
||||
<TradeOrderbookPanelContent
|
||||
market={selectedMarket}
|
||||
asks={orderbook.asks}
|
||||
@@ -452,31 +477,6 @@ const PaperTradingPage: React.FC<Props> = ({
|
||||
tradeStrength={tradeStrength ?? undefined}
|
||||
onRowClick={handleOrderbookRowClick}
|
||||
/>
|
||||
) : (
|
||||
<div className="ptd-right-history">
|
||||
<div className="ptd-tabs ptd-tabs--history">
|
||||
<button type="button" className={`ptd-tab${historyTab === 'open' ? ' active' : ''}`} onClick={() => setHistoryTab('open')}>미체결</button>
|
||||
<button type="button" className={`ptd-tab${historyTab === 'recent' ? ' active' : ''}`} onClick={() => setHistoryTab('recent')}>체결</button>
|
||||
<button type="button" className={`ptd-tab${historyTab === 'ledger' ? ' active' : ''}`} onClick={() => setHistoryTab('ledger')}>원장</button>
|
||||
</div>
|
||||
<div className="ptd-right-history-body">
|
||||
{historyTab === 'open' ? (
|
||||
<PaperPendingOrdersList orders={pendingOrders} onChanged={() => void loadData()} />
|
||||
) : historyTab === 'ledger' ? (
|
||||
<PaperLedgerTab refreshKey={dataRefreshKey} />
|
||||
) : (
|
||||
<PaperTradeHistoryList
|
||||
trades={trades}
|
||||
strategyNames={strategyNames}
|
||||
tickers={tickers}
|
||||
selectedTradeId={selectedTradeId}
|
||||
onSelectTrade={handleTradeHistorySelect}
|
||||
emptyText="체결 내역이 없습니다."
|
||||
className="ptd-trade-history--fill"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TradeRightPanelBody>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import {
|
||||
TradeSplitOrderPanel,
|
||||
TradeTabOrderPanel,
|
||||
TradeOrderbookPanelContent,
|
||||
TradeRightPanelTabs,
|
||||
TradeRightPanelBody,
|
||||
@@ -158,7 +158,7 @@ const RightSidePanel: React.FC<RightSidePanelProps> = ({
|
||||
|
||||
<TradeRightPanelBody anchorRef={tradeSectionRef} className="rsp-body-wrap">
|
||||
{activeTab === 'trade' && (
|
||||
<TradeSplitOrderPanel
|
||||
<TradeTabOrderPanel
|
||||
market={market}
|
||||
tradePrice={tradePrice}
|
||||
availableKrw={availableKrw}
|
||||
@@ -169,6 +169,7 @@ const RightSidePanel: React.FC<RightSidePanelProps> = ({
|
||||
paperTradingEnabled={paperTradingEnabled}
|
||||
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
||||
onPaperOrderFilled={onPaperOrderFilled}
|
||||
hideHistory
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import BacktestAnalysisReportModal from './backtest/BacktestAnalysisReportModal'
|
||||
import type { BacktestAnalysisReportModel } from './backtest/BacktestAnalysisReportModal';
|
||||
import { coerceFiniteNumber } from '../utils/safeFormat';
|
||||
import {
|
||||
TradeSplitOrderPanel,
|
||||
TradeTabOrderPanel,
|
||||
TradeOrderbookPanelContent,
|
||||
TradeRightPanelTabs,
|
||||
TradeRightPanelBody,
|
||||
@@ -753,7 +753,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
||||
bodyClassName="tnl-right-body"
|
||||
>
|
||||
{rightTab === 'trade' ? (
|
||||
<TradeSplitOrderPanel
|
||||
<TradeTabOrderPanel
|
||||
market={selectedMarket}
|
||||
tradePrice={tradePrice}
|
||||
availableKrw={coerceFiniteNumber(summary?.cashBalance) ?? 0}
|
||||
@@ -764,6 +764,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
||||
paperTradingEnabled={paperTradingEnabled}
|
||||
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
||||
onPaperOrderFilled={() => void refreshPaperData()}
|
||||
hideHistory
|
||||
/>
|
||||
) : (
|
||||
<TradeOrderbookPanelContent
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '../utils/backendApi';
|
||||
import type { Theme, TradeOrderFillRequest } from '../types';
|
||||
import type { TickerData } from '../hooks/useMarketTicker';
|
||||
import { TradeSplitOrderPanel, TradeOrderbookPanelContent, TradeRightPanelBody } from './trade';
|
||||
import { TradeTabOrderPanel, TradeOrderbookPanelContent, TradeRightPanelBody } from './trade';
|
||||
import { useUpbitOrderbook } from '../hooks/useUpbitOrderbook';
|
||||
import { useUpbitRecentTrades } from '../hooks/useUpbitRecentTrades';
|
||||
import PaperTradeHistoryList from './paper/PaperTradeHistoryList';
|
||||
@@ -72,7 +72,7 @@ import {
|
||||
} from '../utils/virtualTargetHydrate';
|
||||
import '../styles/virtualTradingDashboard.css';
|
||||
|
||||
type RightTab = 'trade' | 'orderbook' | 'history';
|
||||
type RightTab = 'trade' | 'orderbook';
|
||||
|
||||
interface Props {
|
||||
theme?: Theme;
|
||||
@@ -258,7 +258,7 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
setTrades(tr);
|
||||
} catch { /* ignore */ }
|
||||
onPaperOrderFilled?.();
|
||||
if (opts?.focusHistory) setRightTab('history');
|
||||
if (opts?.focusHistory) setRightTab('trade');
|
||||
}, [onPaperOrderFilled]);
|
||||
|
||||
const handleGlobalDisplayModeChange = useCallback((mode: VirtualCardDisplayMode) => {
|
||||
@@ -472,12 +472,20 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
<div className="bps-right-tabs">
|
||||
<button type="button" className={`bps-right-tab${rightTab === 'trade' ? ' bps-right-tab--on' : ''}`} onClick={() => setRightTab('trade')}>매매</button>
|
||||
<button type="button" className={`bps-right-tab${rightTab === 'orderbook' ? ' bps-right-tab--on' : ''}`} onClick={() => setRightTab('orderbook')}>호가</button>
|
||||
<button type="button" className={`bps-right-tab${rightTab === 'history' ? ' bps-right-tab--on' : ''}`} onClick={() => setRightTab('history')}>
|
||||
거래{trades.length > 0 ? ` (${trades.length})` : ''}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const tradeHistorySlot = (
|
||||
<PaperTradeHistoryList
|
||||
trades={trades}
|
||||
strategyNames={strategyNames}
|
||||
tickers={tickers}
|
||||
onSelectMarket={handleSelectMarket}
|
||||
emptyText="수동·자동 매매 체결 내역이 없습니다."
|
||||
className="ptd-trade-history--fill"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<BuilderPageShell
|
||||
theme={theme}
|
||||
@@ -564,7 +572,7 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
right={(
|
||||
<TradeRightPanelBody anchorRef={orderAnchorRef} embeddedInShell>
|
||||
{rightTab === 'trade' ? (
|
||||
<TradeSplitOrderPanel
|
||||
<TradeTabOrderPanel
|
||||
market={selectedMarket}
|
||||
tradePrice={tradePrice}
|
||||
availableKrw={coerceFiniteNumber(summary?.cashBalance) ?? 0}
|
||||
@@ -575,8 +583,9 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
paperTradingEnabled={paperTradingEnabled}
|
||||
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
||||
onPaperOrderFilled={() => void refreshPaperData({ focusHistory: true })}
|
||||
historySlot={tradeHistorySlot}
|
||||
/>
|
||||
) : rightTab === 'orderbook' ? (
|
||||
) : (
|
||||
<TradeOrderbookPanelContent
|
||||
market={selectedMarket}
|
||||
asks={orderbook.asks}
|
||||
@@ -594,15 +603,6 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
tradeStrength={tradeStrength ?? undefined}
|
||||
onRowClick={handleOrderbookRowClick}
|
||||
/>
|
||||
) : (
|
||||
<PaperTradeHistoryList
|
||||
trades={trades}
|
||||
strategyNames={strategyNames}
|
||||
tickers={tickers}
|
||||
onSelectMarket={handleSelectMarket}
|
||||
emptyText="수동·자동 매매 체결 내역이 없습니다."
|
||||
className="ptd-trade-history--fill"
|
||||
/>
|
||||
)}
|
||||
</TradeRightPanelBody>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* 업비트 스타일 주문 패널
|
||||
* 매수 | 매도 | 간편주문 | 거래내역 탭
|
||||
*/
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import type { TradeOrderFillRequest } from '../../types';
|
||||
import TradeOrderPanel, { fmtKrw } from '../TradeOrderPanel';
|
||||
import { placePaperOrder } from '../../utils/backendApi';
|
||||
import { getKoreanName } from '../../utils/marketNameCache';
|
||||
import { coerceFiniteNumber } from '../../utils/safeFormat';
|
||||
|
||||
export type TradeTabOrderTab = 'buy' | 'sell' | 'quick' | 'history';
|
||||
|
||||
export interface TradeTabOrderPanelProps {
|
||||
market: string;
|
||||
tradePrice: number | null;
|
||||
availableKrw?: number;
|
||||
availableSymbolBuyKrw?: number;
|
||||
availableCoinQty?: number;
|
||||
fillBuy?: TradeOrderFillRequest | null;
|
||||
fillSell?: TradeOrderFillRequest | null;
|
||||
/** 차트 호가에서 자동 입력 */
|
||||
fillRequest?: TradeOrderFillRequest | null;
|
||||
searchAnchorRef?: React.RefObject<HTMLElement | null>;
|
||||
onMarketSelect?: (market: string) => void;
|
||||
paperTradingEnabled?: boolean;
|
||||
paperAutoTradeEnabled?: boolean;
|
||||
onPaperOrderFilled?: () => void;
|
||||
className?: string;
|
||||
defaultTab?: TradeTabOrderTab;
|
||||
/** 거래내역 탭 콘텐츠 (외부 주입) */
|
||||
historySlot?: React.ReactNode;
|
||||
/** 거래내역 탭 숨김 */
|
||||
hideHistory?: boolean;
|
||||
}
|
||||
|
||||
/* ─────────────────────────── 간편주문 ─────────────────────────── */
|
||||
|
||||
const PCT_BTNS = [10, 25, 50, 100] as const;
|
||||
|
||||
function parseNum(s: string): number {
|
||||
const v = Number(String(s).replace(/,/g, '').trim());
|
||||
return Number.isFinite(v) ? v : 0;
|
||||
}
|
||||
|
||||
interface QuickOrderPanelProps {
|
||||
market: string;
|
||||
tradePrice: number | null;
|
||||
availableKrw: number;
|
||||
availableCoinQty: number;
|
||||
paperTradingEnabled: boolean;
|
||||
paperAutoTradeEnabled: boolean;
|
||||
onPaperOrderFilled?: () => void;
|
||||
}
|
||||
|
||||
const QuickOrderPanel: React.FC<QuickOrderPanelProps> = ({
|
||||
market,
|
||||
tradePrice,
|
||||
availableKrw,
|
||||
availableCoinQty,
|
||||
paperTradingEnabled,
|
||||
paperAutoTradeEnabled,
|
||||
onPaperOrderFilled,
|
||||
}) => {
|
||||
const [side, setSide] = useState<'buy' | 'sell'>('buy');
|
||||
const [amountStr, setAmountStr] = useState('');
|
||||
const [pct, setPct] = useState<number | null>(null);
|
||||
|
||||
const code = market.replace(/^KRW-/, '');
|
||||
const koreanName = getKoreanName(market);
|
||||
const refPrice = coerceFiniteNumber(tradePrice) ?? 0;
|
||||
|
||||
useEffect(() => {
|
||||
setAmountStr('');
|
||||
setPct(null);
|
||||
}, [market, side]);
|
||||
|
||||
const handlePct = useCallback((p: number) => {
|
||||
setPct(p);
|
||||
if (side === 'buy') {
|
||||
const budget = availableKrw > 0 ? (availableKrw * p) / 100 : 0;
|
||||
setAmountStr(budget > 0 ? fmtKrw(budget) : '');
|
||||
} else {
|
||||
const qty = availableCoinQty > 0 ? (availableCoinQty * p) / 100 : 0;
|
||||
const krw = refPrice > 0 ? qty * refPrice : 0;
|
||||
setAmountStr(krw > 0 ? fmtKrw(krw) : '');
|
||||
}
|
||||
}, [side, availableKrw, availableCoinQty, refPrice]);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (!paperTradingEnabled) {
|
||||
window.alert('모의투자가 비활성화되어 있습니다. 설정 → 모의투자에서 사용을 켜 주세요.');
|
||||
return;
|
||||
}
|
||||
const totalKrw = parseNum(amountStr);
|
||||
if (totalKrw <= 0 || refPrice <= 0) {
|
||||
window.alert('주문금액을 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
const qty = totalKrw / refPrice;
|
||||
try {
|
||||
const result = await placePaperOrder({
|
||||
market,
|
||||
side: side === 'buy' ? 'BUY' : 'SELL',
|
||||
orderKind: 'market',
|
||||
orderType: 'MARKET',
|
||||
price: refPrice,
|
||||
quantity: qty,
|
||||
source: 'MANUAL',
|
||||
});
|
||||
if (!result?.trade && !result?.order) {
|
||||
window.alert('모의 주문에 실패했습니다.');
|
||||
return;
|
||||
}
|
||||
onPaperOrderFilled?.();
|
||||
const label = side === 'buy' ? '매수' : '매도';
|
||||
if (result.trade) {
|
||||
window.alert(
|
||||
`[모의투자] ${koreanName || code} ${label} 체결\n` +
|
||||
`가격: ${fmtKrw(result.trade.price)} KRW\n` +
|
||||
`수량: ${result.trade.quantity} ${code}`,
|
||||
);
|
||||
} else {
|
||||
window.alert(`[모의투자] ${koreanName || code} ${label} 주문이 접수되었습니다.`);
|
||||
}
|
||||
setAmountStr('');
|
||||
setPct(null);
|
||||
} catch (e) {
|
||||
window.alert(e instanceof Error ? e.message : '모의 주문 처리 중 오류가 발생했습니다.');
|
||||
}
|
||||
}, [paperTradingEnabled, amountStr, refPrice, market, side, koreanName, code, onPaperOrderFilled]);
|
||||
|
||||
const totalKrw = parseNum(amountStr);
|
||||
const estQty = refPrice > 0 && totalKrw > 0 ? totalKrw / refPrice : 0;
|
||||
|
||||
const balance = side === 'buy'
|
||||
? `${availableKrw > 0 ? fmtKrw(availableKrw) : '-'} KRW`
|
||||
: `${availableCoinQty > 0 ? availableCoinQty.toFixed(8).replace(/\.?0+$/, '') : '-'} ${code}`;
|
||||
|
||||
return (
|
||||
<div className={`qop-wrap qop-wrap--${side}`}>
|
||||
{/* 매수/매도 선택 */}
|
||||
<div className="qop-side-row">
|
||||
<button
|
||||
type="button"
|
||||
className={`qop-side-btn qop-side-btn--buy${side === 'buy' ? ' qop-side-btn--on' : ''}`}
|
||||
onClick={() => setSide('buy')}
|
||||
>
|
||||
매수
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`qop-side-btn qop-side-btn--sell${side === 'sell' ? ' qop-side-btn--on' : ''}`}
|
||||
onClick={() => setSide('sell')}
|
||||
>
|
||||
매도
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 주문가능 */}
|
||||
<div className="qop-available">
|
||||
<span className="qop-available-label">주문가능</span>
|
||||
<span className="qop-available-value">{balance}</span>
|
||||
</div>
|
||||
|
||||
{/* 주문금액 */}
|
||||
<div className="qop-field">
|
||||
<span className="qop-label">주문금액 (KRW)</span>
|
||||
<input
|
||||
type="text"
|
||||
className="qop-input"
|
||||
value={amountStr}
|
||||
placeholder="0"
|
||||
inputMode="numeric"
|
||||
onChange={e => {
|
||||
setAmountStr(e.target.value.replace(/[^\d,]/g, ''));
|
||||
setPct(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* % 버튼 */}
|
||||
<div className="qop-pct-row">
|
||||
{PCT_BTNS.map(p => (
|
||||
<button
|
||||
key={p}
|
||||
type="button"
|
||||
className={`qop-pct-btn${pct === p ? ' qop-pct-btn--active' : ''}`}
|
||||
onClick={() => handlePct(p)}
|
||||
>
|
||||
{`${p}%`}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className={`qop-pct-btn${pct === -1 ? ' qop-pct-btn--active' : ''}`}
|
||||
onClick={() => { setPct(-1); setAmountStr(''); }}
|
||||
>
|
||||
직접입력
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 예상 수량 */}
|
||||
{estQty > 0 && (
|
||||
<div className="qop-estimate">
|
||||
≈ {estQty >= 1
|
||||
? estQty.toFixed(8).replace(/\.?0+$/, '')
|
||||
: estQty.toFixed(8)} {code}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 메타 */}
|
||||
<div className="qop-meta">
|
||||
<span>시장가 즉시 체결 · 수수료(부가세 포함): 0.05%</span>
|
||||
</div>
|
||||
|
||||
{/* 제출 */}
|
||||
<div className="qop-actions">
|
||||
<button
|
||||
type="button"
|
||||
className={`qop-submit qop-submit--${side}`}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{side === 'buy' ? '매수' : '매도'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ─────────────────────────── 메인 컴포넌트 ─────────────────────────── */
|
||||
|
||||
const TAB_LIST: { id: TradeTabOrderTab; label: string }[] = [
|
||||
{ id: 'buy', label: '매수' },
|
||||
{ id: 'sell', label: '매도' },
|
||||
{ id: 'quick', label: '간편주문' },
|
||||
{ id: 'history', label: '거래내역' },
|
||||
];
|
||||
|
||||
const TradeTabOrderPanel: React.FC<TradeTabOrderPanelProps> = ({
|
||||
market,
|
||||
tradePrice,
|
||||
availableKrw = 0,
|
||||
availableSymbolBuyKrw,
|
||||
availableCoinQty = 0,
|
||||
fillBuy,
|
||||
fillSell,
|
||||
fillRequest,
|
||||
searchAnchorRef,
|
||||
onMarketSelect,
|
||||
paperTradingEnabled = false,
|
||||
paperAutoTradeEnabled = false,
|
||||
onPaperOrderFilled,
|
||||
className = '',
|
||||
defaultTab = 'buy',
|
||||
historySlot,
|
||||
hideHistory = false,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<TradeTabOrderTab>(defaultTab);
|
||||
|
||||
const resolvedBuyFill = fillBuy ?? (fillRequest?.side === 'buy' ? fillRequest : null);
|
||||
const resolvedSellFill = fillSell ?? (fillRequest?.side === 'sell' ? fillRequest : null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fillRequest) return;
|
||||
setActiveTab(fillRequest.side === 'buy' ? 'buy' : 'sell');
|
||||
}, [fillRequest?.seq, fillRequest?.side]);
|
||||
|
||||
const tabs = hideHistory ? TAB_LIST.filter(t => t.id !== 'history') : TAB_LIST;
|
||||
|
||||
return (
|
||||
<div className={`ttop-wrap ptd-split-panel--right${className ? ` ${className}` : ''}`}>
|
||||
{/* 탭 바 */}
|
||||
<div className="ttop-tabs">
|
||||
{tabs.map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
type="button"
|
||||
className={`ttop-tab ttop-tab--${t.id}${activeTab === t.id ? ' ttop-tab--on' : ''}`}
|
||||
onClick={() => setActiveTab(t.id)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 탭 본문 */}
|
||||
<div className="ttop-body">
|
||||
{(activeTab === 'buy' || activeTab === 'sell') && (
|
||||
<div className="ttop-card-body">
|
||||
<div className="ptd-order-card ptd-order-card--trade-inline">
|
||||
<TradeOrderPanel
|
||||
side={activeTab}
|
||||
market={market}
|
||||
tradePrice={tradePrice}
|
||||
availableKrw={availableKrw}
|
||||
availableSymbolBuyKrw={availableSymbolBuyKrw}
|
||||
availableCoinQty={availableCoinQty}
|
||||
fillRequest={activeTab === 'buy' ? resolvedBuyFill : resolvedSellFill}
|
||||
searchAnchorRef={searchAnchorRef}
|
||||
onMarketSelect={onMarketSelect}
|
||||
showSymbolField={false}
|
||||
paperTradingEnabled={paperTradingEnabled}
|
||||
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
||||
onPaperOrderFilled={onPaperOrderFilled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'quick' && (
|
||||
<QuickOrderPanel
|
||||
market={market}
|
||||
tradePrice={tradePrice}
|
||||
availableKrw={availableKrw}
|
||||
availableCoinQty={availableCoinQty}
|
||||
paperTradingEnabled={paperTradingEnabled}
|
||||
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
||||
onPaperOrderFilled={onPaperOrderFilled}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'history' && (
|
||||
<div className="ttop-history">
|
||||
{historySlot ?? (
|
||||
<div className="ttop-empty">거래내역이 없습니다.</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TradeTabOrderPanel;
|
||||
@@ -4,6 +4,8 @@ export { default as TradeSellOrderPanel } from './TradeSellOrderPanel';
|
||||
export type { TradeSellOrderPanelProps } from './TradeSellOrderPanel';
|
||||
export { default as TradeSplitOrderPanel } from './TradeSplitOrderPanel';
|
||||
export type { TradeSplitOrderPanelProps } from './TradeSplitOrderPanel';
|
||||
export { default as TradeTabOrderPanel } from './TradeTabOrderPanel';
|
||||
export type { TradeTabOrderPanelProps, TradeTabOrderTab } from './TradeTabOrderPanel';
|
||||
export { default as TradeOrderbookPanelContent } from './TradeOrderbookPanelContent';
|
||||
export type { TradeOrderbookPanelContentProps } from './TradeOrderbookPanelContent';
|
||||
export { default as TradeRightPanelTabs } from './TradeRightPanelTabs';
|
||||
|
||||
Reference in New Issue
Block a user