184 lines
5.6 KiB
TypeScript
184 lines
5.6 KiB
TypeScript
/**
|
|
* 실시간 차트 우측 패널
|
|
* - 매매 탭: 상단 매수 / 하단 매도
|
|
* - 호가 탭: 실시간 호가창
|
|
*/
|
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
import {
|
|
TradeSplitOrderPanel,
|
|
TradeOrderbookPanelContent,
|
|
TradeRightPanelTabs,
|
|
TradeRightPanelBody,
|
|
type TradeRightPanelTab,
|
|
} from './trade';
|
|
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?: TradeRightPanelTab;
|
|
onTabChange?: (tab: TradeRightPanelTab) => 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;
|
|
/** 차트 화면·업비트 종목일 때만 체결 WS/REST */
|
|
wsFeedEnabled?: boolean;
|
|
}
|
|
|
|
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,
|
|
wsFeedEnabled = true,
|
|
}) => {
|
|
const [openInternal, setOpenInternal] = useState(false);
|
|
const [tabInternal, setTabInternal] = useState<TradeRightPanelTab>('trade');
|
|
const tradeSectionRef = useRef<HTMLDivElement>(null);
|
|
|
|
const isOpenControlled = openControlled !== undefined && onOpenChange !== undefined;
|
|
const open = isOpenControlled ? !!openControlled : openInternal;
|
|
const { trades: recentTrades, strength: tradeStrength } = useUpbitRecentTrades(market, open && wsFeedEnabled);
|
|
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: TradeRightPanelTab) => {
|
|
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 trade-right-panel">
|
|
<TradeRightPanelTabs
|
|
activeTab={activeTab}
|
|
onTabChange={setActiveTab}
|
|
className="rsp-tabs"
|
|
/>
|
|
|
|
<TradeRightPanelBody anchorRef={tradeSectionRef} className="rsp-body-wrap">
|
|
{activeTab === 'trade' && (
|
|
<TradeSplitOrderPanel
|
|
market={market}
|
|
tradePrice={tradePrice}
|
|
availableKrw={availableKrw}
|
|
availableCoinQty={availableCoinQty}
|
|
fillRequest={fillRequest}
|
|
searchAnchorRef={tradeSectionRef}
|
|
onMarketSelect={onMarketSelect}
|
|
paperTradingEnabled={paperTradingEnabled}
|
|
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
|
onPaperOrderFilled={onPaperOrderFilled}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'orderbook' && (
|
|
<TradeOrderbookPanelContent
|
|
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 ?? undefined}
|
|
onRowClick={handleOrderbookRowClick}
|
|
/>
|
|
)}
|
|
</TradeRightPanelBody>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RightSidePanel;
|