Files
sam-react-prod/src/hooks/useCEODashboard.ts
유병철 db84d6796b feat: [공통] Sidebar, 대시보드 훅, 유틸 개선
- Sidebar 레이아웃 개선
- useCEODashboard 최적화, useDashboardFetch 훅 신규
- amount, status-config 유틸 개선
- dashboard transformers 개선

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:19:53 +09:00

552 lines
16 KiB
TypeScript

/**
* 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, string | number | boolean | null | undefined>,
): 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<DailyReportApiResponse, DailyReportData>(
'daily-report/summary',
transformDailyReportResponse,
);
}
export function useReceivable() {
return useDashboardFetch<ReceivablesApiResponse, ReceivableData>(
'receivables/summary',
transformReceivableResponse,
);
}
export function useDebtCollection() {
return useDashboardFetch<BadDebtApiResponse, DebtCollectionData>(
'bad-debts/summary',
transformDebtCollectionResponse,
);
}
export function useMonthlyExpense() {
return useDashboardFetch<ExpectedExpenseApiResponse, MonthlyExpenseData>(
'expected-expenses/summary',
transformMonthlyExpenseResponse,
);
}
// ============================================
// 5. CardManagement Hook (커스텀: 3개 API 병렬 호출)
// ============================================
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);
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<StatusBoardApiResponse, TodayIssueItem[]>(
'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<TodayIssueApiResponse, TodayIssueData>(
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<TodayIssueApiResponse, TodayIssueData>(
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<CalendarApiResponse, CalendarData>(
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<VatApiResponse, VatData>(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<EntertainmentApiResponse, EntertainmentData>(
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<WelfareApiResponse, WelfareData>(
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<WelfareDetailApiResponse, DetailModalConfig>(
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<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);
const transactionTypeMap: Record<MonthlyExpenseCardId, string | null> = {
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<DailyReportApiResponse, DailyReportData>(
enableDailyReport ? 'daily-report/summary' : null,
transformDailyReportResponse,
{ initialLoading: enableDailyReport },
);
const rv = useDashboardFetch<ReceivablesApiResponse, ReceivableData>(
enableReceivable ? 'receivables/summary' : null,
transformReceivableResponse,
{ initialLoading: enableReceivable },
);
const dc = useDashboardFetch<BadDebtApiResponse, DebtCollectionData>(
enableDebtCollection ? 'bad-debts/summary' : null,
transformDebtCollectionResponse,
{ initialLoading: enableDebtCollection },
);
const me = useDashboardFetch<ExpectedExpenseApiResponse, MonthlyExpenseData>(
enableMonthlyExpense ? 'expected-expenses/summary' : null,
transformMonthlyExpenseResponse,
{ initialLoading: enableMonthlyExpense },
);
const sb = useDashboardFetch<StatusBoardApiResponse, TodayIssueItem[]>(
enableStatusBoard ? 'status-board/summary' : null,
transformStatusBoardResponse,
{ initialLoading: enableStatusBoard },
);
// CardManagement: 커스텀 (3개 API 병렬)
const [cmData, setCmData] = useState<CardManagementData | null>(null);
const [cmLoading, setCmLoading] = useState(enableCardManagement);
const [cmError, setCmError] = useState<string | null>(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,
};
}