119 lines
3.5 KiB
TypeScript
119 lines
3.5 KiB
TypeScript
|
|
import { useCallback, useState, useTransition } from "react";
|
||
|
|
import type { KisRuntimeCredentials } from "@/features/dashboard/store/use-kis-runtime-store";
|
||
|
|
import type {
|
||
|
|
DashboardMarketPhase,
|
||
|
|
DashboardPriceSource,
|
||
|
|
DashboardRealtimeTradeTick,
|
||
|
|
DashboardStockSearchItem,
|
||
|
|
DashboardStockItem,
|
||
|
|
} from "@/features/dashboard/types/dashboard.types";
|
||
|
|
import { fetchStockOverview } from "@/features/dashboard/apis/kis-stock.api";
|
||
|
|
|
||
|
|
interface OverviewMeta {
|
||
|
|
priceSource: DashboardPriceSource;
|
||
|
|
marketPhase: DashboardMarketPhase;
|
||
|
|
fetchedAt: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useStockOverview() {
|
||
|
|
const [selectedStock, setSelectedStock] = useState<DashboardStockItem | null>(
|
||
|
|
null,
|
||
|
|
);
|
||
|
|
const [meta, setMeta] = useState<OverviewMeta | null>(null);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
const [isLoading, startTransition] = useTransition();
|
||
|
|
|
||
|
|
const loadOverview = useCallback(
|
||
|
|
(
|
||
|
|
symbol: string,
|
||
|
|
credentials: KisRuntimeCredentials | null,
|
||
|
|
marketHint?: DashboardStockSearchItem["market"],
|
||
|
|
) => {
|
||
|
|
if (!credentials) return;
|
||
|
|
|
||
|
|
startTransition(async () => {
|
||
|
|
try {
|
||
|
|
setError(null);
|
||
|
|
const data = await fetchStockOverview(symbol, credentials);
|
||
|
|
setSelectedStock({
|
||
|
|
...data.stock,
|
||
|
|
market: marketHint ?? data.stock.market,
|
||
|
|
});
|
||
|
|
setMeta({
|
||
|
|
priceSource: data.priceSource,
|
||
|
|
marketPhase: data.marketPhase,
|
||
|
|
fetchedAt: data.fetchedAt,
|
||
|
|
});
|
||
|
|
} catch (err) {
|
||
|
|
const message =
|
||
|
|
err instanceof Error
|
||
|
|
? err.message
|
||
|
|
: "종목 조회 중 오류가 발생했습니다.";
|
||
|
|
setError(message);
|
||
|
|
setMeta(null);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
},
|
||
|
|
[],
|
||
|
|
);
|
||
|
|
|
||
|
|
// 실시간 체결 데이터 수신 시 헤더/차트 기준 가격을 갱신합니다.
|
||
|
|
const updateRealtimeTradeTick = useCallback(
|
||
|
|
(tick: DashboardRealtimeTradeTick) => {
|
||
|
|
setSelectedStock((prev) => {
|
||
|
|
if (!prev) return prev;
|
||
|
|
const { price, accumulatedVolume, change, changeRate, tickTime } = tick;
|
||
|
|
const pointTime =
|
||
|
|
tickTime && tickTime.length === 6
|
||
|
|
? `${tickTime.slice(0, 2)}:${tickTime.slice(2, 4)}`
|
||
|
|
: "실시간";
|
||
|
|
|
||
|
|
const nextChange = change;
|
||
|
|
const nextChangeRate = Number.isFinite(changeRate)
|
||
|
|
? changeRate
|
||
|
|
: prev.prevClose > 0
|
||
|
|
? (nextChange / prev.prevClose) * 100
|
||
|
|
: prev.changeRate;
|
||
|
|
const nextHigh = prev.high > 0 ? Math.max(prev.high, price) : price;
|
||
|
|
const nextLow = prev.low > 0 ? Math.min(prev.low, price) : price;
|
||
|
|
const nextCandles =
|
||
|
|
prev.candles.length > 0 &&
|
||
|
|
prev.candles[prev.candles.length - 1]?.time === pointTime
|
||
|
|
? [
|
||
|
|
...prev.candles.slice(0, -1),
|
||
|
|
{
|
||
|
|
...prev.candles[prev.candles.length - 1],
|
||
|
|
time: pointTime,
|
||
|
|
price,
|
||
|
|
},
|
||
|
|
]
|
||
|
|
: [...prev.candles, { time: pointTime, price }].slice(-80);
|
||
|
|
|
||
|
|
return {
|
||
|
|
...prev,
|
||
|
|
currentPrice: price,
|
||
|
|
change: nextChange,
|
||
|
|
changeRate: nextChangeRate,
|
||
|
|
high: nextHigh,
|
||
|
|
low: nextLow,
|
||
|
|
volume: accumulatedVolume > 0 ? accumulatedVolume : prev.volume,
|
||
|
|
candles: nextCandles,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
},
|
||
|
|
[],
|
||
|
|
);
|
||
|
|
|
||
|
|
return {
|
||
|
|
selectedStock,
|
||
|
|
setSelectedStock,
|
||
|
|
meta,
|
||
|
|
setMeta,
|
||
|
|
error,
|
||
|
|
setError,
|
||
|
|
isLoading,
|
||
|
|
loadOverview,
|
||
|
|
updateRealtimeTradeTick,
|
||
|
|
};
|
||
|
|
}
|