"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store"; import { fetchDashboardBalance, fetchDashboardIndices, } from "@/features/dashboard/apis/dashboard.api"; import type { DashboardBalanceResponse, DashboardHoldingItem, DashboardIndicesResponse, } from "@/features/dashboard/types/dashboard.types"; interface UseDashboardDataResult { balance: DashboardBalanceResponse | null; indices: DashboardIndicesResponse["items"]; selectedHolding: DashboardHoldingItem | null; selectedSymbol: string | null; setSelectedSymbol: (symbol: string) => void; isLoading: boolean; isRefreshing: boolean; balanceError: string | null; indicesError: 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 [balance, setBalance] = useState(null); const [indices, setIndices] = useState([]); const [selectedSymbol, setSelectedSymbolState] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [balanceError, setBalanceError] = useState(null); const [indicesError, setIndicesError] = 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, ] = [ hasAccountNo ? fetchDashboardBalance(credentials) : Promise.resolve(null), fetchDashboardIndices(credentials), ]; const [balanceResult, indicesResult] = await Promise.allSettled(tasks); if (requestSeq !== requestSeqRef.current) return; let hasAnySuccess = false; if (!hasAccountNo) { setBalance(null); setBalanceError( "계좌번호가 없어 잔고를 조회할 수 없습니다. 설정에서 계좌번호(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 (indicesResult.status === "fulfilled") { hasAnySuccess = true; setIndices(indicesResult.value.items); setIndicesError(null); } else { setIndicesError(indicesResult.reason instanceof Error ? indicesResult.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 selectedHolding = useMemo(() => { if (!selectedSymbol || !balance) return null; return balance.holdings.find((item) => item.symbol === selectedSymbol) ?? null; }, [balance, selectedSymbol]); const setSelectedSymbol = useCallback((symbol: string) => { setSelectedSymbolState(symbol); }, []); return { balance, indices, selectedHolding, selectedSymbol, setSelectedSymbol, isLoading, isRefreshing, balanceError, indicesError, lastUpdatedAt, refresh, }; }