"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store"; import { fetchDashboardActivity, fetchDashboardBalance, fetchDashboardIndices, fetchDashboardMarketHub, } from "@/features/dashboard/apis/dashboard.api"; import type { DashboardActivityResponse, DashboardBalanceResponse, DashboardIndicesResponse, DashboardMarketHubResponse, } from "@/features/dashboard/types/dashboard.types"; interface UseDashboardDataResult { activity: DashboardActivityResponse | null; balance: DashboardBalanceResponse | null; indices: DashboardIndicesResponse["items"]; selectedSymbol: string | null; setSelectedSymbol: (symbol: string) => void; isLoading: boolean; isRefreshing: boolean; activityError: string | null; balanceError: string | null; indicesError: string | null; marketHub: DashboardMarketHubResponse | null; marketHubError: string | null; lastUpdatedAt: string | null; refresh: () => Promise; } 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(null); const [balance, setBalance] = useState(null); const [indices, setIndices] = useState([]); const [selectedSymbol, setSelectedSymbolState] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [activityError, setActivityError] = useState(null); const [balanceError, setBalanceError] = useState(null); const [indicesError, setIndicesError] = useState(null); const [marketHub, setMarketHub] = useState(null); const [marketHubError, setMarketHubError] = useState(null); const [lastUpdatedAt, setLastUpdatedAt] = useState(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, Promise, Promise, Promise, ] = [ hasAccountNo ? fetchDashboardBalance(credentials) : Promise.resolve(null), fetchDashboardIndices(credentials), hasAccountNo ? fetchDashboardActivity(credentials) : Promise.resolve(null), fetchDashboardMarketHub(credentials), ]; const [ balanceResult, indicesResult, activityResult, marketHubResult, ] = await Promise.allSettled(tasks); if (requestSeq !== requestSeqRef.current) return; let hasAnySuccess = false; if (!hasAccountNo) { setBalance(null); setBalanceError( "계좌번호가 없어 잔고를 조회할 수 없습니다. 설정에서 계좌번호(8-2)를 입력해 주세요.", ); setActivity(null); setActivityError( "계좌번호가 없어 주문내역/매매일지를 조회할 수 없습니다. 설정에서 계좌번호(8-2)를 입력해 주세요.", ); 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 : "주문내역/매매일지 조회에 실패했습니다."); } if (indicesResult.status === "fulfilled") { hasAnySuccess = true; setIndices(indicesResult.value.items); setIndicesError(null); } else { setIndicesError(indicesResult.reason instanceof Error ? indicesResult.reason.message : "시장 지수 조회에 실패했습니다."); } if (marketHubResult.status === "fulfilled") { hasAnySuccess = true; setMarketHub(marketHubResult.value); setMarketHubError(null); } else { setMarketHubError( marketHubResult.reason instanceof Error ? marketHubResult.reason.message : "시장 허브 조회에 실패했습니다.", ); } 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, balance, indices, selectedSymbol, setSelectedSymbol, isLoading, isRefreshing, activityError, balanceError, indicesError, marketHub, marketHubError, lastUpdatedAt, refresh, }; }