모의투자 관리 기능 추가

This commit is contained in:
Macbook
2026-05-31 03:47:07 +09:00
parent 9d7dddfa57
commit 16d0e2c226
78 changed files with 4688 additions and 972 deletions
@@ -0,0 +1,21 @@
/**
* 우측 매매 패널 — 매수 주문 화면 (공용)
*/
import React from 'react';
import TradeOrderPanel, { type TradeOrderPanelProps } from '../TradeOrderPanel';
export type TradeBuyOrderPanelProps = Omit<
TradeOrderPanelProps,
'side' | 'availableCoinQty'
>;
const TradeBuyOrderPanel: React.FC<TradeBuyOrderPanelProps> = ({
showSymbolField = true,
...props
}) => (
<div className="ptd-order-card ptd-order-card--trade-inline">
<TradeOrderPanel side="buy" showSymbolField={showSymbolField} {...props} />
</div>
);
export default TradeBuyOrderPanel;
@@ -0,0 +1,39 @@
/**
* 우측 호가 탭 — 실시간 호가 카드 (공용)
*/
import React from 'react';
import { OrderbookPanel } from '../OrderbookPanel';
import type { OrderbookDisplayUnit, WsStatus } from '../../hooks/useUpbitOrderbook';
import type { OrderbookTickerInfo } from '../OrderbookPanel';
import type { RecentTrade } from '../../hooks/useUpbitRecentTrades';
export interface TradeOrderbookPanelContentProps {
market: string;
asks: OrderbookDisplayUnit[];
bids: OrderbookDisplayUnit[];
totalAskSize: number;
totalBidSize: number;
wsStatus: WsStatus;
bestAsk: number;
bestBid: number;
spread: number;
spreadPct: number;
prevClose: number;
tickerInfo?: OrderbookTickerInfo;
recentTrades?: RecentTrade[];
tradeStrength?: number;
onRowClick?: (price: number, rowType: 'ask' | 'bid') => void;
}
const TradeOrderbookPanelContent: React.FC<TradeOrderbookPanelContentProps> = (props) => (
<div className="rsp-ob-stack ptd-ob-stack--fill">
<div className="rsp-trade-card rsp-ob-card">
<div className="rsp-trade-card-title rsp-ob-card-title"> </div>
<div className="rsp-trade-card-body rsp-ob-card-body">
<OrderbookPanel {...props} />
</div>
</div>
</div>
);
export default TradeOrderbookPanelContent;
@@ -0,0 +1,40 @@
/**
* 우측 패널 본문 — bps-right-body + ptd-right-body (paperDashboard compact 매매 UI)
*/
import React from 'react';
interface Props {
children: React.ReactNode;
className?: string;
bodyClassName?: string;
anchorRef?: React.Ref<HTMLDivElement>;
/** BuilderPageShell이 이미 bps-right-body를 제공할 때 (모의·가상매매) */
embeddedInShell?: boolean;
}
const TradeRightPanelBody: React.FC<Props> = ({
children,
className = '',
bodyClassName = '',
anchorRef,
embeddedInShell = false,
}) => {
const inner = (
<div
className={`ptd-right-body${bodyClassName ? ` ${bodyClassName}` : ''}`}
ref={anchorRef}
>
{children}
</div>
);
if (embeddedInShell) {
return inner;
}
return (
<div className={`bps-right-body${className ? ` ${className}` : ''}`}>
{inner}
</div>
);
};
export default TradeRightPanelBody;
@@ -0,0 +1,33 @@
/**
* 우측 패널 — 매매 / 호가 탭 (알림목록·모의투자·실시간 차트 공용)
*/
import React from 'react';
export type TradeRightPanelTab = 'trade' | 'orderbook';
interface Props {
activeTab: TradeRightPanelTab;
onTabChange: (tab: TradeRightPanelTab) => void;
className?: string;
}
const TradeRightPanelTabs: React.FC<Props> = ({ activeTab, onTabChange, className = '' }) => (
<div className={`bps-right-tabs${className ? ` ${className}` : ''}`}>
<button
type="button"
className={`bps-right-tab${activeTab === 'trade' ? ' bps-right-tab--on' : ''}`}
onClick={() => onTabChange('trade')}
>
</button>
<button
type="button"
className={`bps-right-tab${activeTab === 'orderbook' ? ' bps-right-tab--on' : ''}`}
onClick={() => onTabChange('orderbook')}
>
</button>
</div>
);
export default TradeRightPanelTabs;
@@ -0,0 +1,21 @@
/**
* 우측 매매 패널 — 매도 주문 화면 (공용)
*/
import React from 'react';
import TradeOrderPanel, { type TradeOrderPanelProps } from '../TradeOrderPanel';
export type TradeSellOrderPanelProps = Omit<
TradeOrderPanelProps,
'side' | 'availableKrw' | 'availableSymbolBuyKrw'
>;
const TradeSellOrderPanel: React.FC<TradeSellOrderPanelProps> = ({
showSymbolField = false,
...props
}) => (
<div className="ptd-order-card ptd-order-card--trade-inline">
<TradeOrderPanel side="sell" showSymbolField={showSymbolField} {...props} />
</div>
);
export default TradeSellOrderPanel;
@@ -0,0 +1,83 @@
/**
* 우측 매매 탭 — 상단 매수 / 하단 매도 분할 레이아웃 (알림목록·모의투자·가상매매·차트 공용)
*/
import React from 'react';
import type { TradeOrderFillRequest } from '../../types';
import PaperSplitPanel from '../paper/PaperSplitPanel';
import TradeBuyOrderPanel from './TradeBuyOrderPanel';
import TradeSellOrderPanel from './TradeSellOrderPanel';
export interface TradeSplitOrderPanelProps {
market: string;
tradePrice: number | null;
availableKrw?: number;
/** 종목별 추가 매수 가능 (모의투자 한도) */
availableSymbolBuyKrw?: number;
availableCoinQty?: number;
fillBuy?: TradeOrderFillRequest | null;
fillSell?: TradeOrderFillRequest | null;
/** 단일 fill (차트 호가 연동 — side에 따라 매수/매도에만 적용) */
fillRequest?: TradeOrderFillRequest | null;
searchAnchorRef?: React.RefObject<HTMLElement | null>;
onMarketSelect?: (market: string) => void;
paperTradingEnabled?: boolean;
paperAutoTradeEnabled?: boolean;
onPaperOrderFilled?: () => void;
className?: string;
}
const TradeSplitOrderPanel: React.FC<TradeSplitOrderPanelProps> = ({
market,
tradePrice,
availableKrw = 0,
availableSymbolBuyKrw,
availableCoinQty = 0,
fillBuy,
fillSell,
fillRequest,
searchAnchorRef,
onMarketSelect,
paperTradingEnabled = false,
paperAutoTradeEnabled = false,
onPaperOrderFilled,
className = '',
}) => {
const resolvedBuyFill = fillBuy ?? (fillRequest?.side === 'buy' ? fillRequest : null);
const resolvedSellFill = fillSell ?? (fillRequest?.side === 'sell' ? fillRequest : null);
return (
<PaperSplitPanel
className={`ptd-split-panel--right${className ? ` ${className}` : ''}`}
topTitle="매수"
bottomTitle="매도"
top={(
<TradeBuyOrderPanel
market={market}
tradePrice={tradePrice}
availableKrw={availableKrw}
availableSymbolBuyKrw={availableSymbolBuyKrw}
fillRequest={resolvedBuyFill}
searchAnchorRef={searchAnchorRef}
onMarketSelect={onMarketSelect}
paperTradingEnabled={paperTradingEnabled}
paperAutoTradeEnabled={paperAutoTradeEnabled}
onPaperOrderFilled={onPaperOrderFilled}
/>
)}
bottom={(
<TradeSellOrderPanel
market={market}
tradePrice={tradePrice}
availableCoinQty={availableCoinQty}
fillRequest={resolvedSellFill}
onMarketSelect={onMarketSelect}
paperTradingEnabled={paperTradingEnabled}
paperAutoTradeEnabled={paperAutoTradeEnabled}
onPaperOrderFilled={onPaperOrderFilled}
/>
)}
/>
);
};
export default TradeSplitOrderPanel;
+11
View File
@@ -0,0 +1,11 @@
export { default as TradeBuyOrderPanel } from './TradeBuyOrderPanel';
export type { TradeBuyOrderPanelProps } from './TradeBuyOrderPanel';
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 TradeOrderbookPanelContent } from './TradeOrderbookPanelContent';
export type { TradeOrderbookPanelContentProps } from './TradeOrderbookPanelContent';
export { default as TradeRightPanelTabs } from './TradeRightPanelTabs';
export type { TradeRightPanelTab } from './TradeRightPanelTabs';
export { default as TradeRightPanelBody } from './TradeRightPanelBody';