Files
sam-react-prod/src/hooks/useCEODashboard.ts
권혁성 9cfd10a265 feat(CEODashboard): Phase 4 - 카드/가지급금 관리 섹션 카드 API 연동
카드/가지급금 관리 섹션의 4개 카드(cm1~cm4)를 실제 API 데이터로 연동:

- cm1: 카드 사용액 - CardTransaction API (기존)
- cm2: 가지급금 - LoanDashboard API (신규 연동)
- cm3: 법인세 예상 가중 - TaxSimulation API (신규 연동)
- cm4: 대표자 종합세 예상 가중 - TaxSimulation API (신규 연동)

변경 사항:
- transformCardManagementResponse: LoanDashboard, TaxSimulation 파라미터 추가
- useCEODashboard: 3개 API 병렬 호출 (Promise.all)
- useCardManagement: 동일하게 다중 API 호출 적용
- 각 API 실패 시 fallback 데이터 사용 (graceful degradation)
2026-01-23 09:04:56 +09:00

1132 lines
36 KiB
TypeScript

'use client';
/**
* CEO Dashboard API 연동 Hook
*
* 각 섹션별 API 호출 및 데이터 변환 담당
* 참조 패턴: useClientList.ts
*/
import { useState, useCallback, useEffect } from 'react';
import type {
DailyReportApiResponse,
ReceivablesApiResponse,
BadDebtApiResponse,
ExpectedExpenseApiResponse,
CardTransactionApiResponse,
StatusBoardApiResponse,
TodayIssueApiResponse,
CalendarApiResponse,
VatApiResponse,
EntertainmentApiResponse,
WelfareApiResponse,
WelfareDetailApiResponse,
ExpectedExpenseDashboardDetailApiResponse,
LoanDashboardApiResponse,
TaxSimulationApiResponse,
} from '@/lib/api/dashboard/types';
import {
fetchLoanDashboard,
fetchTaxSimulation,
} from '@/lib/api/dashboard/endpoints';
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';
// ============================================
// 공통 fetch 유틸리티
// ============================================
async function fetchApi<T>(endpoint: string): Promise<T> {
const response = await fetch(`/api/proxy/${endpoint}`);
if (!response.ok) {
throw new Error(`API 오류: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || '데이터 조회 실패');
}
return result.data;
}
// ============================================
// 1. DailyReport Hook
// ============================================
export function useDailyReport() {
const [data, setData] = useState<DailyReportData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const apiData = await fetchApi<DailyReportApiResponse>('daily-report/summary');
const transformed = transformDailyReportResponse(apiData);
setData(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
console.error('DailyReport API Error:', err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// ============================================
// 2. Receivable Hook
// ============================================
export function useReceivable() {
const [data, setData] = useState<ReceivableData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const apiData = await fetchApi<ReceivablesApiResponse>('receivables/summary');
const transformed = transformReceivableResponse(apiData);
setData(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
console.error('Receivable API Error:', err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// ============================================
// 3. DebtCollection Hook
// ============================================
export function useDebtCollection() {
const [data, setData] = useState<DebtCollectionData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const apiData = await fetchApi<BadDebtApiResponse>('bad-debts/summary');
const transformed = transformDebtCollectionResponse(apiData);
setData(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
console.error('DebtCollection API Error:', err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// ============================================
// 4. MonthlyExpense Hook
// ============================================
export function useMonthlyExpense() {
const [data, setData] = useState<MonthlyExpenseData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const apiData = await fetchApi<ExpectedExpenseApiResponse>('expected-expenses/summary');
const transformed = transformMonthlyExpenseResponse(apiData);
setData(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
console.error('MonthlyExpense API Error:', err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// ============================================
// 5. CardManagement Hook
// ============================================
export function useCardManagement(fallbackData?: CardManagementData) {
const [data, setData] = useState<CardManagementData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
// 3개 API 병렬 호출: 카드거래, 가지급금, 세금 시뮬레이션
const [cardApiData, loanResponse, taxResponse] = await Promise.all([
fetchApi<CardTransactionApiResponse>('card-transactions/summary'),
fetchLoanDashboard(),
fetchTaxSimulation(),
]);
// LoanDashboard와 TaxSimulation은 ApiResponse wrapper가 있으므로 data 추출
const loanData = loanResponse.success ? loanResponse.data : null;
const taxData = taxResponse.success ? taxResponse.data : null;
const transformed = transformCardManagementResponse(cardApiData, loanData, taxData, fallbackData);
setData(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
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() {
const [data, setData] = useState<TodayIssueItem[] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const apiData = await fetchApi<StatusBoardApiResponse>('status-board/summary');
const transformed = transformStatusBoardResponse(apiData);
setData(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
console.error('StatusBoard API Error:', err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// ============================================
// 7. TodayIssue Hook
// ============================================
export interface TodayIssueData {
items: TodayIssueListItem[];
totalCount: number;
}
export function useTodayIssue(limit: number = 30) {
const [data, setData] = useState<TodayIssueData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const apiData = await fetchApi<TodayIssueApiResponse>(`today-issues/summary?limit=${limit}`);
const transformed = transformTodayIssueResponse(apiData);
setData(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
console.error('TodayIssue API Error:', err);
} finally {
setLoading(false);
}
}, [limit]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// ============================================
// 8. Calendar Hook
// ============================================
export interface CalendarData {
items: CalendarScheduleItem[];
totalCount: number;
}
export interface UseCalendarOptions {
/** 조회 시작일 (Y-m-d, 기본: 이번 달 1일) */
startDate?: string;
/** 조회 종료일 (Y-m-d, 기본: 이번 달 말일) */
endDate?: string;
/** 일정 타입 필터 (schedule|order|construction|other|null=전체) */
type?: 'schedule' | 'order' | 'construction' | 'other' | null;
/** 부서 필터 (all|department|personal) */
departmentFilter?: 'all' | 'department' | 'personal';
}
export function useCalendar(options: UseCalendarOptions = {}) {
const { startDate, endDate, type, departmentFilter = 'all' } = options;
const [data, setData] = useState<CalendarData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
// 쿼리 파라미터 구성
const params = new URLSearchParams();
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
if (type) params.append('type', type);
if (departmentFilter) params.append('department_filter', departmentFilter);
const queryString = params.toString();
const endpoint = queryString ? `calendar/schedules?${queryString}` : 'calendar/schedules';
const apiData = await fetchApi<CalendarApiResponse>(endpoint);
const transformed = transformCalendarResponse(apiData);
setData(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
console.error('Calendar API Error:', err);
} finally {
setLoading(false);
}
}, [startDate, endDate, type, departmentFilter]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// ============================================
// 9. Vat Hook
// ============================================
export interface UseVatOptions {
/** 기간 타입 (quarter: 분기, half: 반기, year: 연간) */
periodType?: 'quarter' | 'half' | 'year';
/** 연도 (기본: 현재 연도) */
year?: number;
/** 기간 번호 (quarter: 1-4, half: 1-2) */
period?: number;
}
export function useVat(options: UseVatOptions = {}) {
const { periodType = 'quarter', year, period } = options;
const [data, setData] = useState<VatData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
// 쿼리 파라미터 구성
const params = new URLSearchParams();
params.append('period_type', periodType);
if (year) params.append('year', year.toString());
if (period) params.append('period', period.toString());
const queryString = params.toString();
const endpoint = `vat/summary?${queryString}`;
const apiData = await fetchApi<VatApiResponse>(endpoint);
const transformed = transformVatResponse(apiData);
setData(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
console.error('Vat API Error:', err);
} finally {
setLoading(false);
}
}, [periodType, year, period]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// ============================================
// 10. Entertainment Hook (접대비)
// ============================================
export interface UseEntertainmentOptions {
/** 기간 타입 (annual: 연간, quarterly: 분기) */
limitType?: 'annual' | 'quarterly';
/** 기업 유형 (large: 대기업, medium: 중견기업, small: 중소기업) */
companyType?: 'large' | 'medium' | 'small';
/** 연도 (기본: 현재 연도) */
year?: number;
/** 분기 번호 (1-4, 기본: 현재 분기) */
quarter?: number;
}
export function useEntertainment(options: UseEntertainmentOptions = {}) {
const { limitType = 'quarterly', companyType = 'medium', year, quarter } = options;
const [data, setData] = useState<EntertainmentData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
// 쿼리 파라미터 구성
const params = new URLSearchParams();
params.append('limit_type', limitType);
params.append('company_type', companyType);
if (year) params.append('year', year.toString());
if (quarter) params.append('quarter', quarter.toString());
const queryString = params.toString();
const endpoint = `entertainment/summary?${queryString}`;
const apiData = await fetchApi<EntertainmentApiResponse>(endpoint);
const transformed = transformEntertainmentResponse(apiData);
setData(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
console.error('Entertainment API Error:', err);
} finally {
setLoading(false);
}
}, [limitType, companyType, year, quarter]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// ============================================
// 11. Welfare Hook (복리후생비)
// ============================================
export interface UseWelfareOptions {
/** 기간 타입 (annual: 연간, quarterly: 분기) */
limitType?: 'annual' | 'quarterly';
/** 계산 방식 (fixed: 1인당 정액, ratio: 급여 대비 비율) */
calculationType?: 'fixed' | 'ratio';
/** 1인당 월 정액 (calculation_type=fixed일 때 사용, 기본: 200000) */
fixedAmountPerMonth?: number;
/** 급여 대비 비율 (calculation_type=ratio일 때 사용, 기본: 0.05) */
ratio?: number;
/** 연도 (기본: 현재 연도) */
year?: number;
/** 분기 번호 (1-4, 기본: 현재 분기) */
quarter?: number;
}
export function useWelfare(options: UseWelfareOptions = {}) {
const {
limitType = 'quarterly',
calculationType = 'fixed',
fixedAmountPerMonth,
ratio,
year,
quarter,
} = options;
const [data, setData] = useState<WelfareData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
// 쿼리 파라미터 구성
const params = new URLSearchParams();
params.append('limit_type', limitType);
params.append('calculation_type', calculationType);
if (fixedAmountPerMonth) params.append('fixed_amount_per_month', fixedAmountPerMonth.toString());
if (ratio) params.append('ratio', ratio.toString());
if (year) params.append('year', year.toString());
if (quarter) params.append('quarter', quarter.toString());
const queryString = params.toString();
const endpoint = `welfare/summary?${queryString}`;
const apiData = await fetchApi<WelfareApiResponse>(endpoint);
const transformed = transformWelfareResponse(apiData);
setData(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
console.error('Welfare API Error:', err);
} finally {
setLoading(false);
}
}, [limitType, calculationType, fixedAmountPerMonth, ratio, year, quarter]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// ============================================
// 12. WelfareDetail Hook (복리후생비 상세 - 모달용)
// ============================================
export interface UseWelfareDetailOptions {
/** 계산 방식 (fixed: 1인당 정액, ratio: 급여 대비 비율) */
calculationType?: 'fixed' | 'ratio';
/** 1인당 월 정액 (calculation_type=fixed일 때 사용, 기본: 200000) */
fixedAmountPerMonth?: number;
/** 급여 대비 비율 (calculation_type=ratio일 때 사용, 기본: 0.05) */
ratio?: number;
/** 연도 (기본: 현재 연도) */
year?: number;
/** 분기 번호 (1-4, 기본: 현재 분기) */
quarter?: number;
}
/**
* 복리후생비 상세 데이터 Hook (모달용)
* API에서 상세 데이터를 가져와 DetailModalConfig로 변환
*/
export function useWelfareDetail(options: UseWelfareDetailOptions = {}) {
const {
calculationType = 'fixed',
fixedAmountPerMonth,
ratio,
year,
quarter,
} = options;
const [modalConfig, setModalConfig] = useState<DetailModalConfig | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
// 쿼리 파라미터 구성
const params = new URLSearchParams();
params.append('calculation_type', calculationType);
if (fixedAmountPerMonth) params.append('fixed_amount_per_month', fixedAmountPerMonth.toString());
if (ratio) params.append('ratio', ratio.toString());
if (year) params.append('year', year.toString());
if (quarter) params.append('quarter', quarter.toString());
const queryString = params.toString();
const endpoint = `welfare/detail?${queryString}`;
const apiData = await fetchApi<WelfareDetailApiResponse>(endpoint);
const transformed = transformWelfareDetailResponse(apiData);
setModalConfig(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
console.error('WelfareDetail API Error:', err);
} finally {
setLoading(false);
}
}, [calculationType, fixedAmountPerMonth, ratio, year, quarter]);
return { modalConfig, loading, error, refetch: fetchData };
}
// ============================================
// 13. PurchaseDetail Hook (매입 상세 - me1 모달용)
// ============================================
/**
* 매입 상세 데이터 Hook (me1 모달용)
* API에서 상세 데이터를 가져와 DetailModalConfig로 변환
*/
export function usePurchaseDetail() {
const [modalConfig, setModalConfig] = useState<DetailModalConfig | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/v1/purchases/dashboard-detail');
if (!response.ok) {
throw new Error(`API 오류: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || '데이터 조회 실패');
}
const transformed = transformPurchaseDetailResponse(result.data);
setModalConfig(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
console.error('PurchaseDetail API Error:', err);
} finally {
setLoading(false);
}
}, []);
return { modalConfig, loading, error, refetch: fetchData };
}
// ============================================
// 14. CardDetail Hook (카드 상세 - me2 모달용)
// ============================================
/**
* 카드 상세 데이터 Hook (me2 모달용)
* API에서 상세 데이터를 가져와 DetailModalConfig로 변환
*/
export function useCardDetail() {
const [modalConfig, setModalConfig] = useState<DetailModalConfig | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/v1/card-transactions/dashboard');
if (!response.ok) {
throw new Error(`API 오류: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || '데이터 조회 실패');
}
const transformed = transformCardDetailResponse(result.data);
setModalConfig(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
console.error('CardDetail API Error:', err);
} finally {
setLoading(false);
}
}, []);
return { modalConfig, loading, error, refetch: fetchData };
}
// ============================================
// 15. BillDetail Hook (발행어음 상세 - me3 모달용)
// ============================================
/**
* 발행어음 상세 데이터 Hook (me3 모달용)
* API에서 상세 데이터를 가져와 DetailModalConfig로 변환
*/
export function useBillDetail() {
const [modalConfig, setModalConfig] = useState<DetailModalConfig | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/v1/bills/dashboard-detail');
if (!response.ok) {
throw new Error(`API 오류: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || '데이터 조회 실패');
}
const transformed = transformBillDetailResponse(result.data);
setModalConfig(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
console.error('BillDetail API Error:', err);
} finally {
setLoading(false);
}
}, []);
return { modalConfig, loading, error, refetch: fetchData };
}
// ============================================
// 16. ExpectedExpenseDetail Hook (지출예상 상세 - me4 모달용)
// ============================================
/**
* 지출예상 상세 데이터 Hook (me4 모달용)
* API에서 상세 데이터를 가져와 DetailModalConfig로 변환
*/
export function useExpectedExpenseDetail() {
const [modalConfig, setModalConfig] = useState<DetailModalConfig | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/v1/expected-expenses/dashboard-detail');
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);
setModalConfig(transformed);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
setError(errorMessage);
console.error('ExpectedExpenseDetail API Error:', err);
} finally {
setLoading(false);
}
}, []);
return { modalConfig, loading, error, refetch: fetchData };
}
// ============================================
// 17. MonthlyExpenseDetail Hook (당월 예상 지출 상세 - 통합 모달용)
// ============================================
export type MonthlyExpenseCardId = 'me1' | 'me2' | 'me3' | 'me4';
/**
* 당월 예상 지출 상세 데이터 Hook (통합 모달용)
* cardId에 따라 다른 API를 호출하고 DetailModalConfig로 변환
*
* @example
* const { modalConfig, loading, error, fetchData } = useMonthlyExpenseDetail();
* await fetchData('me1'); // 매입 상세 API 호출
*/
export function useMonthlyExpenseDetail() {
const [modalConfig, setModalConfig] = useState<DetailModalConfig | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async (cardId: MonthlyExpenseCardId) => {
try {
setLoading(true);
setError(null);
// 모든 카드가 expected-expenses API를 사용하여 데이터 일관성 보장
// transaction_type: me1=purchase, me2=card, me3=bill, me4=전체
let endpoint: string;
let transactionType: string | null = null;
switch (cardId) {
case 'me1':
transactionType = 'purchase';
break;
case 'me2':
transactionType = 'card';
break;
case 'me3':
transactionType = 'bill';
break;
case 'me4':
transactionType = null; // 전체 조회
break;
default:
throw new Error(`Unknown cardId: ${cardId}`);
}
// 단일 API 엔드포인트 사용 (transaction_type으로 필터링)
endpoint = transactionType
? `/api/v1/expected-expenses/dashboard-detail?transaction_type=${transactionType}`
: '/api/v1/expected-expenses/dashboard-detail';
const transformer = (data: unknown) =>
transformExpectedExpenseDetailResponse(data as ExpectedExpenseDashboardDetailApiResponse, cardId);
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 = transformer(result.data);
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 섹션 활성화 */
dailyReport?: boolean;
/** Receivable 섹션 활성화 */
receivable?: boolean;
/** DebtCollection 섹션 활성화 */
debtCollection?: boolean;
/** MonthlyExpense 섹션 활성화 */
monthlyExpense?: boolean;
/** CardManagement 섹션 활성화 */
cardManagement?: boolean;
/** CardManagement fallback 데이터 (가지급금, 법인세, 종합세 등) */
cardManagementFallback?: CardManagementData;
/** StatusBoard 섹션 활성화 */
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;
}
/**
* 통합 CEO Dashboard Hook
* 여러 섹션의 API를 병렬로 호출하여 성능 최적화
*/
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;
// 각 섹션별 상태
const [dailyReportData, setDailyReportData] = useState<DailyReportData | null>(null);
const [dailyReportLoading, setDailyReportLoading] = useState(enableDailyReport);
const [dailyReportError, setDailyReportError] = useState<string | null>(null);
const [receivableData, setReceivableData] = useState<ReceivableData | null>(null);
const [receivableLoading, setReceivableLoading] = useState(enableReceivable);
const [receivableError, setReceivableError] = useState<string | null>(null);
const [debtCollectionData, setDebtCollectionData] = useState<DebtCollectionData | null>(null);
const [debtCollectionLoading, setDebtCollectionLoading] = useState(enableDebtCollection);
const [debtCollectionError, setDebtCollectionError] = useState<string | null>(null);
const [monthlyExpenseData, setMonthlyExpenseData] = useState<MonthlyExpenseData | null>(null);
const [monthlyExpenseLoading, setMonthlyExpenseLoading] = useState(enableMonthlyExpense);
const [monthlyExpenseError, setMonthlyExpenseError] = useState<string | null>(null);
const [cardManagementData, setCardManagementData] = useState<CardManagementData | null>(null);
const [cardManagementLoading, setCardManagementLoading] = useState(enableCardManagement);
const [cardManagementError, setCardManagementError] = useState<string | null>(null);
const [statusBoardData, setStatusBoardData] = useState<TodayIssueItem[] | null>(null);
const [statusBoardLoading, setStatusBoardLoading] = useState(enableStatusBoard);
const [statusBoardError, setStatusBoardError] = useState<string | null>(null);
// 개별 fetch 함수들
const fetchDailyReport = useCallback(async () => {
if (!enableDailyReport) return;
try {
setDailyReportLoading(true);
setDailyReportError(null);
const apiData = await fetchApi<DailyReportApiResponse>('daily-report/summary');
setDailyReportData(transformDailyReportResponse(apiData));
} catch (err) {
setDailyReportError(err instanceof Error ? err.message : '데이터 로딩 실패');
} finally {
setDailyReportLoading(false);
}
}, [enableDailyReport]);
const fetchReceivable = useCallback(async () => {
if (!enableReceivable) return;
try {
setReceivableLoading(true);
setReceivableError(null);
const apiData = await fetchApi<ReceivablesApiResponse>('receivables/summary');
setReceivableData(transformReceivableResponse(apiData));
} catch (err) {
setReceivableError(err instanceof Error ? err.message : '데이터 로딩 실패');
} finally {
setReceivableLoading(false);
}
}, [enableReceivable]);
const fetchDebtCollection = useCallback(async () => {
if (!enableDebtCollection) return;
try {
setDebtCollectionLoading(true);
setDebtCollectionError(null);
const apiData = await fetchApi<BadDebtApiResponse>('bad-debts/summary');
setDebtCollectionData(transformDebtCollectionResponse(apiData));
} catch (err) {
setDebtCollectionError(err instanceof Error ? err.message : '데이터 로딩 실패');
} finally {
setDebtCollectionLoading(false);
}
}, [enableDebtCollection]);
const fetchMonthlyExpense = useCallback(async () => {
if (!enableMonthlyExpense) return;
try {
setMonthlyExpenseLoading(true);
setMonthlyExpenseError(null);
const apiData = await fetchApi<ExpectedExpenseApiResponse>('expected-expenses/summary');
setMonthlyExpenseData(transformMonthlyExpenseResponse(apiData));
} catch (err) {
setMonthlyExpenseError(err instanceof Error ? err.message : '데이터 로딩 실패');
} finally {
setMonthlyExpenseLoading(false);
}
}, [enableMonthlyExpense]);
const fetchCardManagement = useCallback(async () => {
if (!enableCardManagement) return;
try {
setCardManagementLoading(true);
setCardManagementError(null);
// 3개 API 병렬 호출: 카드거래, 가지급금, 세금 시뮬레이션
const [cardApiData, loanResponse, taxResponse] = await Promise.all([
fetchApi<CardTransactionApiResponse>('card-transactions/summary'),
fetchLoanDashboard(),
fetchTaxSimulation(),
]);
// LoanDashboard와 TaxSimulation은 ApiResponse wrapper가 있으므로 data 추출
const loanData = loanResponse.success ? loanResponse.data : null;
const taxData = taxResponse.success ? taxResponse.data : null;
setCardManagementData(
transformCardManagementResponse(cardApiData, loanData, taxData, cardManagementFallback)
);
} catch (err) {
setCardManagementError(err instanceof Error ? err.message : '데이터 로딩 실패');
} finally {
setCardManagementLoading(false);
}
}, [enableCardManagement, cardManagementFallback]);
const fetchStatusBoard = useCallback(async () => {
if (!enableStatusBoard) return;
try {
setStatusBoardLoading(true);
setStatusBoardError(null);
const apiData = await fetchApi<StatusBoardApiResponse>('status-board/summary');
setStatusBoardData(transformStatusBoardResponse(apiData));
} catch (err) {
setStatusBoardError(err instanceof Error ? err.message : '데이터 로딩 실패');
} finally {
setStatusBoardLoading(false);
}
}, [enableStatusBoard]);
// 전체 refetch
const refetchAll = useCallback(() => {
fetchDailyReport();
fetchReceivable();
fetchDebtCollection();
fetchMonthlyExpense();
fetchCardManagement();
fetchStatusBoard();
}, [fetchDailyReport, fetchReceivable, fetchDebtCollection, fetchMonthlyExpense, fetchCardManagement, fetchStatusBoard]);
// 초기 로드
useEffect(() => {
refetchAll();
}, [refetchAll]);
return {
dailyReport: {
data: dailyReportData,
loading: dailyReportLoading,
error: dailyReportError,
},
receivable: {
data: receivableData,
loading: receivableLoading,
error: receivableError,
},
debtCollection: {
data: debtCollectionData,
loading: debtCollectionLoading,
error: debtCollectionError,
},
monthlyExpense: {
data: monthlyExpenseData,
loading: monthlyExpenseLoading,
error: monthlyExpenseError,
},
cardManagement: {
data: cardManagementData,
loading: cardManagementLoading,
error: cardManagementError,
},
statusBoard: {
data: statusBoardData,
loading: statusBoardLoading,
error: statusBoardError,
},
refetchAll,
};
}