feat: CEO 대시보드 Phase 1 API 연동 (5개 섹션)
- DailyReport, Receivable, DebtCollection, MonthlyExpense, CardManagement API 연동 - useCEODashboard Hook 추가 (병렬 API 호출) - API → Frontend 타입 변환 함수 및 CheckPoint 생성 로직 구현 - API 실패 시 mockData fallback 패턴 적용
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { LayoutDashboard, Settings } from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
@@ -25,6 +25,7 @@ import { DEFAULT_DASHBOARD_SETTINGS } from './types';
|
||||
import { ScheduleDetailModal, DetailModal } from './modals';
|
||||
import { DashboardSettingsDialog } from './dialogs/DashboardSettingsDialog';
|
||||
import { mockData } from './mockData';
|
||||
import { useCEODashboard } from '@/hooks/useCEODashboard';
|
||||
import {
|
||||
getMonthlyExpenseModalConfig,
|
||||
getCardManagementModalConfig,
|
||||
@@ -35,8 +36,35 @@ import {
|
||||
|
||||
export function CEODashboard() {
|
||||
const router = useRouter();
|
||||
const [isLoading] = useState(false);
|
||||
const [data] = useState<CEODashboardData>(mockData);
|
||||
|
||||
// API 데이터 Hook (Phase 1 섹션들)
|
||||
const apiData = useCEODashboard({
|
||||
cardManagementFallback: mockData.cardManagement,
|
||||
});
|
||||
|
||||
// 전체 로딩 상태 (모든 API 호출 중일 때)
|
||||
const isLoading = useMemo(() => {
|
||||
return (
|
||||
apiData.dailyReport.loading &&
|
||||
apiData.receivable.loading &&
|
||||
apiData.debtCollection.loading &&
|
||||
apiData.monthlyExpense.loading &&
|
||||
apiData.cardManagement.loading
|
||||
);
|
||||
}, [apiData]);
|
||||
|
||||
// API 데이터와 mockData를 병합 (API 우선, 실패 시 fallback)
|
||||
const data = useMemo<CEODashboardData>(() => ({
|
||||
...mockData,
|
||||
// Phase 1 섹션들: API 데이터 우선, 실패 시 mockData fallback
|
||||
dailyReport: apiData.dailyReport.data ?? mockData.dailyReport,
|
||||
receivable: apiData.receivable.data ?? mockData.receivable,
|
||||
debtCollection: apiData.debtCollection.data ?? mockData.debtCollection,
|
||||
monthlyExpense: apiData.monthlyExpense.data ?? mockData.monthlyExpense,
|
||||
cardManagement: apiData.cardManagement.data ?? mockData.cardManagement,
|
||||
// Phase 2 섹션들: 아직 mockData 사용
|
||||
// todayIssue, todayIssueList, entertainment, welfare, vat, calendarSchedules
|
||||
}), [apiData, mockData]);
|
||||
|
||||
// 일정 상세 모달 상태
|
||||
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
|
||||
|
||||
422
src/hooks/useCEODashboard.ts
Normal file
422
src/hooks/useCEODashboard.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* CEO Dashboard API 연동 Hook
|
||||
*
|
||||
* 각 섹션별 API 호출 및 데이터 변환 담당
|
||||
* 참조 패턴: useClientList.ts
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
|
||||
import type {
|
||||
DailyReportApiResponse,
|
||||
ReceivablesApiResponse,
|
||||
BadDebtApiResponse,
|
||||
ExpectedExpenseApiResponse,
|
||||
CardTransactionApiResponse,
|
||||
} from '@/lib/api/dashboard/types';
|
||||
|
||||
import {
|
||||
transformDailyReportResponse,
|
||||
transformReceivableResponse,
|
||||
transformDebtCollectionResponse,
|
||||
transformMonthlyExpenseResponse,
|
||||
transformCardManagementResponse,
|
||||
} from '@/lib/api/dashboard/transformers';
|
||||
|
||||
import type {
|
||||
DailyReportData,
|
||||
ReceivableData,
|
||||
DebtCollectionData,
|
||||
MonthlyExpenseData,
|
||||
CardManagementData,
|
||||
} 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);
|
||||
|
||||
const apiData = await fetchApi<CardTransactionApiResponse>('card-transactions/summary');
|
||||
const transformed = transformCardManagementResponse(apiData, 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 };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 통합 Dashboard Hook (선택적 사용)
|
||||
// ============================================
|
||||
|
||||
export interface UseCEODashboardOptions {
|
||||
/** DailyReport 섹션 활성화 */
|
||||
dailyReport?: boolean;
|
||||
/** Receivable 섹션 활성화 */
|
||||
receivable?: boolean;
|
||||
/** DebtCollection 섹션 활성화 */
|
||||
debtCollection?: boolean;
|
||||
/** MonthlyExpense 섹션 활성화 */
|
||||
monthlyExpense?: boolean;
|
||||
/** CardManagement 섹션 활성화 */
|
||||
cardManagement?: boolean;
|
||||
/** CardManagement fallback 데이터 */
|
||||
cardManagementFallback?: CardManagementData;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
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,
|
||||
} = 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);
|
||||
|
||||
// 개별 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);
|
||||
const apiData = await fetchApi<CardTransactionApiResponse>('card-transactions/summary');
|
||||
setCardManagementData(transformCardManagementResponse(apiData, cardManagementFallback));
|
||||
} catch (err) {
|
||||
setCardManagementError(err instanceof Error ? err.message : '데이터 로딩 실패');
|
||||
} finally {
|
||||
setCardManagementLoading(false);
|
||||
}
|
||||
}, [enableCardManagement, cardManagementFallback]);
|
||||
|
||||
// 전체 refetch
|
||||
const refetchAll = useCallback(() => {
|
||||
fetchDailyReport();
|
||||
fetchReceivable();
|
||||
fetchDebtCollection();
|
||||
fetchMonthlyExpense();
|
||||
fetchCardManagement();
|
||||
}, [fetchDailyReport, fetchReceivable, fetchDebtCollection, fetchMonthlyExpense, fetchCardManagement]);
|
||||
|
||||
// 초기 로드
|
||||
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,
|
||||
},
|
||||
refetchAll,
|
||||
};
|
||||
}
|
||||
6
src/lib/api/dashboard/index.ts
Normal file
6
src/lib/api/dashboard/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* CEO Dashboard API 모듈 export
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './transformers';
|
||||
431
src/lib/api/dashboard/transformers.ts
Normal file
431
src/lib/api/dashboard/transformers.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
/**
|
||||
* CEO Dashboard API 응답 → Frontend 타입 변환 함수
|
||||
*
|
||||
* 참조: docs/plans/AI_리포트_키워드_색상체계_가이드_v1.4.md
|
||||
*/
|
||||
|
||||
import type {
|
||||
DailyReportApiResponse,
|
||||
ReceivablesApiResponse,
|
||||
BadDebtApiResponse,
|
||||
ExpectedExpenseApiResponse,
|
||||
CardTransactionApiResponse,
|
||||
} from './types';
|
||||
|
||||
import type {
|
||||
DailyReportData,
|
||||
ReceivableData,
|
||||
DebtCollectionData,
|
||||
MonthlyExpenseData,
|
||||
CardManagementData,
|
||||
CheckPoint,
|
||||
CheckPointType,
|
||||
} from '@/components/business/CEODashboard/types';
|
||||
|
||||
// ============================================
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 금액 포맷팅
|
||||
* @example formatAmount(3050000000) → "30.5억원"
|
||||
*/
|
||||
function formatAmount(amount: number): string {
|
||||
const absAmount = Math.abs(amount);
|
||||
if (absAmount >= 100000000) {
|
||||
return `${(amount / 100000000).toFixed(1)}억원`;
|
||||
} else if (absAmount >= 10000) {
|
||||
return `${Math.round(amount / 10000).toLocaleString()}만원`;
|
||||
}
|
||||
return `${amount.toLocaleString()}원`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 포맷팅 (API → 한국어 형식)
|
||||
* @example formatDate("2026-01-20", "월요일") → "2026년 1월 20일 월요일"
|
||||
*/
|
||||
function formatDate(dateStr: string, dayOfWeek: string): string {
|
||||
const date = new Date(dateStr);
|
||||
return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일 ${dayOfWeek}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 퍼센트 변화율 계산
|
||||
*/
|
||||
function calculateChangeRate(current: number, previous: number): number {
|
||||
if (previous === 0) return current > 0 ? 100 : 0;
|
||||
return ((current - previous) / previous) * 100;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 1. DailyReport 변환
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 일일 일보 CheckPoints 생성
|
||||
* 참조: AI 리포트 색상 체계 가이드 - 섹션 2
|
||||
*/
|
||||
function generateDailyReportCheckPoints(api: DailyReportApiResponse): CheckPoint[] {
|
||||
const checkPoints: CheckPoint[] = [];
|
||||
|
||||
// 출금 정보
|
||||
const withdrawal = api.krw_totals.expense;
|
||||
if (withdrawal > 0) {
|
||||
checkPoints.push({
|
||||
id: 'dr-withdrawal',
|
||||
type: 'info' as CheckPointType,
|
||||
message: `어제 ${formatAmount(withdrawal)} 출금했습니다.`,
|
||||
highlights: [
|
||||
{ text: formatAmount(withdrawal), color: 'red' as const },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// 입금 정보
|
||||
const deposit = api.krw_totals.income;
|
||||
if (deposit > 0) {
|
||||
checkPoints.push({
|
||||
id: 'dr-deposit',
|
||||
type: 'success' as CheckPointType,
|
||||
message: `어제 ${formatAmount(deposit)}이 입금되었습니다.`,
|
||||
highlights: [
|
||||
{ text: formatAmount(deposit), color: 'green' as const },
|
||||
{ text: '입금', color: 'green' as const },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// 현금성 자산 현황
|
||||
const cashAsset = api.cash_asset_total;
|
||||
checkPoints.push({
|
||||
id: 'dr-cash-asset',
|
||||
type: 'info' as CheckPointType,
|
||||
message: `총 현금성 자산이 ${formatAmount(cashAsset)}입니다.`,
|
||||
highlights: [
|
||||
{ text: formatAmount(cashAsset), color: 'blue' as const },
|
||||
],
|
||||
});
|
||||
|
||||
return checkPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* DailyReport API 응답 → Frontend 타입 변환
|
||||
*/
|
||||
export function transformDailyReportResponse(api: DailyReportApiResponse): DailyReportData {
|
||||
return {
|
||||
date: formatDate(api.date, api.day_of_week),
|
||||
cards: [
|
||||
{
|
||||
id: 'dr1',
|
||||
label: '현금성 자산 합계',
|
||||
amount: api.cash_asset_total,
|
||||
},
|
||||
{
|
||||
id: 'dr2',
|
||||
label: '외국환(USD) 합계',
|
||||
amount: api.foreign_currency_total,
|
||||
currency: 'USD',
|
||||
},
|
||||
{
|
||||
id: 'dr3',
|
||||
label: '입금 합계',
|
||||
amount: api.krw_totals.income,
|
||||
},
|
||||
{
|
||||
id: 'dr4',
|
||||
label: '출금 합계',
|
||||
amount: api.krw_totals.expense,
|
||||
},
|
||||
],
|
||||
checkPoints: generateDailyReportCheckPoints(api),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 2. Receivable 변환
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 미수금 현황 CheckPoints 생성
|
||||
*/
|
||||
function generateReceivableCheckPoints(api: ReceivablesApiResponse): CheckPoint[] {
|
||||
const checkPoints: CheckPoint[] = [];
|
||||
|
||||
// 연체 거래처 경고
|
||||
if (api.overdue_vendor_count > 0) {
|
||||
checkPoints.push({
|
||||
id: 'rv-overdue',
|
||||
type: 'warning' as CheckPointType,
|
||||
message: `연체 거래처 ${api.overdue_vendor_count}곳. 회수 조치가 필요합니다.`,
|
||||
highlights: [
|
||||
{ text: `연체 거래처 ${api.overdue_vendor_count}곳`, color: 'red' as const },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// 미수금 현황
|
||||
if (api.total_receivables > 0) {
|
||||
checkPoints.push({
|
||||
id: 'rv-total',
|
||||
type: 'info' as CheckPointType,
|
||||
message: `총 미수금 ${formatAmount(api.total_receivables)}입니다.`,
|
||||
highlights: [
|
||||
{ text: formatAmount(api.total_receivables), color: 'blue' as const },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return checkPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receivables API 응답 → Frontend 타입 변환
|
||||
*/
|
||||
export function transformReceivableResponse(api: ReceivablesApiResponse): ReceivableData {
|
||||
// 누적 미수금 = 이월 + 매출 - 입금
|
||||
const cumulativeReceivable = api.total_carry_forward + api.total_sales - api.total_deposits;
|
||||
|
||||
return {
|
||||
cards: [
|
||||
{
|
||||
id: 'rv1',
|
||||
label: '누적 미수금',
|
||||
amount: cumulativeReceivable,
|
||||
subItems: [
|
||||
{ label: '이월', value: api.total_carry_forward },
|
||||
{ label: '매출', value: api.total_sales },
|
||||
{ label: '입금', value: api.total_deposits },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'rv2',
|
||||
label: '당월 미수금',
|
||||
amount: api.total_receivables,
|
||||
subItems: [
|
||||
{ label: '매출', value: api.total_sales },
|
||||
{ label: '입금', value: api.total_deposits },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'rv3',
|
||||
label: '거래처 현황',
|
||||
amount: api.vendor_count,
|
||||
unit: '곳',
|
||||
subLabel: `연체 ${api.overdue_vendor_count}곳`,
|
||||
},
|
||||
],
|
||||
checkPoints: generateReceivableCheckPoints(api),
|
||||
detailButtonLabel: '미수금 상세',
|
||||
detailButtonPath: '/accounting/receivables-status',
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 3. DebtCollection 변환
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 채권추심 CheckPoints 생성
|
||||
*/
|
||||
function generateDebtCollectionCheckPoints(api: BadDebtApiResponse): CheckPoint[] {
|
||||
const checkPoints: CheckPoint[] = [];
|
||||
|
||||
// 법적조치 진행 중
|
||||
if (api.legal_action_amount > 0) {
|
||||
checkPoints.push({
|
||||
id: 'dc-legal',
|
||||
type: 'warning' as CheckPointType,
|
||||
message: `법적조치 진행 중 ${formatAmount(api.legal_action_amount)}입니다.`,
|
||||
highlights: [
|
||||
{ text: formatAmount(api.legal_action_amount), color: 'red' as const },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// 회수 완료
|
||||
if (api.recovered_amount > 0) {
|
||||
checkPoints.push({
|
||||
id: 'dc-recovered',
|
||||
type: 'success' as CheckPointType,
|
||||
message: `총 ${formatAmount(api.recovered_amount)}을 회수 완료했습니다.`,
|
||||
highlights: [
|
||||
{ text: formatAmount(api.recovered_amount), color: 'green' as const },
|
||||
{ text: '회수 완료', color: 'green' as const },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return checkPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* BadDebt API 응답 → Frontend 타입 변환
|
||||
*/
|
||||
export function transformDebtCollectionResponse(api: BadDebtApiResponse): DebtCollectionData {
|
||||
return {
|
||||
cards: [
|
||||
{
|
||||
id: 'dc1',
|
||||
label: '누적 악성채권',
|
||||
amount: api.total_amount,
|
||||
},
|
||||
{
|
||||
id: 'dc2',
|
||||
label: '추심중',
|
||||
amount: api.collecting_amount,
|
||||
},
|
||||
{
|
||||
id: 'dc3',
|
||||
label: '법적조치',
|
||||
amount: api.legal_action_amount,
|
||||
},
|
||||
{
|
||||
id: 'dc4',
|
||||
label: '회수완료',
|
||||
amount: api.recovered_amount,
|
||||
},
|
||||
],
|
||||
checkPoints: generateDebtCollectionCheckPoints(api),
|
||||
detailButtonPath: '/accounting/bad-debt-collection',
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 4. MonthlyExpense 변환
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 당월 예상 지출 CheckPoints 생성
|
||||
*/
|
||||
function generateMonthlyExpenseCheckPoints(api: ExpectedExpenseApiResponse): CheckPoint[] {
|
||||
const checkPoints: CheckPoint[] = [];
|
||||
|
||||
// 총 예상 지출
|
||||
checkPoints.push({
|
||||
id: 'me-total',
|
||||
type: 'info' as CheckPointType,
|
||||
message: `이번 달 예상 지출은 ${formatAmount(api.total_amount)}입니다.`,
|
||||
highlights: [
|
||||
{ text: formatAmount(api.total_amount), color: 'blue' as const },
|
||||
],
|
||||
});
|
||||
|
||||
return checkPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* ExpectedExpense API 응답 → Frontend 타입 변환
|
||||
* 주의: 실제 API는 상세 분류(매입/카드/어음 등)를 제공하지 않음
|
||||
* by_transaction_type에서 추출하거나 기본값 사용
|
||||
*/
|
||||
export function transformMonthlyExpenseResponse(api: ExpectedExpenseApiResponse): MonthlyExpenseData {
|
||||
// transaction_type별 금액 추출
|
||||
const purchaseTotal = api.by_transaction_type['purchase']?.total ?? 0;
|
||||
const cardTotal = api.by_transaction_type['card']?.total ?? 0;
|
||||
const billTotal = api.by_transaction_type['bill']?.total ?? 0;
|
||||
|
||||
return {
|
||||
cards: [
|
||||
{
|
||||
id: 'me1',
|
||||
label: '매입',
|
||||
amount: purchaseTotal,
|
||||
},
|
||||
{
|
||||
id: 'me2',
|
||||
label: '카드',
|
||||
amount: cardTotal,
|
||||
},
|
||||
{
|
||||
id: 'me3',
|
||||
label: '발행어음',
|
||||
amount: billTotal,
|
||||
},
|
||||
{
|
||||
id: 'me4',
|
||||
label: '총 예상 지출 합계',
|
||||
amount: api.total_amount,
|
||||
},
|
||||
],
|
||||
checkPoints: generateMonthlyExpenseCheckPoints(api),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 5. CardManagement 변환
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 카드/가지급금 CheckPoints 생성
|
||||
*/
|
||||
function generateCardManagementCheckPoints(api: CardTransactionApiResponse): CheckPoint[] {
|
||||
const checkPoints: CheckPoint[] = [];
|
||||
|
||||
// 전월 대비 변화
|
||||
const changeRate = calculateChangeRate(api.current_month_total, api.previous_month_total);
|
||||
if (Math.abs(changeRate) > 10) {
|
||||
const type: CheckPointType = changeRate > 0 ? 'warning' : 'info';
|
||||
checkPoints.push({
|
||||
id: 'cm-change',
|
||||
type,
|
||||
message: `당월 카드 사용액이 전월 대비 ${changeRate > 0 ? '+' : ''}${changeRate.toFixed(1)}% 변동했습니다.`,
|
||||
highlights: [
|
||||
{ text: `${changeRate > 0 ? '+' : ''}${changeRate.toFixed(1)}%`, color: changeRate > 0 ? 'red' as const : 'green' as const },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// 당월 사용액
|
||||
checkPoints.push({
|
||||
id: 'cm-current',
|
||||
type: 'info' as CheckPointType,
|
||||
message: `당월 카드 사용 총 ${formatAmount(api.current_month_total)}입니다.`,
|
||||
highlights: [
|
||||
{ text: formatAmount(api.current_month_total), color: 'blue' as const },
|
||||
],
|
||||
});
|
||||
|
||||
return checkPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* CardTransaction API 응답 → Frontend 타입 변환
|
||||
* 주의: 가지급금, 법인세 예상 가중 등은 별도 API 필요 (현재 목업 유지)
|
||||
*/
|
||||
export function transformCardManagementResponse(
|
||||
api: CardTransactionApiResponse,
|
||||
fallbackData?: CardManagementData
|
||||
): CardManagementData {
|
||||
const changeRate = calculateChangeRate(api.current_month_total, api.previous_month_total);
|
||||
|
||||
return {
|
||||
// 가지급금 관련 경고는 API 데이터가 없으므로 fallback 사용
|
||||
warningBanner: fallbackData?.warningBanner,
|
||||
cards: [
|
||||
{
|
||||
id: 'cm1',
|
||||
label: '카드',
|
||||
amount: api.current_month_total,
|
||||
previousLabel: `전월 대비 ${changeRate > 0 ? '+' : ''}${changeRate.toFixed(1)}%`,
|
||||
},
|
||||
// 아래 항목들은 API에서 제공하지 않으므로 fallback 사용
|
||||
fallbackData?.cards[1] ?? {
|
||||
id: 'cm2',
|
||||
label: '가지급금',
|
||||
amount: 0,
|
||||
},
|
||||
fallbackData?.cards[2] ?? {
|
||||
id: 'cm3',
|
||||
label: '법인세 예상 가중',
|
||||
amount: 0,
|
||||
},
|
||||
fallbackData?.cards[3] ?? {
|
||||
id: 'cm4',
|
||||
label: '대표자 종합세 예상 가중',
|
||||
amount: 0,
|
||||
},
|
||||
],
|
||||
checkPoints: generateCardManagementCheckPoints(api),
|
||||
};
|
||||
}
|
||||
120
src/lib/api/dashboard/types.ts
Normal file
120
src/lib/api/dashboard/types.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* CEO Dashboard API 응답 타입 정의
|
||||
*
|
||||
* Laravel API 응답과 Frontend 타입 간의 매핑을 위한 타입들
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// 1. DailyReport API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** KRW/USD 통화별 합계 */
|
||||
export interface CurrencyTotals {
|
||||
carryover: number; // 전월 이월
|
||||
income: number; // 수입 (입금)
|
||||
expense: number; // 지출 (출금)
|
||||
balance: number; // 잔액
|
||||
}
|
||||
|
||||
/** GET /api/proxy/daily-report/summary 응답 */
|
||||
export interface DailyReportApiResponse {
|
||||
date: string; // "2026-01-20"
|
||||
day_of_week: string; // "월요일"
|
||||
note_receivable_total: number; // 수취채권 합계
|
||||
foreign_currency_total: number; // 외화 합계 (USD)
|
||||
cash_asset_total: number; // 현금성 자산 합계
|
||||
krw_totals: CurrencyTotals; // 원화 합계
|
||||
usd_totals: CurrencyTotals; // 달러 합계
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 2. Receivables API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** GET /api/proxy/receivables/summary 응답 */
|
||||
export interface ReceivablesApiResponse {
|
||||
total_carry_forward: number; // 이월 미수금
|
||||
total_sales: number; // 당월 매출
|
||||
total_deposits: number; // 당월 입금
|
||||
total_bills: number; // 당월 어음
|
||||
total_receivables: number; // 미수금 잔액
|
||||
vendor_count: number; // 거래처 수
|
||||
overdue_vendor_count: number; // 연체 거래처 수
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 3. BadDebt (채권추심) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** GET /api/proxy/bad-debts/summary 응답 */
|
||||
export interface BadDebtApiResponse {
|
||||
total_amount: number; // 총 악성채권
|
||||
collecting_amount: number; // 추심중
|
||||
legal_action_amount: number; // 법적조치
|
||||
recovered_amount: number; // 회수완료
|
||||
bad_debt_amount: number; // 대손처리
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 4. ExpectedExpense (당월 예상 지출) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 상태/유형별 집계 아이템 */
|
||||
export interface ExpenseSummaryItem {
|
||||
total: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
/** GET /api/proxy/expected-expenses/summary 응답 */
|
||||
export interface ExpectedExpenseApiResponse {
|
||||
total_amount: number;
|
||||
total_count: number;
|
||||
by_payment_status: Record<string, ExpenseSummaryItem>;
|
||||
by_transaction_type: Record<string, ExpenseSummaryItem>;
|
||||
by_month: Record<string, ExpenseSummaryItem>;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 5. CardTransaction (카드/가지급금) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** GET /api/proxy/card-transactions/summary 응답 */
|
||||
export interface CardTransactionApiResponse {
|
||||
previous_month_total: number; // 전월 카드 사용액
|
||||
current_month_total: number; // 당월 카드 사용액
|
||||
total_count: number; // 총 건수
|
||||
total_amount: number; // 총 금액
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 공통 API 응답 Wrapper
|
||||
// ============================================
|
||||
|
||||
/** 표준 API 응답 래퍼 */
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Dashboard Hook 상태 타입
|
||||
// ============================================
|
||||
|
||||
/** 섹션별 로딩 상태 */
|
||||
export interface DashboardLoadingState {
|
||||
dailyReport: boolean;
|
||||
receivable: boolean;
|
||||
debtCollection: boolean;
|
||||
monthlyExpense: boolean;
|
||||
cardManagement: boolean;
|
||||
}
|
||||
|
||||
/** 섹션별 에러 상태 */
|
||||
export interface DashboardErrorState {
|
||||
dailyReport: string | null;
|
||||
receivable: string | null;
|
||||
debtCollection: string | null;
|
||||
monthlyExpense: string | null;
|
||||
cardManagement: string | null;
|
||||
}
|
||||
Reference in New Issue
Block a user