feat: CEO 대시보드 리팩토링 및 회계 관리 개선
- CEO 대시보드: 컴포넌트 분리(DashboardSettingsSections, DetailModalSections), 모달/섹션 개선, useCEODashboard 최적화 - 회계: 부실채권/매출/매입/일일보고 UI 및 타입 개선 - 공통: Sidebar, useDashboardFetch 훅 추가, amount/status-config 유틸 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
156
src/hooks/useDashboardFetch.ts
Normal file
156
src/hooks/useDashboardFetch.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* CEO Dashboard API 호출을 위한 제네릭 훅
|
||||
*
|
||||
* @param endpoint - API 엔드포인트 (예: 'daily-report/summary')
|
||||
* @param transformer - API 응답을 프론트엔드 데이터로 변환하는 함수
|
||||
* @param options - 추가 옵션
|
||||
*
|
||||
* @example
|
||||
* // 자동 fetch (마운트 시 즉시 호출)
|
||||
* const { data, loading, error, refetch } = useDashboardFetch(
|
||||
* 'daily-report/summary',
|
||||
* transformDailyReportResponse,
|
||||
* );
|
||||
*
|
||||
* // 수동 fetch (lazy: true → 마운트 시 호출하지 않음)
|
||||
* const { data, loading, error, refetch } = useDashboardFetch(
|
||||
* 'welfare/detail',
|
||||
* transformWelfareDetailResponse,
|
||||
* { lazy: true },
|
||||
* );
|
||||
* // 필요할 때 수동 호출
|
||||
* await refetch();
|
||||
*/
|
||||
export function useDashboardFetch<TApi, TResult>(
|
||||
endpoint: string | null,
|
||||
transformer: (data: TApi) => TResult,
|
||||
options?: {
|
||||
/** true이면 마운트 시 자동 호출하지 않음 */
|
||||
lazy?: boolean;
|
||||
/** 초기 로딩 상태 (기본: !lazy) */
|
||||
initialLoading?: boolean;
|
||||
},
|
||||
) {
|
||||
const lazy = options?.lazy ?? false;
|
||||
const [data, setData] = useState<TResult | null>(null);
|
||||
const [loading, setLoading] = useState(options?.initialLoading ?? !lazy);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!endpoint) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
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 || '데이터 조회 실패');
|
||||
}
|
||||
|
||||
const transformed = transformer(result.data);
|
||||
setData(transformed);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
|
||||
setError(errorMessage);
|
||||
console.error(`Dashboard API Error [${endpoint}]:`, err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [endpoint, transformer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!lazy && endpoint) {
|
||||
fetchData();
|
||||
}
|
||||
}, [lazy, endpoint, fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
|
||||
/**
|
||||
* 여러 API를 병렬 호출하는 제네릭 훅
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error, refetch } = useDashboardMultiFetch(
|
||||
* [
|
||||
* { endpoint: 'card-transactions/summary' },
|
||||
* { endpoint: 'loans/dashboard', fetchFn: fetchLoanDashboard },
|
||||
* ],
|
||||
* ([cardData, loanData]) => transformCardManagementResponse(cardData, loanData),
|
||||
* );
|
||||
*/
|
||||
export function useDashboardMultiFetch<TResult>(
|
||||
sources: Array<{
|
||||
endpoint: string;
|
||||
/** 커스텀 fetch 함수 (기본: fetchApi 패턴) */
|
||||
fetchFn?: () => Promise<unknown>;
|
||||
}>,
|
||||
transformer: (results: unknown[]) => TResult,
|
||||
options?: {
|
||||
lazy?: boolean;
|
||||
initialLoading?: boolean;
|
||||
},
|
||||
) {
|
||||
const lazy = options?.lazy ?? false;
|
||||
const [data, setData] = useState<TResult | null>(null);
|
||||
const [loading, setLoading] = useState(options?.initialLoading ?? !lazy);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// sources를 JSON으로 비교하여 안정적인 의존성 확보
|
||||
const sourcesKey = JSON.stringify(sources.map((s) => s.endpoint));
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const results = await Promise.all(
|
||||
sources.map(async (source) => {
|
||||
if (source.fetchFn) {
|
||||
const result = await source.fetchFn();
|
||||
// fetchFn이 { success, data } 형태를 반환할 수 있음
|
||||
if (result && typeof result === 'object' && 'success' in result) {
|
||||
const r = result as { success: boolean; data: unknown };
|
||||
return r.success ? r.data : null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/proxy/${source.endpoint}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API 오류: ${response.status}`);
|
||||
}
|
||||
const json = await response.json();
|
||||
if (!json.success) {
|
||||
throw new Error(json.message || '데이터 조회 실패');
|
||||
}
|
||||
return json.data;
|
||||
}),
|
||||
);
|
||||
|
||||
const transformed = transformer(results);
|
||||
setData(transformed);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '데이터 로딩 실패';
|
||||
setError(errorMessage);
|
||||
console.error('Dashboard MultiFetch Error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sourcesKey, transformer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!lazy) {
|
||||
fetchData();
|
||||
}
|
||||
}, [lazy, fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
Reference in New Issue
Block a user