대시보드 구현
This commit is contained in:
182
features/dashboard/hooks/use-dashboard-data.ts
Normal file
182
features/dashboard/hooks/use-dashboard-data.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
"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<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 [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 [balanceError, setBalanceError] = useState<string | null>(null);
|
||||
const [indicesError, setIndicesError] = useState<string | null>(null);
|
||||
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>,
|
||||
] = [
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user