Files
auto-trade/features/dashboard/hooks/use-dashboard-data.ts

226 lines
7.5 KiB
TypeScript
Raw Normal View History

2026-02-12 14:20:07 +09:00
"use client";
2026-02-13 12:17:35 +09:00
import { useCallback, useEffect, useRef, useState } from "react";
2026-02-12 14:20:07 +09:00
import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store";
import {
fetchDashboardActivity,
2026-02-12 14:20:07 +09:00
fetchDashboardBalance,
fetchDashboardIndices,
2026-03-12 09:26:27 +09:00
fetchDashboardMarketHub,
2026-02-12 14:20:07 +09:00
} from "@/features/dashboard/apis/dashboard.api";
import type {
DashboardActivityResponse,
2026-02-12 14:20:07 +09:00
DashboardBalanceResponse,
DashboardIndicesResponse,
2026-03-12 09:26:27 +09:00
DashboardMarketHubResponse,
2026-02-12 14:20:07 +09:00
} from "@/features/dashboard/types/dashboard.types";
interface UseDashboardDataResult {
activity: DashboardActivityResponse | null;
2026-02-12 14:20:07 +09:00
balance: DashboardBalanceResponse | null;
indices: DashboardIndicesResponse["items"];
selectedSymbol: string | null;
setSelectedSymbol: (symbol: string) => void;
isLoading: boolean;
isRefreshing: boolean;
activityError: string | null;
2026-02-12 14:20:07 +09:00
balanceError: string | null;
indicesError: string | null;
2026-03-12 09:26:27 +09:00
marketHub: DashboardMarketHubResponse | null;
marketHubError: string | null;
2026-02-12 14:20:07 +09:00
lastUpdatedAt: string | null;
refresh: () => Promise<void>;
}
const POLLING_INTERVAL_MS = 60_000;
/**
* @description / .
* @param credentials KIS
* @returns //
* @remarks UI 흐름: 대시보드 -> refresh("initial") -> balance/indices API ->
* @see features/dashboard/components/DashboardContainer.tsx
* @see features/dashboard/apis/dashboard.api.ts API
*/
export function useDashboardData(
credentials: KisRuntimeCredentials | null,
): UseDashboardDataResult {
const [activity, setActivity] = useState<DashboardActivityResponse | null>(null);
2026-02-12 14:20:07 +09:00
const [balance, setBalance] = useState<DashboardBalanceResponse | null>(null);
const [indices, setIndices] = useState<DashboardIndicesResponse["items"]>([]);
const [selectedSymbol, setSelectedSymbolState] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [activityError, setActivityError] = useState<string | null>(null);
2026-02-12 14:20:07 +09:00
const [balanceError, setBalanceError] = useState<string | null>(null);
const [indicesError, setIndicesError] = useState<string | null>(null);
2026-03-12 09:26:27 +09:00
const [marketHub, setMarketHub] = useState<DashboardMarketHubResponse | null>(null);
const [marketHubError, setMarketHubError] = useState<string | null>(null);
2026-02-12 14:20:07 +09:00
const [lastUpdatedAt, setLastUpdatedAt] = useState<string | null>(null);
const requestSeqRef = useRef(0);
const hasAccountNo = Boolean(credentials?.accountNo?.trim());
/**
* @description / .
* @param mode / /
* @see features/dashboard/hooks/use-dashboard-data.ts useEffect //
*/
const refreshInternal = useCallback(
async (mode: "initial" | "manual" | "polling") => {
if (!credentials) return;
const requestSeq = ++requestSeqRef.current;
const isInitial = mode === "initial";
if (isInitial) {
setIsLoading(true);
} else {
setIsRefreshing(true);
}
const tasks: [
Promise<DashboardBalanceResponse | null>,
Promise<DashboardIndicesResponse>,
Promise<DashboardActivityResponse | null>,
2026-03-12 09:26:27 +09:00
Promise<DashboardMarketHubResponse>,
2026-02-12 14:20:07 +09:00
] = [
hasAccountNo
? fetchDashboardBalance(credentials)
: Promise.resolve(null),
fetchDashboardIndices(credentials),
hasAccountNo
? fetchDashboardActivity(credentials)
: Promise.resolve(null),
2026-03-12 09:26:27 +09:00
fetchDashboardMarketHub(credentials),
2026-02-12 14:20:07 +09:00
];
2026-03-12 09:26:27 +09:00
const [
balanceResult,
indicesResult,
activityResult,
marketHubResult,
] = await Promise.allSettled(tasks);
2026-02-12 14:20:07 +09:00
if (requestSeq !== requestSeqRef.current) return;
let hasAnySuccess = false;
if (!hasAccountNo) {
setBalance(null);
setBalanceError(
"계좌번호가 없어 잔고를 조회할 수 없습니다. 설정에서 계좌번호(8-2)를 입력해 주세요.",
);
setActivity(null);
setActivityError(
"계좌번호가 없어 주문내역/매매일지를 조회할 수 없습니다. 설정에서 계좌번호(8-2)를 입력해 주세요.",
);
2026-02-12 14:20:07 +09:00
setSelectedSymbolState(null);
} else if (balanceResult.status === "fulfilled") {
hasAnySuccess = true;
setBalance(balanceResult.value);
setBalanceError(null);
setSelectedSymbolState((prev) => {
const nextHoldings = balanceResult.value?.holdings ?? [];
if (nextHoldings.length === 0) return null;
if (prev && nextHoldings.some((item) => item.symbol === prev)) {
return prev;
}
return nextHoldings[0]?.symbol ?? null;
});
} else {
setBalanceError(balanceResult.reason instanceof Error ? balanceResult.reason.message : "잔고 조회에 실패했습니다.");
}
if (hasAccountNo && activityResult.status === "fulfilled") {
hasAnySuccess = true;
setActivity(activityResult.value);
setActivityError(null);
} else if (hasAccountNo && activityResult.status === "rejected") {
setActivityError(activityResult.reason instanceof Error ? activityResult.reason.message : "주문내역/매매일지 조회에 실패했습니다.");
}
2026-02-12 14:20:07 +09:00
if (indicesResult.status === "fulfilled") {
hasAnySuccess = true;
setIndices(indicesResult.value.items);
setIndicesError(null);
} else {
setIndicesError(indicesResult.reason instanceof Error ? indicesResult.reason.message : "시장 지수 조회에 실패했습니다.");
}
2026-03-12 09:26:27 +09:00
if (marketHubResult.status === "fulfilled") {
hasAnySuccess = true;
setMarketHub(marketHubResult.value);
setMarketHubError(null);
} else {
setMarketHubError(
marketHubResult.reason instanceof Error
? marketHubResult.reason.message
: "시장 허브 조회에 실패했습니다.",
);
}
2026-02-12 14:20:07 +09:00
if (hasAnySuccess) {
setLastUpdatedAt(new Date().toISOString());
}
if (isInitial) {
setIsLoading(false);
} else {
setIsRefreshing(false);
}
},
[credentials, hasAccountNo],
);
/**
* @description .
* @see features/dashboard/components/StatusHeader.tsx onClick
*/
const refresh = useCallback(async () => {
await refreshInternal("manual");
}, [refreshInternal]);
useEffect(() => {
if (!credentials) return;
const timeout = window.setTimeout(() => {
void refreshInternal("initial");
}, 0);
return () => window.clearTimeout(timeout);
}, [credentials, refreshInternal]);
useEffect(() => {
if (!credentials) return;
const interval = window.setInterval(() => {
void refreshInternal("polling");
}, POLLING_INTERVAL_MS);
return () => window.clearInterval(interval);
}, [credentials, refreshInternal]);
const setSelectedSymbol = useCallback((symbol: string) => {
setSelectedSymbolState(symbol);
}, []);
return {
activity,
2026-02-12 14:20:07 +09:00
balance,
indices,
selectedSymbol,
setSelectedSymbol,
isLoading,
isRefreshing,
activityError,
2026-02-12 14:20:07 +09:00
balanceError,
indicesError,
2026-03-12 09:26:27 +09:00
marketHub,
marketHubError,
2026-02-12 14:20:07 +09:00
lastUpdatedAt,
refresh,
};
}