From 0e963c0f116eec116b600feb58c00f2f8aae3a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Tue, 20 Jan 2026 18:51:05 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20CEO=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20Phase=201=20API=20=EC=97=B0=EB=8F=99=20(5=EA=B0=9C?= =?UTF-8?q?=20=EC=84=B9=EC=85=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DailyReport, Receivable, DebtCollection, MonthlyExpense, CardManagement API 연동 - useCEODashboard Hook 추가 (병렬 API 호출) - API → Frontend 타입 변환 함수 및 CheckPoint 생성 로직 구현 - API 실패 시 mockData fallback 패턴 적용 --- .../business/CEODashboard/CEODashboard.tsx | 34 +- src/hooks/useCEODashboard.ts | 422 +++++++++++++++++ src/lib/api/dashboard/index.ts | 6 + src/lib/api/dashboard/transformers.ts | 431 ++++++++++++++++++ src/lib/api/dashboard/types.ts | 120 +++++ 5 files changed, 1010 insertions(+), 3 deletions(-) create mode 100644 src/hooks/useCEODashboard.ts create mode 100644 src/lib/api/dashboard/index.ts create mode 100644 src/lib/api/dashboard/transformers.ts create mode 100644 src/lib/api/dashboard/types.ts diff --git a/src/components/business/CEODashboard/CEODashboard.tsx b/src/components/business/CEODashboard/CEODashboard.tsx index 3336f6d4..6d15f5c6 100644 --- a/src/components/business/CEODashboard/CEODashboard.tsx +++ b/src/components/business/CEODashboard/CEODashboard.tsx @@ -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(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(() => ({ + ...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); diff --git a/src/hooks/useCEODashboard.ts b/src/hooks/useCEODashboard.ts new file mode 100644 index 00000000..38b06ff5 --- /dev/null +++ b/src/hooks/useCEODashboard.ts @@ -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(endpoint: string): Promise { + 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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + + const apiData = await fetchApi('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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + + const apiData = await fetchApi('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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + + const apiData = await fetchApi('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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + + const apiData = await fetchApi('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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + + const apiData = await fetchApi('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(null); + const [dailyReportLoading, setDailyReportLoading] = useState(enableDailyReport); + const [dailyReportError, setDailyReportError] = useState(null); + + const [receivableData, setReceivableData] = useState(null); + const [receivableLoading, setReceivableLoading] = useState(enableReceivable); + const [receivableError, setReceivableError] = useState(null); + + const [debtCollectionData, setDebtCollectionData] = useState(null); + const [debtCollectionLoading, setDebtCollectionLoading] = useState(enableDebtCollection); + const [debtCollectionError, setDebtCollectionError] = useState(null); + + const [monthlyExpenseData, setMonthlyExpenseData] = useState(null); + const [monthlyExpenseLoading, setMonthlyExpenseLoading] = useState(enableMonthlyExpense); + const [monthlyExpenseError, setMonthlyExpenseError] = useState(null); + + const [cardManagementData, setCardManagementData] = useState(null); + const [cardManagementLoading, setCardManagementLoading] = useState(enableCardManagement); + const [cardManagementError, setCardManagementError] = useState(null); + + // 개별 fetch 함수들 + const fetchDailyReport = useCallback(async () => { + if (!enableDailyReport) return; + try { + setDailyReportLoading(true); + setDailyReportError(null); + const apiData = await fetchApi('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('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('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('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('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, + }; +} \ No newline at end of file diff --git a/src/lib/api/dashboard/index.ts b/src/lib/api/dashboard/index.ts new file mode 100644 index 00000000..c831c140 --- /dev/null +++ b/src/lib/api/dashboard/index.ts @@ -0,0 +1,6 @@ +/** + * CEO Dashboard API 모듈 export + */ + +export * from './types'; +export * from './transformers'; \ No newline at end of file diff --git a/src/lib/api/dashboard/transformers.ts b/src/lib/api/dashboard/transformers.ts new file mode 100644 index 00000000..118c314f --- /dev/null +++ b/src/lib/api/dashboard/transformers.ts @@ -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), + }; +} \ No newline at end of file diff --git a/src/lib/api/dashboard/types.ts b/src/lib/api/dashboard/types.ts new file mode 100644 index 00000000..717b2cb3 --- /dev/null +++ b/src/lib/api/dashboard/types.ts @@ -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; + by_transaction_type: Record; + by_month: Record; +} + +// ============================================ +// 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 { + 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; +} \ No newline at end of file