feat(WEB): CEO 대시보드 Phase 2 API 연동 완료

- StatusBoard(현황판) API Hook 및 타입 추가
- TodayIssue(오늘의 이슈) API Hook 및 타입 추가
- Calendar(캘린더) API Hook 및 타입 추가
- Vat(부가세) API Hook 및 타입 추가
- Entertainment(접대비) API Hook 및 타입 추가
- Welfare(복리후생비) API Hook 및 타입 추가
- CEODashboard.tsx에 모든 Phase 2 Hook 통합
- API 응답 → Frontend 타입 변환 transformer 추가
- WelfareCalculationType 'percentage' → 'ratio' 타입 일치 수정
This commit is contained in:
2026-01-21 10:38:09 +09:00
parent e6ef80f17f
commit fb1d7bf241
5 changed files with 774 additions and 9 deletions

View File

@@ -15,6 +15,12 @@ import type {
BadDebtApiResponse,
ExpectedExpenseApiResponse,
CardTransactionApiResponse,
StatusBoardApiResponse,
TodayIssueApiResponse,
CalendarApiResponse,
VatApiResponse,
EntertainmentApiResponse,
WelfareApiResponse,
} from '@/lib/api/dashboard/types';
import {
@@ -23,6 +29,12 @@ import {
transformDebtCollectionResponse,
transformMonthlyExpenseResponse,
transformCardManagementResponse,
transformStatusBoardResponse,
transformTodayIssueResponse,
transformCalendarResponse,
transformVatResponse,
transformEntertainmentResponse,
transformWelfareResponse,
} from '@/lib/api/dashboard/transformers';
import type {
@@ -31,6 +43,12 @@ import type {
DebtCollectionData,
MonthlyExpenseData,
CardManagementData,
TodayIssueItem,
TodayIssueListItem,
CalendarScheduleItem,
VatData,
EntertainmentData,
WelfareData,
} from '@/components/business/CEODashboard/types';
// ============================================
@@ -223,6 +241,318 @@ export function useCardManagement(fallbackData?: CardManagementData) {
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 };
}
// ============================================
// 통합 Dashboard Hook (선택적 사용)
// ============================================
@@ -240,6 +570,8 @@ export interface UseCEODashboardOptions {
cardManagement?: boolean;
/** CardManagement fallback 데이터 */
cardManagementFallback?: CardManagementData;
/** StatusBoard 섹션 활성화 */
statusBoard?: boolean;
}
export interface CEODashboardState {
@@ -268,6 +600,11 @@ export interface CEODashboardState {
loading: boolean;
error: string | null;
};
statusBoard: {
data: TodayIssueItem[] | null;
loading: boolean;
error: string | null;
};
refetchAll: () => void;
}
@@ -283,6 +620,7 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo
monthlyExpense: enableMonthlyExpense = true,
cardManagement: enableCardManagement = true,
cardManagementFallback,
statusBoard: enableStatusBoard = true,
} = options;
// 각 섹션별 상태
@@ -306,6 +644,10 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo
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;
@@ -377,6 +719,20 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo
}
}, [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();
@@ -384,7 +740,8 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo
fetchDebtCollection();
fetchMonthlyExpense();
fetchCardManagement();
}, [fetchDailyReport, fetchReceivable, fetchDebtCollection, fetchMonthlyExpense, fetchCardManagement]);
fetchStatusBoard();
}, [fetchDailyReport, fetchReceivable, fetchDebtCollection, fetchMonthlyExpense, fetchCardManagement, fetchStatusBoard]);
// 초기 로드
useEffect(() => {
@@ -417,6 +774,11 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo
loading: cardManagementLoading,
error: cardManagementError,
},
statusBoard: {
data: statusBoardData,
loading: statusBoardLoading,
error: statusBoardError,
},
refetchAll,
};
}