- Sidebar 레이아웃 개선 - useCEODashboard 최적화, useDashboardFetch 훅 신규 - amount, status-config 유틸 개선 - dashboard transformers 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
552 lines
16 KiB
TypeScript
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,
|
|
};
|
|
} |