업비트 스타일 매수/매도 탭 주문 패널 적용
- 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 PaperLedgerTab from './paper/PaperLedgerTab';
|
||||||
import PaperAssetTrendChart from './paper/PaperAssetTrendChart';
|
import PaperAssetTrendChart from './paper/PaperAssetTrendChart';
|
||||||
import { computePaperMetrics } from '../utils/paperMetrics';
|
import { computePaperMetrics } from '../utils/paperMetrics';
|
||||||
import { TradeSplitOrderPanel, TradeOrderbookPanelContent, TradeRightPanelBody } from './trade';
|
import { TradeTabOrderPanel, TradeOrderbookPanelContent, TradeRightPanelBody } from './trade';
|
||||||
import type { Theme, TradeOrderFillRequest } from '../types';
|
import type { Theme, TradeOrderFillRequest } from '../types';
|
||||||
import PaperAnalysisChart from './paper/PaperAnalysisChart';
|
import PaperAnalysisChart from './paper/PaperAnalysisChart';
|
||||||
import PaperLeftStrategyTab from './paper/PaperLeftStrategyTab';
|
import PaperLeftStrategyTab from './paper/PaperLeftStrategyTab';
|
||||||
@@ -38,7 +38,7 @@ import { formatVirtualTargetOptionLabel } from '../utils/virtualTargetNames';
|
|||||||
|
|
||||||
type HistoryTab = 'open' | 'recent' | 'ledger';
|
type HistoryTab = 'open' | 'recent' | 'ledger';
|
||||||
type CenterTab = 'invest' | 'chart';
|
type CenterTab = 'invest' | 'chart';
|
||||||
type RightTab = 'trade' | 'orderbook' | 'history';
|
type RightTab = 'trade' | 'orderbook';
|
||||||
type LeftTab = 'strategy' | 'settings';
|
type LeftTab = 'strategy' | 'settings';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -140,7 +140,7 @@ const PaperTradingPage: React.FC<Props> = ({
|
|||||||
} finally {
|
} finally {
|
||||||
setInitialLoading(false);
|
setInitialLoading(false);
|
||||||
}
|
}
|
||||||
if (opts?.focusHistory) setRightTab('history');
|
if (opts?.focusHistory) setRightTab('trade');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -293,9 +293,33 @@ const PaperTradingPage: React.FC<Props> = ({
|
|||||||
<div className="bps-right-tabs">
|
<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 === '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 === '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')}>
|
</div>
|
||||||
거래{trades.length > 0 ? ` (${trades.length})` : ''}
|
);
|
||||||
</button>
|
|
||||||
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -421,7 +445,7 @@ const PaperTradingPage: React.FC<Props> = ({
|
|||||||
right={(
|
right={(
|
||||||
<TradeRightPanelBody anchorRef={orderAnchorRef} embeddedInShell>
|
<TradeRightPanelBody anchorRef={orderAnchorRef} embeddedInShell>
|
||||||
{rightTab === 'trade' ? (
|
{rightTab === 'trade' ? (
|
||||||
<TradeSplitOrderPanel
|
<TradeTabOrderPanel
|
||||||
market={selectedMarket}
|
market={selectedMarket}
|
||||||
tradePrice={tradePrice}
|
tradePrice={tradePrice}
|
||||||
availableKrw={orderableCash}
|
availableKrw={orderableCash}
|
||||||
@@ -433,8 +457,9 @@ const PaperTradingPage: React.FC<Props> = ({
|
|||||||
paperTradingEnabled={paperTradingEnabled}
|
paperTradingEnabled={paperTradingEnabled}
|
||||||
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
||||||
onPaperOrderFilled={() => void loadData({ focusHistory: true })}
|
onPaperOrderFilled={() => void loadData({ focusHistory: true })}
|
||||||
|
historySlot={tradeHistorySlot}
|
||||||
/>
|
/>
|
||||||
) : rightTab === 'orderbook' ? (
|
) : (
|
||||||
<TradeOrderbookPanelContent
|
<TradeOrderbookPanelContent
|
||||||
market={selectedMarket}
|
market={selectedMarket}
|
||||||
asks={orderbook.asks}
|
asks={orderbook.asks}
|
||||||
@@ -452,31 +477,6 @@ const PaperTradingPage: React.FC<Props> = ({
|
|||||||
tradeStrength={tradeStrength ?? undefined}
|
tradeStrength={tradeStrength ?? undefined}
|
||||||
onRowClick={handleOrderbookRowClick}
|
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>
|
</TradeRightPanelBody>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
TradeSplitOrderPanel,
|
TradeTabOrderPanel,
|
||||||
TradeOrderbookPanelContent,
|
TradeOrderbookPanelContent,
|
||||||
TradeRightPanelTabs,
|
TradeRightPanelTabs,
|
||||||
TradeRightPanelBody,
|
TradeRightPanelBody,
|
||||||
@@ -158,7 +158,7 @@ const RightSidePanel: React.FC<RightSidePanelProps> = ({
|
|||||||
|
|
||||||
<TradeRightPanelBody anchorRef={tradeSectionRef} className="rsp-body-wrap">
|
<TradeRightPanelBody anchorRef={tradeSectionRef} className="rsp-body-wrap">
|
||||||
{activeTab === 'trade' && (
|
{activeTab === 'trade' && (
|
||||||
<TradeSplitOrderPanel
|
<TradeTabOrderPanel
|
||||||
market={market}
|
market={market}
|
||||||
tradePrice={tradePrice}
|
tradePrice={tradePrice}
|
||||||
availableKrw={availableKrw}
|
availableKrw={availableKrw}
|
||||||
@@ -169,6 +169,7 @@ const RightSidePanel: React.FC<RightSidePanelProps> = ({
|
|||||||
paperTradingEnabled={paperTradingEnabled}
|
paperTradingEnabled={paperTradingEnabled}
|
||||||
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
||||||
onPaperOrderFilled={onPaperOrderFilled}
|
onPaperOrderFilled={onPaperOrderFilled}
|
||||||
|
hideHistory
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import BacktestAnalysisReportModal from './backtest/BacktestAnalysisReportModal'
|
|||||||
import type { BacktestAnalysisReportModel } from './backtest/BacktestAnalysisReportModal';
|
import type { BacktestAnalysisReportModel } from './backtest/BacktestAnalysisReportModal';
|
||||||
import { coerceFiniteNumber } from '../utils/safeFormat';
|
import { coerceFiniteNumber } from '../utils/safeFormat';
|
||||||
import {
|
import {
|
||||||
TradeSplitOrderPanel,
|
TradeTabOrderPanel,
|
||||||
TradeOrderbookPanelContent,
|
TradeOrderbookPanelContent,
|
||||||
TradeRightPanelTabs,
|
TradeRightPanelTabs,
|
||||||
TradeRightPanelBody,
|
TradeRightPanelBody,
|
||||||
@@ -753,7 +753,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
|||||||
bodyClassName="tnl-right-body"
|
bodyClassName="tnl-right-body"
|
||||||
>
|
>
|
||||||
{rightTab === 'trade' ? (
|
{rightTab === 'trade' ? (
|
||||||
<TradeSplitOrderPanel
|
<TradeTabOrderPanel
|
||||||
market={selectedMarket}
|
market={selectedMarket}
|
||||||
tradePrice={tradePrice}
|
tradePrice={tradePrice}
|
||||||
availableKrw={coerceFiniteNumber(summary?.cashBalance) ?? 0}
|
availableKrw={coerceFiniteNumber(summary?.cashBalance) ?? 0}
|
||||||
@@ -764,6 +764,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
|||||||
paperTradingEnabled={paperTradingEnabled}
|
paperTradingEnabled={paperTradingEnabled}
|
||||||
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
||||||
onPaperOrderFilled={() => void refreshPaperData()}
|
onPaperOrderFilled={() => void refreshPaperData()}
|
||||||
|
hideHistory
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TradeOrderbookPanelContent
|
<TradeOrderbookPanelContent
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from '../utils/backendApi';
|
} from '../utils/backendApi';
|
||||||
import type { Theme, TradeOrderFillRequest } from '../types';
|
import type { Theme, TradeOrderFillRequest } from '../types';
|
||||||
import type { TickerData } from '../hooks/useMarketTicker';
|
import type { TickerData } from '../hooks/useMarketTicker';
|
||||||
import { TradeSplitOrderPanel, TradeOrderbookPanelContent, TradeRightPanelBody } from './trade';
|
import { TradeTabOrderPanel, TradeOrderbookPanelContent, TradeRightPanelBody } from './trade';
|
||||||
import { useUpbitOrderbook } from '../hooks/useUpbitOrderbook';
|
import { useUpbitOrderbook } from '../hooks/useUpbitOrderbook';
|
||||||
import { useUpbitRecentTrades } from '../hooks/useUpbitRecentTrades';
|
import { useUpbitRecentTrades } from '../hooks/useUpbitRecentTrades';
|
||||||
import PaperTradeHistoryList from './paper/PaperTradeHistoryList';
|
import PaperTradeHistoryList from './paper/PaperTradeHistoryList';
|
||||||
@@ -72,7 +72,7 @@ import {
|
|||||||
} from '../utils/virtualTargetHydrate';
|
} from '../utils/virtualTargetHydrate';
|
||||||
import '../styles/virtualTradingDashboard.css';
|
import '../styles/virtualTradingDashboard.css';
|
||||||
|
|
||||||
type RightTab = 'trade' | 'orderbook' | 'history';
|
type RightTab = 'trade' | 'orderbook';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
@@ -258,7 +258,7 @@ const VirtualTradingPage: React.FC<Props> = ({
|
|||||||
setTrades(tr);
|
setTrades(tr);
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
onPaperOrderFilled?.();
|
onPaperOrderFilled?.();
|
||||||
if (opts?.focusHistory) setRightTab('history');
|
if (opts?.focusHistory) setRightTab('trade');
|
||||||
}, [onPaperOrderFilled]);
|
}, [onPaperOrderFilled]);
|
||||||
|
|
||||||
const handleGlobalDisplayModeChange = useCallback((mode: VirtualCardDisplayMode) => {
|
const handleGlobalDisplayModeChange = useCallback((mode: VirtualCardDisplayMode) => {
|
||||||
@@ -472,12 +472,20 @@ const VirtualTradingPage: React.FC<Props> = ({
|
|||||||
<div className="bps-right-tabs">
|
<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 === '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 === '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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tradeHistorySlot = (
|
||||||
|
<PaperTradeHistoryList
|
||||||
|
trades={trades}
|
||||||
|
strategyNames={strategyNames}
|
||||||
|
tickers={tickers}
|
||||||
|
onSelectMarket={handleSelectMarket}
|
||||||
|
emptyText="수동·자동 매매 체결 내역이 없습니다."
|
||||||
|
className="ptd-trade-history--fill"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BuilderPageShell
|
<BuilderPageShell
|
||||||
theme={theme}
|
theme={theme}
|
||||||
@@ -564,7 +572,7 @@ const VirtualTradingPage: React.FC<Props> = ({
|
|||||||
right={(
|
right={(
|
||||||
<TradeRightPanelBody anchorRef={orderAnchorRef} embeddedInShell>
|
<TradeRightPanelBody anchorRef={orderAnchorRef} embeddedInShell>
|
||||||
{rightTab === 'trade' ? (
|
{rightTab === 'trade' ? (
|
||||||
<TradeSplitOrderPanel
|
<TradeTabOrderPanel
|
||||||
market={selectedMarket}
|
market={selectedMarket}
|
||||||
tradePrice={tradePrice}
|
tradePrice={tradePrice}
|
||||||
availableKrw={coerceFiniteNumber(summary?.cashBalance) ?? 0}
|
availableKrw={coerceFiniteNumber(summary?.cashBalance) ?? 0}
|
||||||
@@ -575,8 +583,9 @@ const VirtualTradingPage: React.FC<Props> = ({
|
|||||||
paperTradingEnabled={paperTradingEnabled}
|
paperTradingEnabled={paperTradingEnabled}
|
||||||
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
||||||
onPaperOrderFilled={() => void refreshPaperData({ focusHistory: true })}
|
onPaperOrderFilled={() => void refreshPaperData({ focusHistory: true })}
|
||||||
|
historySlot={tradeHistorySlot}
|
||||||
/>
|
/>
|
||||||
) : rightTab === 'orderbook' ? (
|
) : (
|
||||||
<TradeOrderbookPanelContent
|
<TradeOrderbookPanelContent
|
||||||
market={selectedMarket}
|
market={selectedMarket}
|
||||||
asks={orderbook.asks}
|
asks={orderbook.asks}
|
||||||
@@ -594,15 +603,6 @@ const VirtualTradingPage: React.FC<Props> = ({
|
|||||||
tradeStrength={tradeStrength ?? undefined}
|
tradeStrength={tradeStrength ?? undefined}
|
||||||
onRowClick={handleOrderbookRowClick}
|
onRowClick={handleOrderbookRowClick}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<PaperTradeHistoryList
|
|
||||||
trades={trades}
|
|
||||||
strategyNames={strategyNames}
|
|
||||||
tickers={tickers}
|
|
||||||
onSelectMarket={handleSelectMarket}
|
|
||||||
emptyText="수동·자동 매매 체결 내역이 없습니다."
|
|
||||||
className="ptd-trade-history--fill"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</TradeRightPanelBody>
|
</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 type { TradeSellOrderPanelProps } from './TradeSellOrderPanel';
|
||||||
export { default as TradeSplitOrderPanel } from './TradeSplitOrderPanel';
|
export { default as TradeSplitOrderPanel } from './TradeSplitOrderPanel';
|
||||||
export type { TradeSplitOrderPanelProps } 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 { default as TradeOrderbookPanelContent } from './TradeOrderbookPanelContent';
|
||||||
export type { TradeOrderbookPanelContentProps } from './TradeOrderbookPanelContent';
|
export type { TradeOrderbookPanelContentProps } from './TradeOrderbookPanelContent';
|
||||||
export { default as TradeRightPanelTabs } from './TradeRightPanelTabs';
|
export { default as TradeRightPanelTabs } from './TradeRightPanelTabs';
|
||||||
|
|||||||
@@ -411,3 +411,263 @@
|
|||||||
.trade-right-panel .ptd-order-card--trade-inline .top-paper-hint {
|
.trade-right-panel .ptd-order-card--trade-inline .top-paper-hint {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════════════
|
||||||
|
업비트 스타일 탭 주문 패널 (TradeTabOrderPanel)
|
||||||
|
ttop-wrap > ttop-tabs / ttop-body
|
||||||
|
══════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* 래퍼: 전체 높이 flex 컬럼 */
|
||||||
|
.trade-right-panel .ttop-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 탭 바 ── */
|
||||||
|
.trade-right-panel .ttop-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trade-right-panel .ttop-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 13px 4px 11px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
color: var(--text3);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.12s, border-color 0.12s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.trade-right-panel .ttop-tab:hover {
|
||||||
|
color: var(--text2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 매수 탭 활성 */
|
||||||
|
.trade-right-panel .ttop-tab--buy.ttop-tab--on {
|
||||||
|
color: var(--gc-trade-buy);
|
||||||
|
border-bottom-color: var(--gc-trade-buy);
|
||||||
|
}
|
||||||
|
/* 매도 탭 활성 */
|
||||||
|
.trade-right-panel .ttop-tab--sell.ttop-tab--on {
|
||||||
|
color: var(--gc-trade-sell);
|
||||||
|
border-bottom-color: var(--gc-trade-sell);
|
||||||
|
}
|
||||||
|
/* 간편주문·거래내역 탭 활성 */
|
||||||
|
.trade-right-panel .ttop-tab--quick.ttop-tab--on,
|
||||||
|
.trade-right-panel .ttop-tab--history.ttop-tab--on {
|
||||||
|
color: var(--accent, #7aa2f7);
|
||||||
|
border-bottom-color: var(--accent, #7aa2f7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 탭 본문 ── */
|
||||||
|
.trade-right-panel .ttop-body {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 주문 폼 카드 영역 (매수/매도 탭) */
|
||||||
|
.trade-right-panel .ttop-card-body {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ttop-card-body 안의 order-card — ptd-split-card-body 안과 동일 규칙 */
|
||||||
|
.trade-right-panel .ptd-split-panel--right .ttop-card-body > .ptd-order-card--trade-inline {
|
||||||
|
flex: 1 1 0;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 거래내역 영역 */
|
||||||
|
.trade-right-panel .ttop-history {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trade-right-panel .ttop-empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text3);
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 간편주문 폼 (QuickOrderPanel) ── */
|
||||||
|
.trade-right-panel .qop-wrap {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--gc-trade-card-pad);
|
||||||
|
gap: var(--gc-trade-row-gap);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 매수/매도 전환 */
|
||||||
|
.trade-right-panel .qop-side-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.trade-right-panel .qop-side-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg3);
|
||||||
|
color: var(--text2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
||||||
|
}
|
||||||
|
.trade-right-panel .qop-side-btn--buy.qop-side-btn--on {
|
||||||
|
background: color-mix(in srgb, var(--gc-trade-buy) 15%, var(--bg3));
|
||||||
|
border-color: var(--gc-trade-buy);
|
||||||
|
color: var(--gc-trade-buy);
|
||||||
|
}
|
||||||
|
.trade-right-panel .qop-side-btn--sell.qop-side-btn--on {
|
||||||
|
background: color-mix(in srgb, var(--gc-trade-sell) 15%, var(--bg3));
|
||||||
|
border-color: var(--gc-trade-sell);
|
||||||
|
color: var(--gc-trade-sell);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 주문가능 */
|
||||||
|
.trade-right-panel .qop-available {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.trade-right-panel .qop-available-label {
|
||||||
|
color: var(--text2);
|
||||||
|
}
|
||||||
|
.trade-right-panel .qop-available-value {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 주문금액 필드 */
|
||||||
|
.trade-right-panel .qop-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.trade-right-panel .qop-label {
|
||||||
|
flex: 0 0 var(--gc-trade-row-label-w);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text2);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.trade-right-panel .qop-input {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.25;
|
||||||
|
border: 1px solid var(--gc-trade-upbit-input-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* % 버튼 */
|
||||||
|
.trade-right-panel .qop-pct-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: calc(var(--gc-trade-row-label-w) + 8px);
|
||||||
|
}
|
||||||
|
.trade-right-panel .qop-pct-btn {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 6px 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
border: 1px solid var(--gc-trade-upbit-input-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.1s, color 0.1s;
|
||||||
|
}
|
||||||
|
.trade-right-panel .qop-pct-btn--active {
|
||||||
|
border-color: var(--gc-trade-upbit-accent);
|
||||||
|
color: var(--gc-trade-upbit-accent);
|
||||||
|
background: color-mix(in srgb, var(--gc-trade-upbit-accent) 8%, var(--bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 예상 수량 */
|
||||||
|
.trade-right-panel .qop-estimate {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text3);
|
||||||
|
text-align: right;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 메타 안내 */
|
||||||
|
.trade-right-panel .qop-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text3);
|
||||||
|
text-align: right;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 제출 버튼 */
|
||||||
|
.trade-right-panel .qop-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
.trade-right-panel .qop-submit {
|
||||||
|
width: 100%;
|
||||||
|
padding: 13px 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.12s;
|
||||||
|
}
|
||||||
|
.trade-right-panel .qop-submit:hover { opacity: 0.9; }
|
||||||
|
.trade-right-panel .qop-submit--buy {
|
||||||
|
background: var(--gc-trade-buy);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.trade-right-panel .qop-submit--sell {
|
||||||
|
background: var(--gc-trade-sell);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user