/** * CEO Dashboard API 연동 Hook * * 각 섹션별 API 호출 및 데이터 변환 담당 * 제네릭 useDashboardFetch 훅을 활용하여 보일러플레이트 최소화 */ import { useState, useCallback, useEffect, useMemo } from 'react'; import { useDashboardFetch } from './useDashboardFetch'; import { fetchLoanDashboard, fetchTaxSimulation, } from '@/lib/api/dashboard/endpoints'; import type { DailyReportApiResponse, ReceivablesApiResponse, BadDebtApiResponse, ExpectedExpenseApiResponse, CardTransactionApiResponse, StatusBoardApiResponse, TodayIssueApiResponse, CalendarApiResponse, VatApiResponse, EntertainmentApiResponse, WelfareApiResponse, WelfareDetailApiResponse, ExpectedExpenseDashboardDetailApiResponse, } from '@/lib/api/dashboard/types'; import { transformDailyReportResponse, transformReceivableResponse, transformDebtCollectionResponse, transformMonthlyExpenseResponse, transformCardManagementResponse, transformStatusBoardResponse, transformTodayIssueResponse, transformCalendarResponse, transformVatResponse, transformEntertainmentResponse, transformWelfareResponse, transformWelfareDetailResponse, transformExpectedExpenseDetailResponse, } from '@/lib/api/dashboard/transformers'; import type { DailyReportData, ReceivableData, DebtCollectionData, MonthlyExpenseData, CardManagementData, TodayIssueItem, TodayIssueListItem, CalendarScheduleItem, VatData, EntertainmentData, WelfareData, DetailModalConfig, } from '@/components/business/CEODashboard/types'; // ============================================ // 쿼리 파라미터 빌더 유틸리티 // ============================================ function buildEndpoint( base: string, params: Record, ): string { const searchParams = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value != null && value !== '') { searchParams.append(key, String(value)); } } const qs = searchParams.toString(); return qs ? `${base}?${qs}` : base; } // ============================================ // CardManagement 전용 fetch 유틸리티 // ============================================ async function fetchCardManagementData(fallbackData?: CardManagementData) { const [cardApiData, loanResponse, taxResponse] = await Promise.all([ fetch('/api/proxy/card-transactions/summary').then(async (r) => { if (!r.ok) throw new Error(`API 오류: ${r.status}`); const json = await r.json(); if (!json.success) throw new Error(json.message || '데이터 조회 실패'); return json.data as CardTransactionApiResponse; }), fetchLoanDashboard(), fetchTaxSimulation(), ]); const loanData = loanResponse.success ? loanResponse.data : null; const taxData = taxResponse.success ? taxResponse.data : null; return transformCardManagementResponse(cardApiData, loanData, taxData, fallbackData); } // ============================================ // 1~4. 단순 섹션 Hooks (파라미터 없음) // ============================================ export function useDailyReport() { return useDashboardFetch( 'daily-report/summary', transformDailyReportResponse, ); } export function useReceivable() { return useDashboardFetch( 'receivables/summary', transformReceivableResponse, ); } export function useDebtCollection() { return useDashboardFetch( 'bad-debts/summary', transformDebtCollectionResponse, ); } export function useMonthlyExpense() { return useDashboardFetch( 'expected-expenses/summary', transformMonthlyExpenseResponse, ); } // ============================================ // 5. CardManagement Hook (커스텀: 3개 API 병렬 호출) // ============================================ export function useCardManagement(fallbackData?: CardManagementData) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchData = useCallback(async () => { try { setLoading(true); setError(null); const result = await fetchCardManagementData(fallbackData); setData(result); } catch (err) { setError(err instanceof Error ? err.message : '데이터 로딩 실패'); console.error('CardManagement API Error:', err); } finally { setLoading(false); } }, [fallbackData]); useEffect(() => { fetchData(); }, [fetchData]); return { data, loading, error, refetch: fetchData }; } // ============================================ // 6. StatusBoard Hook // ============================================ export function useStatusBoard() { return useDashboardFetch( 'status-board/summary', transformStatusBoardResponse, ); } // ============================================ // 7. TodayIssue Hook // ============================================ export interface TodayIssueData { items: TodayIssueListItem[]; totalCount: number; } export function useTodayIssue(limit: number = 30) { const endpoint = useMemo( () => buildEndpoint('today-issues/summary', { limit }), [limit], ); return useDashboardFetch( endpoint, transformTodayIssueResponse, ); } // ============================================ // 7-1. PastIssue Hook (이전 이슈 - 날짜별 조회) // ============================================ export function usePastIssue(date: string | null, limit: number = 30) { const endpoint = useMemo( () => (date ? buildEndpoint('today-issues/summary', { limit, date }) : null), [date, limit], ); return useDashboardFetch( endpoint, transformTodayIssueResponse, { initialLoading: false }, ); } // ============================================ // 8. Calendar Hook // ============================================ export interface CalendarData { items: CalendarScheduleItem[]; totalCount: number; } export interface UseCalendarOptions { startDate?: string; endDate?: string; type?: 'schedule' | 'order' | 'construction' | 'other' | null; departmentFilter?: 'all' | 'department' | 'personal'; } export function useCalendar(options: UseCalendarOptions = {}) { const { startDate, endDate, type, departmentFilter = 'all' } = options; const endpoint = useMemo( () => buildEndpoint('calendar/schedules', { start_date: startDate, end_date: endDate, type: type ?? undefined, department_filter: departmentFilter, }), [startDate, endDate, type, departmentFilter], ); return useDashboardFetch( endpoint, transformCalendarResponse, ); } // ============================================ // 9. Vat Hook // ============================================ export interface UseVatOptions { periodType?: 'quarter' | 'half' | 'year'; year?: number; period?: number; } export function useVat(options: UseVatOptions = {}) { const { periodType = 'quarter', year, period } = options; const endpoint = useMemo( () => buildEndpoint('vat/summary', { period_type: periodType, year, period, }), [periodType, year, period], ); return useDashboardFetch(endpoint, transformVatResponse); } // ============================================ // 10. Entertainment Hook (접대비) // ============================================ export interface UseEntertainmentOptions { limitType?: 'annual' | 'quarterly'; companyType?: 'large' | 'medium' | 'small'; year?: number; quarter?: number; } export function useEntertainment(options: UseEntertainmentOptions = {}) { const { limitType = 'quarterly', companyType = 'medium', year, quarter } = options; const endpoint = useMemo( () => buildEndpoint('entertainment/summary', { limit_type: limitType, company_type: companyType, year, quarter, }), [limitType, companyType, year, quarter], ); return useDashboardFetch( endpoint, transformEntertainmentResponse, ); } // ============================================ // 11. Welfare Hook (복리후생비) // ============================================ export interface UseWelfareOptions { limitType?: 'annual' | 'quarterly'; calculationType?: 'fixed' | 'ratio'; fixedAmountPerMonth?: number; ratio?: number; year?: number; quarter?: number; } export function useWelfare(options: UseWelfareOptions = {}) { const { limitType = 'quarterly', calculationType = 'fixed', fixedAmountPerMonth, ratio, year, quarter, } = options; const endpoint = useMemo( () => buildEndpoint('welfare/summary', { limit_type: limitType, calculation_type: calculationType, fixed_amount_per_month: fixedAmountPerMonth, ratio, year, quarter, }), [limitType, calculationType, fixedAmountPerMonth, ratio, year, quarter], ); return useDashboardFetch( endpoint, transformWelfareResponse, ); } // ============================================ // 12. WelfareDetail Hook (복리후생비 상세 - 모달용) // ============================================ export interface UseWelfareDetailOptions { calculationType?: 'fixed' | 'ratio'; fixedAmountPerMonth?: number; ratio?: number; year?: number; quarter?: number; } export function useWelfareDetail(options: UseWelfareDetailOptions = {}) { const { calculationType = 'fixed', fixedAmountPerMonth, ratio, year, quarter, } = options; const endpoint = useMemo( () => buildEndpoint('welfare/detail', { calculation_type: calculationType, fixed_amount_per_month: fixedAmountPerMonth, ratio, year, quarter, }), [calculationType, fixedAmountPerMonth, ratio, year, quarter], ); const result = useDashboardFetch( endpoint, transformWelfareDetailResponse, { lazy: true }, ); return { modalConfig: result.data, loading: result.loading, error: result.error, refetch: result.refetch, }; } // ============================================ // 13. MonthlyExpenseDetail Hook (당월 예상 지출 상세 - 통합 모달용) // ============================================ export type MonthlyExpenseCardId = 'me1' | 'me2' | 'me3' | 'me4'; export function useMonthlyExpenseDetail() { const [modalConfig, setModalConfig] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const fetchData = useCallback(async (cardId: MonthlyExpenseCardId) => { try { setLoading(true); setError(null); const transactionTypeMap: Record = { me1: 'purchase', me2: 'card', me3: 'bill', me4: null, }; const transactionType = transactionTypeMap[cardId]; const endpoint = transactionType ? `/api/proxy/expected-expenses/dashboard-detail?transaction_type=${transactionType}` : '/api/proxy/expected-expenses/dashboard-detail'; const response = await fetch(endpoint); if (!response.ok) throw new Error(`API 오류: ${response.status}`); const result = await response.json(); if (!result.success) throw new Error(result.message || '데이터 조회 실패'); const transformed = transformExpectedExpenseDetailResponse( result.data as ExpectedExpenseDashboardDetailApiResponse, cardId, ); setModalConfig(transformed); return transformed; } catch (err) { const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패'; setError(errorMessage); console.error('MonthlyExpenseDetail API Error:', err); return null; } finally { setLoading(false); } }, []); return { modalConfig, loading, error, fetchData }; } // ============================================ // 통합 Dashboard Hook // ============================================ export interface UseCEODashboardOptions { dailyReport?: boolean; receivable?: boolean; debtCollection?: boolean; monthlyExpense?: boolean; cardManagement?: boolean; cardManagementFallback?: CardManagementData; statusBoard?: boolean; } export interface CEODashboardState { dailyReport: { data: DailyReportData | null; loading: boolean; error: string | null }; receivable: { data: ReceivableData | null; loading: boolean; error: string | null }; debtCollection: { data: DebtCollectionData | null; loading: boolean; error: string | null }; monthlyExpense: { data: MonthlyExpenseData | null; loading: boolean; error: string | null }; cardManagement: { data: CardManagementData | null; loading: boolean; error: string | null }; statusBoard: { data: TodayIssueItem[] | null; loading: boolean; error: string | null }; refetchAll: () => void; } export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashboardState { const { dailyReport: enableDailyReport = true, receivable: enableReceivable = true, debtCollection: enableDebtCollection = true, monthlyExpense: enableMonthlyExpense = true, cardManagement: enableCardManagement = true, cardManagementFallback, statusBoard: enableStatusBoard = true, } = options; // 비활성 섹션은 endpoint를 null로 → useDashboardFetch가 skip const dr = useDashboardFetch( enableDailyReport ? 'daily-report/summary' : null, transformDailyReportResponse, { initialLoading: enableDailyReport }, ); const rv = useDashboardFetch( enableReceivable ? 'receivables/summary' : null, transformReceivableResponse, { initialLoading: enableReceivable }, ); const dc = useDashboardFetch( enableDebtCollection ? 'bad-debts/summary' : null, transformDebtCollectionResponse, { initialLoading: enableDebtCollection }, ); const me = useDashboardFetch( enableMonthlyExpense ? 'expected-expenses/summary' : null, transformMonthlyExpenseResponse, { initialLoading: enableMonthlyExpense }, ); const sb = useDashboardFetch( enableStatusBoard ? 'status-board/summary' : null, transformStatusBoardResponse, { initialLoading: enableStatusBoard }, ); // CardManagement: 커스텀 (3개 API 병렬) const [cmData, setCmData] = useState(null); const [cmLoading, setCmLoading] = useState(enableCardManagement); const [cmError, setCmError] = useState(null); const fetchCM = useCallback(async () => { if (!enableCardManagement) return; try { setCmLoading(true); setCmError(null); const result = await fetchCardManagementData(cardManagementFallback); setCmData(result); } catch (err) { setCmError(err instanceof Error ? err.message : '데이터 로딩 실패'); console.error('CardManagement API Error:', err); } finally { setCmLoading(false); } }, [enableCardManagement, cardManagementFallback]); useEffect(() => { fetchCM(); }, [fetchCM]); const refetchAll = useCallback(() => { dr.refetch(); rv.refetch(); dc.refetch(); me.refetch(); fetchCM(); sb.refetch(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [dr.refetch, rv.refetch, dc.refetch, me.refetch, fetchCM, sb.refetch]); return { dailyReport: { data: dr.data, loading: dr.loading, error: dr.error }, receivable: { data: rv.data, loading: rv.loading, error: rv.error }, debtCollection: { data: dc.data, loading: dc.loading, error: dc.error }, monthlyExpense: { data: me.data, loading: me.loading, error: me.error }, cardManagement: { data: cmData, loading: cmLoading, error: cmError }, statusBoard: { data: sb.data, loading: sb.loading, error: sb.error }, refetchAll, }; }