실시간 웹소켓 리팩토링
This commit is contained in:
@@ -14,7 +14,6 @@ import { AnimatedQuantity } from "./AnimatedQuantity";
|
||||
interface OrderBookProps {
|
||||
symbol?: string;
|
||||
referencePrice?: number;
|
||||
currentPrice?: number;
|
||||
latestTick: DashboardRealtimeTradeTick | null;
|
||||
recentTicks: DashboardRealtimeTradeTick[];
|
||||
orderBook: DashboardStockOrderBookResponse | null;
|
||||
@@ -24,7 +23,7 @@ interface OrderBookProps {
|
||||
interface BookRow {
|
||||
price: number;
|
||||
size: number;
|
||||
changePercent: number | null;
|
||||
changeValue: number | null;
|
||||
isHighlighted: boolean;
|
||||
}
|
||||
|
||||
@@ -79,6 +78,51 @@ function pctChange(price: number, base: number) {
|
||||
return base > 0 ? ((price - base) / base) * 100 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 기준가 대비 증감값/증감률을 함께 계산합니다.
|
||||
* @see features/trade/components/orderbook/OrderBook.tsx buildBookRows
|
||||
*/
|
||||
function resolvePriceChange(price: number, basePrice: number) {
|
||||
if (price <= 0 || basePrice <= 0) {
|
||||
return { changeValue: null } as const;
|
||||
}
|
||||
|
||||
const changeValue = price - basePrice;
|
||||
|
||||
return { changeValue } as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 증감 숫자를 부호 포함 문자열로 포맷합니다.
|
||||
* @see features/trade/components/orderbook/OrderBook.tsx BookSideRows
|
||||
*/
|
||||
function fmtSignedChange(v: number) {
|
||||
if (!Number.isFinite(v)) return "-";
|
||||
if (v > 0) return `+${fmt(v)}`;
|
||||
if (v < 0) return `-${fmt(Math.abs(v))}`;
|
||||
return "0";
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 증감값에 따라 색상 톤 클래스를 반환합니다.
|
||||
* @see features/trade/components/orderbook/OrderBook.tsx BookSideRows
|
||||
*/
|
||||
function getChangeToneClass(
|
||||
changeValue: number | null,
|
||||
neutralClass = "text-muted-foreground",
|
||||
) {
|
||||
if (changeValue === null) {
|
||||
return neutralClass;
|
||||
}
|
||||
if (changeValue > 0) {
|
||||
return "text-red-500";
|
||||
}
|
||||
if (changeValue < 0) {
|
||||
return "text-blue-600 dark:text-blue-400";
|
||||
}
|
||||
return neutralClass;
|
||||
}
|
||||
|
||||
/** 체결 시각 포맷 */
|
||||
function fmtTime(hms: string) {
|
||||
if (!hms || hms.length !== 6) return "--:--:--";
|
||||
@@ -131,6 +175,65 @@ function resolveTickExecutionSide(
|
||||
return "neutral" as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 호가 레벨을 화면 렌더링용 행 모델로 변환합니다.
|
||||
* UI 흐름: OrderBook(levels/basePrice/latestPrice) -> buildBookRows -> BookSideRows -> 가격/증감 반영
|
||||
* @see features/trade/components/orderbook/OrderBook.tsx OrderBook askRows/bidRows 계산
|
||||
*/
|
||||
function buildBookRows({
|
||||
levels,
|
||||
side,
|
||||
basePrice,
|
||||
latestPrice,
|
||||
}: {
|
||||
levels: DashboardStockOrderBookResponse["levels"];
|
||||
side: "ask" | "bid";
|
||||
basePrice: number;
|
||||
latestPrice: number;
|
||||
}) {
|
||||
const normalizedLevels = side === "ask" ? [...levels].reverse() : levels;
|
||||
|
||||
return normalizedLevels.map((level) => {
|
||||
const price = side === "ask" ? level.askPrice : level.bidPrice;
|
||||
const size = side === "ask" ? level.askSize : level.bidSize;
|
||||
const { changeValue } = resolvePriceChange(price, basePrice);
|
||||
|
||||
return {
|
||||
price,
|
||||
size: Math.max(size, 0),
|
||||
changeValue,
|
||||
isHighlighted: latestPrice > 0 && price === latestPrice,
|
||||
} satisfies BookRow;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 호가/체결 증감 표시용 기준가(전일종가)를 결정합니다.
|
||||
* @summary UI 흐름: OrderBook props -> resolveReferencePrice -> 호가행/체결가 증감 계산 반영
|
||||
* @see features/trade/components/orderbook/OrderBook.tsx basePrice 계산
|
||||
*/
|
||||
function resolveReferencePrice({
|
||||
referencePrice,
|
||||
latestTick,
|
||||
}: {
|
||||
referencePrice?: number;
|
||||
latestTick: DashboardRealtimeTradeTick | null;
|
||||
}) {
|
||||
if ((referencePrice ?? 0) > 0) {
|
||||
return referencePrice!;
|
||||
}
|
||||
|
||||
// referencePrice 미전달 케이스에서도 틱 데이터(price-change)로 전일종가를 역산합니다.
|
||||
if (latestTick?.price && Number.isFinite(latestTick.change)) {
|
||||
const derivedPrevClose = latestTick.price - latestTick.change;
|
||||
if (derivedPrevClose > 0) {
|
||||
return derivedPrevClose;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ─── 메인 컴포넌트 ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -139,7 +242,6 @@ function resolveTickExecutionSide(
|
||||
export function OrderBook({
|
||||
symbol,
|
||||
referencePrice,
|
||||
currentPrice,
|
||||
latestTick,
|
||||
recentTicks,
|
||||
orderBook,
|
||||
@@ -162,42 +264,29 @@ export function OrderBook({
|
||||
latestTick?.price && latestTick.price > 0 ? latestTick.price : 0;
|
||||
|
||||
// 등락률 기준가
|
||||
const basePrice =
|
||||
(referencePrice ?? 0) > 0
|
||||
? referencePrice!
|
||||
: (currentPrice ?? 0) > 0
|
||||
? currentPrice!
|
||||
: latestPrice > 0
|
||||
? latestPrice
|
||||
: 0;
|
||||
const basePrice = resolveReferencePrice({ referencePrice, latestTick });
|
||||
|
||||
// 매도호가 (역순: 10호가 → 1호가)
|
||||
const askRows: BookRow[] = useMemo(
|
||||
() =>
|
||||
[...levels].reverse().map((l) => ({
|
||||
price: l.askPrice,
|
||||
size: Math.max(l.askSize, 0),
|
||||
changePercent:
|
||||
l.askPrice > 0 && basePrice > 0
|
||||
? pctChange(l.askPrice, basePrice)
|
||||
: null,
|
||||
isHighlighted: latestPrice > 0 && l.askPrice === latestPrice,
|
||||
})),
|
||||
buildBookRows({
|
||||
levels,
|
||||
side: "ask",
|
||||
basePrice,
|
||||
latestPrice,
|
||||
}),
|
||||
[levels, basePrice, latestPrice],
|
||||
);
|
||||
|
||||
// 매수호가 (1호가 → 10호가)
|
||||
const bidRows: BookRow[] = useMemo(
|
||||
() =>
|
||||
levels.map((l) => ({
|
||||
price: l.bidPrice,
|
||||
size: Math.max(l.bidSize, 0),
|
||||
changePercent:
|
||||
l.bidPrice > 0 && basePrice > 0
|
||||
? pctChange(l.bidPrice, basePrice)
|
||||
: null,
|
||||
isHighlighted: latestPrice > 0 && l.bidPrice === latestPrice,
|
||||
})),
|
||||
buildBookRows({
|
||||
levels,
|
||||
side: "bid",
|
||||
basePrice,
|
||||
latestPrice,
|
||||
}),
|
||||
[levels, basePrice, latestPrice],
|
||||
);
|
||||
|
||||
@@ -439,15 +528,11 @@ function BookSideRows({
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-[10px]",
|
||||
row.changePercent !== null
|
||||
? row.changePercent >= 0
|
||||
? "text-red-500"
|
||||
: "text-blue-600 dark:text-blue-400"
|
||||
: "text-muted-foreground",
|
||||
"w-[58px] shrink-0 text-right text-[10px] tabular-nums",
|
||||
getChangeToneClass(row.changeValue),
|
||||
)}
|
||||
>
|
||||
{row.changePercent === null ? "-" : fmtPct(row.changePercent)}
|
||||
{row.changeValue === null ? "-" : fmtSignedChange(row.changeValue)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -635,7 +720,12 @@ function TradeTape({ ticks }: { ticks: DashboardRealtimeTradeTick[] }) {
|
||||
<div className="flex items-center tabular-nums">
|
||||
{fmtTime(t.tickTime)}
|
||||
</div>
|
||||
<div className="flex items-center justify-end tabular-nums text-red-600">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-end tabular-nums",
|
||||
getChangeToneClass(t.change, "text-foreground dark:text-brand-50"),
|
||||
)}
|
||||
>
|
||||
{fmt(t.price)}
|
||||
</div>
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user