카드/가지급금 관리 섹션의 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)
1132 lines
36 KiB
TypeScript
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,
|
|
};
|
|
} |