223 lines
7.3 KiB
TypeScript
223 lines
7.3 KiB
TypeScript
/**
|
|
* 실시간 차트 우측 패널
|
|
* - 매매 탭: 상단 매수 / 하단 매도
|
|
* - 호가 탭: 실시간 호가창
|
|
*/
|
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
import { OrderbookPanel } from './OrderbookPanel';
|
|
import TradeOrderPanel from './TradeOrderPanel';
|
|
import type { OrderbookDisplayUnit, WsStatus } from '../hooks/useUpbitOrderbook';
|
|
import { useUpbitRecentTrades } from '../hooks/useUpbitRecentTrades';
|
|
import type { OrderbookTickerInfo } from './OrderbookPanel';
|
|
import type { TradeOrderFillRequest } from '../types';
|
|
|
|
type PanelTab = 'trade' | 'orderbook';
|
|
|
|
export interface RightSidePanelProps {
|
|
/** 모바일 등 외부 제어 시 열림 상태 */
|
|
open?: boolean;
|
|
onOpenChange?: (open: boolean) => void;
|
|
/** 모바일 도크에서 호가/매매 탭 지정 */
|
|
activeTab?: PanelTab;
|
|
onTabChange?: (tab: PanelTab) => void;
|
|
market: string;
|
|
tradePrice: number | null;
|
|
asks: OrderbookDisplayUnit[];
|
|
bids: OrderbookDisplayUnit[];
|
|
totalAskSize: number;
|
|
totalBidSize: number;
|
|
wsStatus: WsStatus;
|
|
bestAsk: number;
|
|
bestBid: number;
|
|
spread: number;
|
|
spreadPct: number;
|
|
prevClose: number;
|
|
tickerInfo?: OrderbookTickerInfo;
|
|
fillRequest?: TradeOrderFillRequest | null;
|
|
onOrderbookPick?: (price: number, rowType: 'ask' | 'bid') => void;
|
|
onMarketSelect?: (market: string) => void;
|
|
availableKrw?: number;
|
|
availableCoinQty?: number;
|
|
paperTradingEnabled?: boolean;
|
|
paperAutoTradeEnabled?: boolean;
|
|
onPaperOrderFilled?: () => void;
|
|
}
|
|
|
|
const PANEL_TABS: { id: PanelTab; label: string }[] = [
|
|
{ id: 'trade', label: '매매' },
|
|
{ id: 'orderbook', label: '호가' },
|
|
];
|
|
|
|
const RightSidePanel: React.FC<RightSidePanelProps> = ({
|
|
open: openControlled,
|
|
onOpenChange,
|
|
activeTab: activeTabControlled,
|
|
onTabChange,
|
|
market,
|
|
tradePrice,
|
|
asks,
|
|
bids,
|
|
totalAskSize,
|
|
totalBidSize,
|
|
wsStatus,
|
|
bestAsk,
|
|
bestBid,
|
|
spread,
|
|
spreadPct,
|
|
prevClose,
|
|
tickerInfo,
|
|
fillRequest,
|
|
onOrderbookPick,
|
|
onMarketSelect,
|
|
availableKrw = 0,
|
|
availableCoinQty = 0,
|
|
paperTradingEnabled = false,
|
|
paperAutoTradeEnabled = false,
|
|
onPaperOrderFilled,
|
|
}) => {
|
|
const [openInternal, setOpenInternal] = useState(false);
|
|
const [tabInternal, setTabInternal] = useState<PanelTab>('trade');
|
|
const tradeSectionRef = useRef<HTMLDivElement>(null);
|
|
const { trades: recentTrades, strength: tradeStrength } = useUpbitRecentTrades(market);
|
|
|
|
const isOpenControlled = openControlled !== undefined && onOpenChange !== undefined;
|
|
const open = isOpenControlled ? !!openControlled : openInternal;
|
|
const setOpen = (next: boolean | ((p: boolean) => boolean)) => {
|
|
const cur = isOpenControlled ? !!openControlled : openInternal;
|
|
const value = typeof next === 'function' ? next(cur) : next;
|
|
if (isOpenControlled && onOpenChange) onOpenChange(value);
|
|
else setOpenInternal(value);
|
|
};
|
|
|
|
const activeTab = activeTabControlled ?? tabInternal;
|
|
const setActiveTab = useCallback((tab: PanelTab) => {
|
|
if (onTabChange) onTabChange(tab);
|
|
else setTabInternal(tab);
|
|
}, [onTabChange]);
|
|
|
|
useEffect(() => {
|
|
if (!fillRequest) return;
|
|
setOpen(true);
|
|
setActiveTab('trade');
|
|
}, [fillRequest?.seq, fillRequest?.side, setOpen, setActiveTab]);
|
|
|
|
const handleOrderbookRowClick = useCallback((price: number, rowType: 'ask' | 'bid') => {
|
|
setActiveTab('trade');
|
|
onOrderbookPick?.(price, rowType);
|
|
}, [onOrderbookPick]);
|
|
|
|
return (
|
|
<div className={`rsp-wrap ${open ? 'rsp-wrap--open' : ''}`}>
|
|
<button
|
|
type="button"
|
|
className="rsp-handle"
|
|
onClick={() => setOpen(v => !v)}
|
|
title={open ? '패널 닫기' : '패널 열기'}
|
|
>
|
|
<svg
|
|
width="8" height="14"
|
|
viewBox="0 0 8 14"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.8"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
{open
|
|
? <polyline points="2,1 6,7 2,13" />
|
|
: <polyline points="6,1 2,7 6,13" />}
|
|
</svg>
|
|
</button>
|
|
|
|
<div className="rsp-panel">
|
|
<div className="rsp-tabs">
|
|
{PANEL_TABS.map(tab => (
|
|
<button
|
|
key={tab.id}
|
|
type="button"
|
|
className={`rsp-tab ${activeTab === tab.id ? 'rsp-tab--active' : ''}`}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="rsp-body">
|
|
{activeTab === 'trade' && (
|
|
<div className="rsp-trade-stack" ref={tradeSectionRef}>
|
|
<div className="rsp-trade-card rsp-trade-card--buy">
|
|
<div className="rsp-trade-card-title">매수</div>
|
|
<div className="rsp-trade-card-body">
|
|
<TradeOrderPanel
|
|
side="buy"
|
|
market={market}
|
|
tradePrice={tradePrice}
|
|
availableKrw={availableKrw}
|
|
fillRequest={fillRequest}
|
|
searchAnchorRef={tradeSectionRef}
|
|
onMarketSelect={onMarketSelect}
|
|
showSymbolField
|
|
paperTradingEnabled={paperTradingEnabled}
|
|
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
|
onPaperOrderFilled={onPaperOrderFilled}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rsp-trade-card rsp-trade-card--sell">
|
|
<div className="rsp-trade-card-title">매도</div>
|
|
<div className="rsp-trade-card-body">
|
|
<TradeOrderPanel
|
|
side="sell"
|
|
market={market}
|
|
tradePrice={tradePrice}
|
|
availableKrw={availableKrw}
|
|
availableCoinQty={availableCoinQty}
|
|
fillRequest={fillRequest}
|
|
searchAnchorRef={tradeSectionRef}
|
|
onMarketSelect={onMarketSelect}
|
|
showSymbolField={false}
|
|
paperTradingEnabled={paperTradingEnabled}
|
|
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
|
onPaperOrderFilled={onPaperOrderFilled}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'orderbook' && (
|
|
<div className="rsp-ob-stack">
|
|
<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
|
|
market={market}
|
|
asks={asks}
|
|
bids={bids}
|
|
totalAskSize={totalAskSize}
|
|
totalBidSize={totalBidSize}
|
|
wsStatus={wsStatus}
|
|
bestAsk={bestAsk}
|
|
bestBid={bestBid}
|
|
spread={spread}
|
|
spreadPct={spreadPct}
|
|
prevClose={prevClose}
|
|
tickerInfo={tickerInfo}
|
|
recentTrades={recentTrades}
|
|
tradeStrength={tradeStrength}
|
|
onRowClick={handleOrderbookRowClick}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RightSidePanel;
|