From 9ad4c8ee9f52dbc5f954ae641dc9d8e0bd089902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Sat, 7 Mar 2026 03:03:07 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[CEO=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C]=20API=20=EC=97=B0=EB=8F=99=20+=20=EC=84=B9=EC=85=98?= =?UTF-8?q?=20=ED=99=95=EC=9E=A5=20+=20SummaryNavBar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 접대비/복리후생비/매출채권/캘린더 섹션 API 연동 - SummaryNavBar 추가 + mockData/modalConfigs 대규모 리팩토링 - Dashboard transformers 도메인별 분리 - 상세 모달 ScheduleDetailModal 추가 --- .../business/CEODashboard/CEODashboard.tsx | 451 +++++++++++---- .../business/CEODashboard/SummaryNavBar.tsx | 255 +++++++++ .../business/CEODashboard/components.tsx | 19 +- .../dialogs/DashboardSettingsSections.tsx | 14 +- .../business/CEODashboard/mockData.ts | 517 +----------------- .../cardManagementConfigTransformers.ts | 65 ++- .../modalConfigs/cardManagementConfigs.ts | 289 +--------- .../modalConfigs/entertainmentConfigs.ts | 237 +------- .../modalConfigs/monthlyExpenseConfigs.ts | 269 +-------- .../CEODashboard/modalConfigs/vatConfigs.ts | 75 +-- .../modalConfigs/welfareConfigs.ts | 148 +---- .../CEODashboard/modals/DetailModal.tsx | 5 +- .../modals/DetailModalSections.tsx | 57 +- .../modals/ScheduleDetailModal.tsx | 83 +-- .../CEODashboard/sections/CalendarSection.tsx | 32 +- .../sections/CardManagementSection.tsx | 71 ++- .../sections/DailyAttendanceSection.tsx | 2 +- .../sections/DailyProductionSection.tsx | 7 + .../sections/EnhancedSections.tsx | 60 +- .../sections/EntertainmentSection.tsx | 8 +- .../sections/ReceivableSection.tsx | 6 +- .../sections/StatusBoardSection.tsx | 3 +- .../CEODashboard/sections/WelfareSection.tsx | 8 +- src/components/business/CEODashboard/types.ts | 21 +- .../CEODashboard/useSectionSummary.ts | 293 ++++++++++ src/hooks/useCEODashboard.ts | 316 +++++++++-- src/hooks/useCardManagementModals.ts | 18 +- src/hooks/useDashboardFetch.ts | 8 +- src/lib/api/dashboard/endpoints.ts | 25 +- src/lib/api/dashboard/transformers.ts | 7 +- .../dashboard/transformers/daily-report.ts | 43 +- .../dashboard/transformers/expense-detail.ts | 306 ++++++++++- src/lib/api/dashboard/transformers/expense.ts | 162 ++++-- src/lib/api/dashboard/transformers/hr.ts | 32 ++ .../transformers/production-logistics.ts | 105 ++++ .../api/dashboard/transformers/receivable.ts | 101 ++-- .../dashboard/transformers/sales-purchase.ts | 75 +++ .../dashboard/transformers/status-issue.ts | 41 +- .../dashboard/transformers/tax-benefits.ts | 285 +++++++++- src/lib/api/dashboard/types.ts | 393 ++++++++++++- 40 files changed, 2935 insertions(+), 1977 deletions(-) create mode 100644 src/components/business/CEODashboard/SummaryNavBar.tsx create mode 100644 src/components/business/CEODashboard/useSectionSummary.ts create mode 100644 src/lib/api/dashboard/transformers/hr.ts create mode 100644 src/lib/api/dashboard/transformers/production-logistics.ts create mode 100644 src/lib/api/dashboard/transformers/sales-purchase.ts diff --git a/src/components/business/CEODashboard/CEODashboard.tsx b/src/components/business/CEODashboard/CEODashboard.tsx index daf5fcc0..8ee91867 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, useMemo } from 'react'; +import { useState, useCallback, useEffect, useMemo, type RefCallback } from 'react'; import { useRouter } from 'next/navigation'; import { LayoutDashboard, Settings } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -32,24 +32,27 @@ import type { CEODashboardData, CalendarScheduleItem, DashboardSettings, DetailM import { DEFAULT_DASHBOARD_SETTINGS, DEFAULT_SECTION_ORDER } from './types'; import { ScheduleDetailModal, DetailModal } from './modals'; import { DashboardSettingsDialog } from './dialogs/DashboardSettingsDialog'; -import { mockData } from './mockData'; import { LazySection } from './LazySection'; -import { useCEODashboard, useTodayIssue, useCalendar, useVat, useEntertainment, useWelfare, useWelfareDetail, useMonthlyExpenseDetail } from '@/hooks/useCEODashboard'; +import { EmptySection } from './components'; +import { SummaryNavBar } from './SummaryNavBar'; +import { useSectionSummary } from './useSectionSummary'; +import { useCEODashboard, useTodayIssue, useCalendar, useVat, useEntertainment, useEntertainmentDetail, useWelfare, useWelfareDetail, useVatDetail, useMonthlyExpenseDetail, type MonthlyExpenseCardId } from '@/hooks/useCEODashboard'; import { useCardManagementModals } from '@/hooks/useCardManagementModals'; -import { - getMonthlyExpenseModalConfig, - getCardManagementModalConfig, - getEntertainmentModalConfig, - getWelfareModalConfig, - getVatModalConfig, -} from './modalConfigs'; +import { getCardManagementModalConfigWithData } from './modalConfigs'; +import { transformEntertainmentDetailResponse, transformWelfareDetailResponse, transformVatDetailResponse } from '@/lib/api/dashboard/transformers'; +import { toast } from 'sonner'; export function CEODashboard() { const router = useRouter(); - // API 데이터 Hook (Phase 1 섹션들) + // API 데이터 Hook const apiData = useCEODashboard({ - cardManagementFallback: mockData.cardManagement, + salesStatus: true, + purchaseStatus: true, + dailyProduction: true, + unshipped: true, + construction: true, + dailyAttendance: true, }); // TodayIssue API Hook (Phase 2) @@ -79,6 +82,12 @@ export function CEODashboard() { apiData.monthlyExpense.loading || apiData.cardManagement.loading || apiData.statusBoard.loading || + apiData.salesStatus.loading || + apiData.purchaseStatus.loading || + apiData.dailyProduction.loading || + apiData.unshipped.loading || + apiData.construction.loading || + apiData.dailyAttendance.loading || todayIssueData.loading || calendarData.loading || vatData.loading || @@ -87,35 +96,37 @@ export function CEODashboard() { ); }, [apiData, todayIssueData.loading, calendarData.loading, vatData.loading, entertainmentData.loading, welfareData.loading]); - // API 데이터와 mockData를 병합 (API 우선, 실패 시 fallback) + // API 데이터만으로 구성 (mock 제거 — API 미응답 시 undefined → 빈 상태 UI) const data = useMemo(() => ({ - ...mockData, - // Phase 1 섹션들: API 데이터 우선, 실패 시 mockData fallback - // TODO: 자금현황 카드 변경 (일일일보/미수금/미지급금/당월예상지출) - 새 API 구현 후 교체 - dailyReport: mockData.dailyReport, - // TODO: D1.7 카드 구조 변경 - 새 백엔드 API 구현 후 API 데이터로 교체 - // cardManagement: 카드/경조사/상품권/접대비 (기존: 카드/가지급금/법인세/종합세) - // entertainment: 주말심야/기피업종/고액결제/증빙미비 (기존: 매출/한도/잔여한도/사용금액) - // welfare: 비과세초과/사적사용/특정인편중/한도초과 (기존: 한도/잔여한도/사용금액) - // receivable: 누적/당월/거래처/Top3 (기존: 누적/당월/거래처현황) - receivable: mockData.receivable, - debtCollection: apiData.debtCollection.data ?? mockData.debtCollection, - monthlyExpense: apiData.monthlyExpense.data ?? mockData.monthlyExpense, - cardManagement: mockData.cardManagement, - // Phase 2 섹션들 (API 연동 완료 - 목업 fallback 제거) todayIssue: apiData.statusBoard.data ?? [], todayIssueList: todayIssueData.data?.items ?? [], - calendarSchedules: calendarData.data?.items ?? mockData.calendarSchedules, - vat: vatData.data ?? mockData.vat, - entertainment: mockData.entertainment, - welfare: mockData.welfare, - // 신규 섹션 (API 미구현 - mock 데이터) - salesStatus: mockData.salesStatus, - purchaseStatus: mockData.purchaseStatus, - dailyProduction: mockData.dailyProduction, - unshipped: mockData.unshipped, - dailyAttendance: mockData.dailyAttendance, - }), [apiData, todayIssueData.data, calendarData.data, vatData.data, entertainmentData.data, welfareData.data, mockData]); + dailyReport: apiData.dailyReport.data ?? undefined, + monthlyExpense: apiData.monthlyExpense.data ?? undefined, + cardManagement: apiData.cardManagement.data ?? undefined, + entertainment: entertainmentData.data ?? undefined, + welfare: welfareData.data ?? undefined, + receivable: apiData.receivable.data ?? undefined, + debtCollection: apiData.debtCollection.data ?? undefined, + vat: vatData.data ?? undefined, + calendarSchedules: calendarData.data?.items ?? undefined, + salesStatus: apiData.salesStatus.data ?? { + cumulativeSales: 0, achievementRate: 0, yoyChange: 0, monthlySales: 0, + monthlyTrend: [], clientSales: [], dailyItems: [], dailyTotal: 0, + }, + purchaseStatus: apiData.purchaseStatus.data ?? { + cumulativePurchase: 0, unpaidAmount: 0, yoyChange: 0, + monthlyTrend: [], materialRatio: [], dailyItems: [], dailyTotal: 0, + }, + dailyProduction: apiData.dailyProduction.data ?? { + date: '', processes: [], + shipment: { expectedAmount: 0, expectedCount: 0, actualAmount: 0, actualCount: 0 }, + }, + unshipped: apiData.unshipped.data ?? { items: [] }, + constructionData: apiData.construction.data ?? { thisMonth: 0, completed: 0, items: [] }, + dailyAttendance: apiData.dailyAttendance.data ?? { + present: 0, onLeave: 0, late: 0, absent: 0, employees: [], + }, + }), [apiData, todayIssueData.data, calendarData.data, vatData.data, entertainmentData.data, welfareData.data]); // 일정 상세 모달 상태 const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false); @@ -125,17 +136,24 @@ export function CEODashboard() { const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); const [dashboardSettings, setDashboardSettings] = useState(DEFAULT_DASHBOARD_SETTINGS); + // EntertainmentDetail Hook (모달용 상세 API) - dashboardSettings 이후에 선언 + const entertainmentDetailData = useEntertainmentDetail(); + // WelfareDetail Hook (모달용 상세 API) - dashboardSettings 이후에 선언 const welfareDetailData = useWelfareDetail({ calculationType: dashboardSettings.welfare.calculationType, }); + // VatDetail Hook (부가세 상세 모달용 API) + const vatDetailData = useVatDetail(); + // MonthlyExpenseDetail Hook (당월 예상 지출 모달용 상세 API) const monthlyExpenseDetailData = useMonthlyExpenseDetail(); // 상세 모달 상태 const [isDetailModalOpen, setIsDetailModalOpen] = useState(false); const [detailModalConfig, setDetailModalConfig] = useState(null); + const [currentModalCardId, setCurrentModalCardId] = useState(null); // 클라이언트에서만 localStorage에서 설정 불러오기 (hydration 에러 방지) useEffect(() => { @@ -204,17 +222,87 @@ export function CEODashboard() { const handleDetailModalClose = useCallback(() => { setIsDetailModalOpen(false); setDetailModalConfig(null); + setCurrentModalCardId(null); }, []); - // 당월 예상 지출 카드 클릭 (개별 카드 클릭 시 상세 모달) - // TODO: D1.7 모달 구조 변경 - 새 백엔드 API 구현 후 API 데이터로 교체 - const handleMonthlyExpenseCardClick = useCallback((cardId: string) => { - const config = getMonthlyExpenseModalConfig(cardId); + // 당월 예상 지출 카드 클릭 - API 데이터로 모달 열기 + const handleMonthlyExpenseCardClick = useCallback(async (cardId: string) => { + const config = await monthlyExpenseDetailData.fetchData(cardId as MonthlyExpenseCardId); if (config) { + setCurrentModalCardId(cardId); setDetailModalConfig(config); setIsDetailModalOpen(true); } - }, []); + }, [monthlyExpenseDetailData]); + + // 모달 날짜/검색 필터 변경 → 재조회 (당월 예상 지출 + 가지급금 + 접대비 상세) + const handleDateFilterChange = useCallback(async (params: { startDate: string; endDate: string; search: string }) => { + if (!currentModalCardId) return; + + // cm2: 가지급금 상세 모달 날짜 필터 + if (currentModalCardId === 'cm2') { + try { + const modalData = await cardManagementModals.fetchModalData('cm2', { + start_date: params.startDate, + end_date: params.endDate, + }); + const config = getCardManagementModalConfigWithData('cm2', modalData); + if (config) { + setDetailModalConfig(config); + } + } catch { + // 실패 시 기존 config 유지 + } + return; + } + + // 복리후생비 상세 모달 날짜 필터 + if (currentModalCardId === 'welfare_detail') { + try { + const response = await fetch( + `/api/proxy/welfare/detail?calculation_type=${dashboardSettings.welfare.calculationType}&start_date=${params.startDate}&end_date=${params.endDate}`, + ); + if (response.ok) { + const result = await response.json(); + if (result.success) { + const config = transformWelfareDetailResponse(result.data); + setDetailModalConfig(config); + } + } + } catch { + // 실패 시 기존 config 유지 + } + return; + } + + // 접대비 상세 모달 날짜 필터 + if (currentModalCardId === 'entertainment_detail') { + try { + const response = await fetch( + `/api/proxy/entertainment/detail?company_type=${dashboardSettings.entertainment.companyType}&start_date=${params.startDate}&end_date=${params.endDate}`, + ); + if (response.ok) { + const result = await response.json(); + if (result.success) { + const config = transformEntertainmentDetailResponse(result.data); + setDetailModalConfig(config); + } + } + } catch { + // 실패 시 기존 config 유지 + } + return; + } + + // 당월 예상 지출 모달 날짜 필터 + const config = await monthlyExpenseDetailData.fetchData( + currentModalCardId as MonthlyExpenseCardId, + params, + ); + if (config) { + setDetailModalConfig(config); + } + }, [currentModalCardId, monthlyExpenseDetailData, cardManagementModals, dashboardSettings.entertainment, dashboardSettings.welfare]); // 당월 예상 지출 클릭 (deprecated - 개별 카드 클릭으로 대체) const handleMonthlyExpenseClick = useCallback(() => { @@ -222,42 +310,87 @@ export function CEODashboard() { // 카드/가지급금 관리 카드 클릭 → 모두 가지급금 상세(cm2) 모달 // 기획서 P52: 카드, 경조사, 상품권, 접대비, 총합계 모두 동일한 가지급금 상세 모달 - const handleCardManagementCardClick = useCallback((cardId: string) => { - const config = getCardManagementModalConfig('cm2'); - if (config) { - setDetailModalConfig(config); - setIsDetailModalOpen(true); + const handleCardManagementCardClick = useCallback(async (cardId: string) => { + try { + const modalData = await cardManagementModals.fetchModalData('cm2'); + const config = getCardManagementModalConfigWithData('cm2', modalData); + if (config) { + setCurrentModalCardId('cm2'); + setDetailModalConfig(config); + setIsDetailModalOpen(true); + } else { + toast.error('데이터를 불러올 수 없습니다'); + } + } catch { + toast.error('데이터를 불러올 수 없습니다'); } - }, []); + }, [cardManagementModals]); - // 접대비 현황 카드 클릭 (개별 카드 클릭 시 상세 모달) - const handleEntertainmentCardClick = useCallback((cardId: string) => { - const config = getEntertainmentModalConfig(cardId); - if (config) { - setDetailModalConfig(config); + // 접대비 현황 카드 클릭 - API 데이터로 모달 열기 + const handleEntertainmentCardClick = useCallback(async (cardId: string) => { + setCurrentModalCardId('entertainment_detail'); + const apiConfig = await entertainmentDetailData.refetch(); + if (apiConfig) { + setDetailModalConfig(apiConfig); setIsDetailModalOpen(true); + } else { + toast.error('데이터를 불러올 수 없습니다'); } - }, []); + }, [entertainmentDetailData]); // 복리후생비 현황 카드 클릭 (모든 카드가 동일한 상세 모달) - // 복리후생비 클릭 - API 데이터로 모달 열기 (fallback: 정적 config) const handleWelfareCardClick = useCallback(async () => { - // 1. 먼저 API에서 데이터 fetch 시도 - await welfareDetailData.refetch(); + const apiConfig = await welfareDetailData.refetch(); + if (apiConfig) { + setDetailModalConfig(apiConfig); + setCurrentModalCardId('welfare_detail'); + setIsDetailModalOpen(true); + } else { + toast.error('데이터를 불러올 수 없습니다'); + } + }, [welfareDetailData]); - // 2. API 데이터가 있으면 사용, 없으면 fallback config 사용 - const config = welfareDetailData.modalConfig ?? getWelfareModalConfig(dashboardSettings.welfare.calculationType); - setDetailModalConfig(config); - setIsDetailModalOpen(true); - }, [welfareDetailData, dashboardSettings.welfare.calculationType]); - - // 부가세 클릭 (모든 카드가 동일한 상세 모달) - const handleVatClick = useCallback(() => { - const config = getVatModalConfig(); - setDetailModalConfig(config); - setIsDetailModalOpen(true); + // 신고기간 변경 시 API 재호출 + const handlePeriodChange = useCallback(async (periodValue: string) => { + // periodValue: "2026-quarter-1" → parse + const parts = periodValue.split('-'); + if (parts.length < 3) return; + const [year, periodType, period] = parts; + try { + const response = await fetch( + `/api/proxy/vat/detail?period_type=${periodType}&year=${year}&period=${period}`, + ); + if (response.ok) { + const result = await response.json(); + if (result.success) { + const config = transformVatDetailResponse(result.data); + // 새 config에도 onPeriodChange 콜백 주입 + if (config.periodSelect) { + config.periodSelect.onPeriodChange = handlePeriodChange; + } + setDetailModalConfig(config); + } + } + } catch { + // 실패 시 기존 config 유지 + } }, []); + // 부가세 클릭 (모든 카드가 동일한 상세 모달) - API 데이터로 열기 + const handleVatClick = useCallback(async () => { + setCurrentModalCardId('vat_detail'); + const apiConfig = await vatDetailData.refetch(); + if (apiConfig) { + if (apiConfig.periodSelect) { + apiConfig.periodSelect.onPeriodChange = handlePeriodChange; + } + setDetailModalConfig(apiConfig); + setIsDetailModalOpen(true); + } else { + toast.error('데이터를 불러올 수 없습니다'); + } + }, [vatDetailData, handlePeriodChange]); + // 캘린더 일정 클릭 (기존 일정 수정) const handleScheduleClick = useCallback((schedule: CalendarScheduleItem) => { setSelectedSchedule(schedule); @@ -276,8 +409,8 @@ export function CEODashboard() { setSelectedSchedule(null); }, []); - // 일정 저장 - const handleScheduleSave = useCallback((formData: { + // 일정 저장 (optimistic update — refetch 없이 로컬 상태만 갱신) + const handleScheduleSave = useCallback(async (formData: { title: string; department: string; startDate: string; @@ -288,21 +421,138 @@ export function CEODashboard() { color: string; content: string; }) => { - // TODO: API 호출하여 일정 저장 - setIsScheduleModalOpen(false); - setSelectedSchedule(null); - }, []); + try { + // schedule_ 접두사에서 실제 ID 추출 + const rawId = selectedSchedule?.id; + const numericId = rawId?.startsWith('schedule_') ? rawId.replace('schedule_', '') : null; - // 일정 삭제 - const handleScheduleDelete = useCallback((id: string) => { - // TODO: API 호출하여 일정 삭제 - setIsScheduleModalOpen(false); - setSelectedSchedule(null); - }, []); + const body = { + title: formData.title, + description: formData.content, + start_date: formData.startDate, + end_date: formData.endDate, + start_time: formData.isAllDay ? null : (formData.startTime || null), + end_time: formData.isAllDay ? null : (formData.endTime || null), + is_all_day: formData.isAllDay, + color: formData.color || null, + }; + + const url = numericId + ? `/api/proxy/calendar/schedules/${numericId}` + : '/api/proxy/calendar/schedules'; + const method = numericId ? 'PUT' : 'POST'; + + const response = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + if (!response.ok) throw new Error('Failed to save schedule'); + + // API 응답에서 실제 ID 추출 (없으면 임시 ID) + let savedId = numericId; + try { + const result = await response.json(); + savedId = result.data?.id?.toString() || numericId || `temp_${Date.now()}`; + } catch { + savedId = numericId || `temp_${Date.now()}`; + } + + const updatedSchedule: CalendarScheduleItem = { + id: `schedule_${savedId}`, + title: formData.title, + startDate: formData.startDate, + endDate: formData.endDate, + startTime: formData.isAllDay ? undefined : formData.startTime, + endTime: formData.isAllDay ? undefined : formData.endTime, + isAllDay: formData.isAllDay, + type: 'schedule', + department: formData.department !== 'all' ? formData.department : undefined, + color: formData.color, + }; + + // Optimistic update: loading 변화 없이 데이터만 갱신 → 캘린더만 리렌더 + calendarData.setData((prev) => { + if (!prev) return { items: [updatedSchedule], totalCount: 1 }; + if (numericId) { + // 수정: 기존 항목 교체 + return { + ...prev, + items: prev.items.map((item) => + item.id === rawId ? updatedSchedule : item + ), + }; + } + // 신규: 추가 + return { + ...prev, + items: [...prev.items, updatedSchedule], + totalCount: prev.totalCount + 1, + }; + }); + } catch { + // 에러 시 서버 데이터로 동기화 + calendarData.refetch(); + } finally { + setIsScheduleModalOpen(false); + setSelectedSchedule(null); + } + }, [selectedSchedule, calendarData]); + + // 일정 삭제 (optimistic update) + const handleScheduleDelete = useCallback(async (id: string) => { + try { + // schedule_ 접두사에서 실제 ID 추출 + const numericId = id.startsWith('schedule_') ? id.replace('schedule_', '') : id; + + const response = await fetch(`/api/proxy/calendar/schedules/${numericId}`, { + method: 'DELETE', + }); + + if (!response.ok) throw new Error('Failed to delete schedule'); + + // Optimistic update: 삭제된 항목만 제거 → 캘린더만 리렌더 + calendarData.setData((prev) => { + if (!prev) return prev; + return { + ...prev, + items: prev.items.filter((item) => item.id !== id), + totalCount: Math.max(0, prev.totalCount - 1), + }; + }); + } catch { + // 에러 시 서버 데이터로 동기화 + calendarData.refetch(); + } finally { + setIsScheduleModalOpen(false); + setSelectedSchedule(null); + } + }, [calendarData]); // 섹션 순서 const sectionOrder = dashboardSettings.sectionOrder ?? DEFAULT_SECTION_ORDER; + // 요약 네비게이션 바 훅 + const { summaries, activeSectionKey, sectionRefs, scrollToSection } = useSectionSummary({ + data, + sectionOrder, + dashboardSettings, + }); + + // 섹션 ref 수집 콜백 + const setSectionRef = useCallback( + (key: SectionKey): RefCallback => + (el) => { + if (el) { + sectionRefs.current.set(key, el); + } else { + sectionRefs.current.delete(key); + } + }, + [sectionRefs], + ); + // 섹션 렌더링 함수 const renderDashboardSection = (key: SectionKey): React.ReactNode => { switch (key) { @@ -315,12 +565,13 @@ export function CEODashboard() { ); case 'dailyReport': - if (!dashboardSettings.dailyReport) return null; + if (!dashboardSettings.dailyReport || !data.dailyReport) return null; return ( handleMonthlyExpenseCardClick('me4')} /> ); @@ -337,7 +588,7 @@ export function CEODashboard() { ); case 'monthlyExpense': - if (!dashboardSettings.monthlyExpense) return null; + if (!dashboardSettings.monthlyExpense || !data.monthlyExpense) return null; return ( @@ -389,7 +640,7 @@ export function CEODashboard() { ); case 'debtCollection': - if (!dashboardSettings.debtCollection) return null; + if (!dashboardSettings.debtCollection || !data.debtCollection) return null; return ( @@ -397,7 +648,7 @@ export function CEODashboard() { ); case 'vat': - if (!dashboardSettings.vat) return null; + if (!dashboardSettings.vat || !data.vat) return null; return ( @@ -405,7 +656,7 @@ export function CEODashboard() { ); case 'calendar': - if (!dashboardSettings.calendar) return null; + if (!dashboardSettings.calendar || !data.calendarSchedules) return null; return ( + +
- {sectionOrder.map(renderDashboardSection)} + {sectionOrder.map((key) => { + const node = renderDashboardSection(key); + if (!node) return null; + return ( +
+ {node} +
+ ); + })}
- {/* 일정 상세 모달 */} + {/* 일정 상세 모달 — schedule_ 접두사만 수정/삭제 가능 */} {/* 항목 설정 모달 */} @@ -543,6 +809,7 @@ export function CEODashboard() { isOpen={isDetailModalOpen} onClose={handleDetailModalClose} config={detailModalConfig} + onDateFilterChange={handleDateFilterChange} /> )} diff --git a/src/components/business/CEODashboard/SummaryNavBar.tsx b/src/components/business/CEODashboard/SummaryNavBar.tsx new file mode 100644 index 00000000..cd9b70d7 --- /dev/null +++ b/src/components/business/CEODashboard/SummaryNavBar.tsx @@ -0,0 +1,255 @@ +'use client'; + +import { useRef, useEffect, useCallback, useState } from 'react'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { SectionSummary, SummaryStatus } from './useSectionSummary'; +import type { SectionKey } from './types'; + +/** 상태별 점(dot) 색상 */ +const STATUS_DOT: Record = { + normal: 'bg-green-500', + warning: 'bg-yellow-500', + danger: 'bg-red-500', +}; + +/** 상태별 칩 배경색 (비활성) */ +const STATUS_BG: Record = { + normal: 'bg-background border-border', + warning: 'bg-yellow-50 border-yellow-300 dark:bg-yellow-950/30 dark:border-yellow-700', + danger: 'bg-red-50 border-red-300 dark:bg-red-950/30 dark:border-red-700', +}; + +/** 상태별 칩 배경색 (활성) */ +const STATUS_BG_ACTIVE: Record = { + normal: 'bg-accent border-primary/40', + warning: 'bg-yellow-100 border-yellow-400 dark:bg-yellow-900/40 dark:border-yellow-600', + danger: 'bg-red-100 border-red-400 dark:bg-red-900/40 dark:border-red-600', +}; + +interface SummaryChipProps { + summary: SectionSummary; + isActive: boolean; + onClick: () => void; +} + +function SummaryChip({ summary, isActive, onClick }: SummaryChipProps) { + return ( + + ); +} + +const HEADER_BOTTOM = 100; // 헤더 하단 고정 위치 (px) +const BAR_HEIGHT = 56; // 요약바 높이 (px) — 고령 친화 확대 +const SCROLL_STEP = 200; // 화살표 버튼 클릭 시 스크롤 이동량 (px) + +interface SummaryNavBarProps { + summaries: SectionSummary[]; + activeSectionKey: SectionKey | null; + onChipClick: (key: SectionKey) => void; +} + +export function SummaryNavBar({ summaries, activeSectionKey, onChipClick }: SummaryNavBarProps) { + const scrollRef = useRef(null); + const sentinelRef = useRef(null); + const [isFixed, setIsFixed] = useState(false); + const [barRect, setBarRect] = useState({ left: 0, width: 0 }); + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(false); + + // 스크롤 가능 여부 체크 + const updateScrollButtons = useCallback(() => { + const el = scrollRef.current; + if (!el) return; + setCanScrollLeft(el.scrollLeft > 4); + setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 4); + }, []); + + // sentinel 위치 감시: sentinel이 헤더 뒤로 지나가면 fixed 모드 + useEffect(() => { + const handleScroll = () => { + if (!sentinelRef.current) return; + const rect = sentinelRef.current.getBoundingClientRect(); + const shouldFix = rect.top < HEADER_BOTTOM; + setIsFixed(shouldFix); + + if (shouldFix) { + const main = document.querySelector('main'); + if (main) { + const mainRect = main.getBoundingClientRect(); + const mainStyle = getComputedStyle(main); + const pl = parseFloat(mainStyle.paddingLeft) || 0; + const pr = parseFloat(mainStyle.paddingRight) || 0; + setBarRect({ + left: mainRect.left + pl, + width: mainRect.width - pl - pr, + }); + } + } + }; + + window.addEventListener('scroll', handleScroll, { passive: true }); + window.addEventListener('resize', handleScroll, { passive: true }); + handleScroll(); + return () => { + window.removeEventListener('scroll', handleScroll); + window.removeEventListener('resize', handleScroll); + }; + }, []); + + // 칩 영역 스크롤 상태 감시 + useEffect(() => { + const el = scrollRef.current; + if (!el) return; + updateScrollButtons(); + el.addEventListener('scroll', updateScrollButtons, { passive: true }); + const ro = new ResizeObserver(updateScrollButtons); + ro.observe(el); + return () => { + el.removeEventListener('scroll', updateScrollButtons); + ro.disconnect(); + }; + }, [updateScrollButtons, summaries]); + + // 활성 칩 자동 스크롤 into view + useEffect(() => { + if (!activeSectionKey || !scrollRef.current) return; + const chipEl = scrollRef.current.querySelector(`[data-chip-key="${activeSectionKey}"]`) as HTMLElement | null; + if (!chipEl) return; + + const container = scrollRef.current; + const chipLeft = chipEl.offsetLeft; + const chipWidth = chipEl.offsetWidth; + const containerWidth = container.offsetWidth; + const scrollLeft = container.scrollLeft; + + if (chipLeft < scrollLeft + 50 || chipLeft + chipWidth > scrollLeft + containerWidth - 50) { + container.scrollTo({ + left: chipLeft - containerWidth / 2 + chipWidth / 2, + behavior: 'smooth', + }); + } + }, [activeSectionKey]); + + const handleChipClick = useCallback( + (key: SectionKey) => { + onChipClick(key); + }, + [onChipClick], + ); + + // 화살표 버튼 핸들러 + const scrollBy = useCallback((direction: 'left' | 'right') => { + const el = scrollRef.current; + if (!el) return; + el.scrollBy({ + left: direction === 'left' ? -SCROLL_STEP : SCROLL_STEP, + behavior: 'smooth', + }); + }, []); + + if (summaries.length === 0) return null; + + const arrowBtnClass = cn( + 'flex items-center justify-center w-8 h-8 rounded-full shrink-0', + 'bg-muted/80 hover:bg-muted text-foreground', + 'border border-border shadow-sm', + 'transition-opacity duration-150', + ); + + const barContent = ( +
+ {/* 좌측 화살표 */} + + + {/* 칩 목록 */} +
+ {summaries.map((s) => ( +
+ handleChipClick(s.key)} + /> +
+ ))} +
+ + {/* 우측 화살표 */} + +
+ ); + + return ( + <> + {/* sentinel: 이 div가 헤더 뒤로 사라지면 fixed 모드 활성화 */} +
+ + {/* fixed일 때 레이아웃 공간 유지용 spacer */} + {isFixed &&
} + + {/* 실제 바 */} +
+ {barContent} +
+ + ); +} diff --git a/src/components/business/CEODashboard/components.tsx b/src/components/business/CEODashboard/components.tsx index d90d29d5..ff59e89c 100644 --- a/src/components/business/CEODashboard/components.tsx +++ b/src/components/business/CEODashboard/components.tsx @@ -319,8 +319,8 @@ export const AmountCardItem = ({
{card.subItems.map((item, idx) => (
- {item.label} - {typeof item.value === 'number' ? formatKoreanAmount(item.value) : item.value} + {item.label} + {typeof item.value === 'number' ? formatKoreanAmount(item.value) : item.value}
))}
@@ -479,4 +479,19 @@ export function CollapsibleDashboardCard({ )}
); +} + +/** + * 데이터가 없거나 API 미연동 섹션에 표시하는 빈 상태 컴포넌트 + */ +export function EmptySection({ title, message = '데이터를 불러올 수 없습니다' }: { title: string; message?: string }) { + return ( + + + +

{title}

+

{message}

+
+
+ ); } \ No newline at end of file diff --git a/src/components/business/CEODashboard/dialogs/DashboardSettingsSections.tsx b/src/components/business/CEODashboard/dialogs/DashboardSettingsSections.tsx index 11a3d4e2..04088e98 100644 --- a/src/components/business/CEODashboard/dialogs/DashboardSettingsSections.tsx +++ b/src/components/business/CEODashboard/dialogs/DashboardSettingsSections.tsx @@ -35,7 +35,7 @@ export const STATUS_BOARD_LABELS: Record = { annualLeave: '연차', vehicle: '차량', equipment: '장비', - purchase: '발주', + purchase: '발주', // [2026-03-03] 비활성화 — 설정 모달에서 숨김 처리 (STATUS_BOARD_HIDDEN_SETTINGS) approvalRequest: '결재 요청', fundStatus: '자금 현황', }; @@ -123,6 +123,13 @@ export function SectionRow({ ); } +// [2026-03-03] 설정 모달에서 숨길 항목 +// - purchase: 백엔드 path 오류 + 데이터 정합성 이슈 (API-SPEC N4 참조) +// - vehicle, equipment, fundStatus: 백엔드 API에서 미제공 (StatusBoard 응답에 없음) +const STATUS_BOARD_HIDDEN_SETTINGS = new Set([ + 'purchase', 'vehicle', 'equipment', 'fundStatus', +]); + // ─── 현황판 항목 토글 리스트 ──────────────────────── export function StatusBoardItemsList({ items, @@ -133,8 +140,9 @@ export function StatusBoardItemsList({ }) { return (
- {(Object.keys(STATUS_BOARD_LABELS) as Array).map( - (itemKey) => ( + {(Object.keys(STATUS_BOARD_LABELS) as Array) + .filter((itemKey) => !STATUS_BOARD_HIDDEN_SETTINGS.has(itemKey)) + .map((itemKey) => (
= { + // 영문 키 (category_breakdown용) + card: '카드', + congratulatory: '경조사', + gift_certificate: '상품권', + entertainment: '접대비', + // 한글 값 (loans[].category가 이미 한글인 경우 — 그대로 통과) + '카드': '카드', + '경조사': '경조사', + '상품권': '상품권', + '접대비': '접대비', +}; + /** * 가지급금 대시보드 API 응답을 cm2 모달 설정으로 변환 */ export function transformCm2ModalConfig( data: LoanDashboardApiResponse ): DetailModalConfig { - const { summary, items = [] } = data; + const { summary, category_breakdown, loans = [] } = data; - // 테이블 데이터 매핑 - const tableData = (items || []).map((item) => ({ + // 테이블 데이터 매핑 (백엔드 필드명 기준, 영문 키 → 한글 변환) + const tableData = (loans || []).map((item) => ({ date: item.loan_date, - classification: item.status_label || '카드', - category: '-', + classification: CATEGORY_LABELS[item.category] || item.category || '카드', + category: item.status_label || '-', amount: item.amount, - content: item.description, + response: item.content, })); // 분류 필터 옵션 동적 생성 @@ -202,22 +220,42 @@ export function transformCm2ModalConfig( })), ]; + // reviewCards: category_breakdown에서 4개 카테고리 카드 생성 + const reviewCards = category_breakdown + ? { + title: '가지급금 검토 필요', + cards: Object.entries(category_breakdown).map(([key, breakdown]) => ({ + label: CATEGORY_LABELS[key] || key, + amount: breakdown.outstanding_amount, + subLabel: breakdown.unverified_count > 0 + ? `미증빙 ${breakdown.unverified_count}건` + : `${breakdown.total_count}건`, + })), + } + : undefined; + return { title: '가지급금 상세', + dateFilter: { + enabled: true, + defaultPreset: '당월', + showSearch: true, + }, summaryCards: [ { label: '가지급금 합계', value: formatKoreanCurrency(summary.total_outstanding) }, { label: '인정비율 4.6%', value: summary.recognized_interest, unit: '원' }, - { label: '미정리/미분류', value: `${summary.pending_count ?? 0}건` }, + { label: '건수', value: `${summary.outstanding_count ?? 0}건` }, ], + reviewCards, table: { - title: '가지급금 관련 내역', + title: '가지급금 내역', columns: [ { key: 'no', label: 'No.', align: 'center' }, { key: 'date', label: '발생일', align: 'center' }, { key: 'classification', label: '분류', align: 'center' }, { key: 'category', label: '구분', align: 'center' }, { key: 'amount', label: '금액', align: 'right', format: 'currency' }, - { key: 'content', label: '내용', align: 'left' }, + { key: 'response', label: '대응', align: 'left' }, ], data: tableData, filters: [ @@ -227,11 +265,12 @@ export function transformCm2ModalConfig( defaultValue: 'all', }, { - key: 'category', + key: 'sortOrder', options: [ - { value: 'all', label: '전체' }, - { value: '카드명', label: '카드명' }, - { value: '계좌명', label: '계좌명' }, + { value: 'all', label: '정렬' }, + { value: 'amountDesc', label: '금액 높은순' }, + { value: 'amountAsc', label: '금액 낮은순' }, + { value: 'latest', label: '최신순' }, ], defaultValue: 'all', }, diff --git a/src/components/business/CEODashboard/modalConfigs/cardManagementConfigs.ts b/src/components/business/CEODashboard/modalConfigs/cardManagementConfigs.ts index 0d9ad10f..67328314 100644 --- a/src/components/business/CEODashboard/modalConfigs/cardManagementConfigs.ts +++ b/src/components/business/CEODashboard/modalConfigs/cardManagementConfigs.ts @@ -32,7 +32,7 @@ export interface CardManagementModalData { /** * API 데이터를 사용하여 모달 설정을 동적으로 생성 - * 데이터가 없는 경우 fallback 설정 사용 + * 데이터가 없는 경우 null 반환 (mock fallback 제거) */ export function getCardManagementModalConfigWithData( cardId: string, @@ -40,297 +40,26 @@ export function getCardManagementModalConfigWithData( ): DetailModalConfig | null { switch (cardId) { case 'cm1': - if (data.cm1Data) { - return transformCm1ModalConfig(data.cm1Data); - } - return getCardManagementModalConfig(cardId); + return data.cm1Data ? transformCm1ModalConfig(data.cm1Data) : null; case 'cm2': - if (data.cm2Data) { - return transformCm2ModalConfig(data.cm2Data); - } - return getCardManagementModalConfig(cardId); + return data.cm2Data ? transformCm2ModalConfig(data.cm2Data) : null; case 'cm3': - if (data.cm3Data) { - return transformCm3ModalConfig(data.cm3Data); - } - return getCardManagementModalConfig(cardId); + return data.cm3Data ? transformCm3ModalConfig(data.cm3Data) : null; case 'cm4': - if (data.cm4Data) { - return transformCm4ModalConfig(data.cm4Data); - } - return getCardManagementModalConfig(cardId); + return data.cm4Data ? transformCm4ModalConfig(data.cm4Data) : null; default: return null; } } -// ============================================ -// Fallback 모달 설정 (API 데이터 없을 때 사용) -// ============================================ - /** - * Fallback: 정적 목업 데이터 기반 모달 설정 - * API 데이터가 없을 때 사용 + * Fallback 모달 설정 (mock 제거 완료 — null 반환) + * API 데이터가 없을 때 모달을 열지 않음 */ -export function getCardManagementModalConfig(cardId: string): DetailModalConfig | null { - const configs: Record = { - cm1: { - title: '카드 사용 상세', - summaryCards: [ - { label: '당월 카드 사용', value: 30123000, unit: '원' }, - { label: '전월 대비', value: '+10.5%', isComparison: true, isPositive: true }, - { label: '미정리 건수', value: '5건' }, - ], - barChart: { - title: '월별 카드 사용 추이', - data: [ - { name: '7월', value: 28000000 }, - { name: '8월', value: 32000000 }, - { name: '9월', value: 27000000 }, - { name: '10월', value: 35000000 }, - { name: '11월', value: 29000000 }, - { name: '12월', value: 30123000 }, - ], - dataKey: 'value', - xAxisKey: 'name', - color: '#60A5FA', - }, - pieChart: { - title: '사용자별 카드 사용 비율', - data: [ - { name: '대표이사', value: 15000000, percentage: 50, color: '#60A5FA' }, - { name: '경영지원팀', value: 9000000, percentage: 30, color: '#34D399' }, - { name: '영업팀', value: 6123000, percentage: 20, color: '#FBBF24' }, - ], - }, - table: { - title: '카드 사용 내역', - columns: [ - { key: 'no', label: 'No.', align: 'center' }, - { key: 'cardName', label: '카드명', align: 'left' }, - { key: 'user', label: '사용자', align: 'center' }, - { key: 'date', label: '사용일시', align: 'center', format: 'date' }, - { key: 'store', label: '가맹점명', align: 'left' }, - { key: 'amount', label: '사용금액', align: 'right', format: 'currency' }, - { key: 'usageType', label: '사용유형', align: 'center', highlightValue: '미설정' }, - ], - data: [ - { cardName: '법인카드1', user: '대표이사', date: '2026-01-05 18:30', store: '스타벅스 강남점', amount: 45000, usageType: '복리후생비' }, - { cardName: '법인카드1', user: '대표이사', date: '2026-01-04 12:15', store: '한식당', amount: 350000, usageType: '접대비' }, - { cardName: '법인카드2', user: '경영지원팀', date: '2026-01-03 14:20', store: '오피스디포', amount: 125000, usageType: '소모품비' }, - { cardName: '법인카드1', user: '대표이사', date: '2026-01-02 19:45', store: '골프장', amount: 850000, usageType: '미설정' }, - { cardName: '법인카드3', user: '영업팀', date: '2026-01-02 11:30', store: 'GS칼텍스', amount: 80000, usageType: '교통비' }, - { cardName: '법인카드2', user: '경영지원팀', date: '2026-01-01 16:00', store: '이마트', amount: 230000, usageType: '미설정' }, - { cardName: '법인카드1', user: '대표이사', date: '2025-12-30 20:30', store: '백화점', amount: 1500000, usageType: '미설정' }, - { cardName: '법인카드3', user: '영업팀', date: '2025-12-29 09:15', store: '커피빈', amount: 32000, usageType: '복리후생비' }, - { cardName: '법인카드2', user: '경영지원팀', date: '2025-12-28 13:45', store: '문구점', amount: 55000, usageType: '소모품비' }, - { cardName: '법인카드1', user: '대표이사', date: '2025-12-27 21:00', store: '호텔', amount: 450000, usageType: '미설정' }, - ], - filters: [ - { - key: 'user', - options: [ - { value: 'all', label: '전체' }, - { value: '대표이사', label: '대표이사' }, - { value: '경영지원팀', label: '경영지원팀' }, - { value: '영업팀', label: '영업팀' }, - ], - defaultValue: 'all', - }, - { - key: 'usageType', - options: [ - { value: 'all', label: '전체' }, - { value: '미설정', label: '미설정' }, - { value: '복리후생비', label: '복리후생비' }, - { value: '접대비', label: '접대비' }, - { value: '소모품비', label: '소모품비' }, - { value: '교통비', label: '교통비' }, - ], - defaultValue: 'all', - }, - ], - showTotal: true, - totalLabel: '합계', - totalValue: 30123000, - totalColumnKey: 'amount', - }, - }, - // P52: 가지급금 상세 - cm2: { - title: '가지급금 상세', - dateFilter: { - enabled: true, - defaultPreset: '당월', - showSearch: true, - }, - summaryCards: [ - { label: '가지급금 합계', value: '4.5억원' }, - { label: '가지급금 총액', value: 6000000, unit: '원' }, - { label: '건수', value: '10건' }, - ], - reviewCards: { - title: '가지급금 검토 필요', - cards: [ - { label: '카드', amount: 3123000, subLabel: '미정리 5건' }, - { label: '경조사', amount: 3123000, subLabel: '미증빙 5건' }, - { label: '상품권', amount: 3123000, subLabel: '미증빙 5건' }, - { label: '접대비', amount: 3123000, subLabel: '미증빙 5건' }, - ], - }, - table: { - title: '가지급금 내역', - columns: [ - { key: 'no', label: 'No.', align: 'center' }, - { key: 'date', label: '발생일', align: 'center' }, - { key: 'classification', label: '분류', align: 'center' }, - { key: 'category', label: '구분', align: 'center' }, - { key: 'amount', label: '금액', align: 'right', format: 'currency' }, - { key: 'response', label: '대응', align: 'left' }, - ], - data: [ - { date: '2025-12-12', classification: '카드', category: '카드명', amount: 1000000, response: '미정리' }, - { date: '2025-12-12', classification: '카드', category: '카드명', amount: 1000000, response: '미증빙' }, - { date: '2025-12-12', classification: '경조사', category: '계좌명', amount: 1000000, response: '미증빙' }, - { date: '2025-12-12', classification: '상품권', category: '계좌명', amount: 1000000, response: '미증빙' }, - { date: '2025-12-12', classification: '접대비', category: '카드명', amount: 1000000, response: '주말 카드 사용' }, - { date: '2025-12-12', classification: '접대비', category: '카드명', amount: 1000000, response: '접대비 불인정' }, - { date: '2025-12-12', classification: '카드', category: '카드명', amount: 1000000, response: '불인정 가맹점(귀금속)' }, - ], - filters: [ - { - key: 'classification', - options: [ - { value: 'all', label: '전체' }, - { value: '카드', label: '카드' }, - { value: '경조사', label: '경조사' }, - { value: '상품권', label: '상품권' }, - { value: '접대비', label: '접대비' }, - ], - defaultValue: 'all', - }, - { - key: 'sortOrder', - options: [ - { value: 'all', label: '정렬' }, - { value: 'amountDesc', label: '금액 높은순' }, - { value: 'amountAsc', label: '금액 낮은순' }, - { value: 'latest', label: '최신순' }, - ], - defaultValue: 'all', - }, - ], - showTotal: true, - totalLabel: '합계', - totalValue: 111000000, - totalColumnKey: 'amount', - }, - }, - cm3: { - title: '법인세 예상 가중 상세', - summaryCards: [ - { label: '법인세 예상 증가', value: 3123000, unit: '원' }, - { label: '인정 이자', value: 6000000, unit: '원' }, - { label: '가지급금', value: '4.5억원' }, - { label: '인정이자', value: 6000000, unit: '원' }, - ], - comparisonSection: { - leftBox: { - title: '없을때 법인세', - items: [ - { label: '과세표준', value: '3억원' }, - { label: '법인세', value: 50970000, unit: '원' }, - ], - borderColor: 'orange', - }, - rightBox: { - title: '있을때 법인세', - items: [ - { label: '과세표준', value: '3.06억원' }, - { label: '법인세', value: 54093000, unit: '원' }, - ], - borderColor: 'blue', - }, - vsLabel: '법인세 예상 증가', - vsValue: 3123000, - vsSubLabel: '법인 세율 -12.5%', - }, - referenceTable: { - title: '법인세 과세표준 (2024년 기준)', - columns: [ - { key: 'bracket', label: '과세표준', align: 'left' }, - { key: 'rate', label: '세율', align: 'center' }, - { key: 'formula', label: '계산식', align: 'left' }, - ], - data: [ - { bracket: '2억원 이하', rate: '9%', formula: '과세표준 × 9%' }, - { bracket: '2억원 초과 ~ 200억원 이하', rate: '19%', formula: '1,800만원 + (2억원 초과분 × 19%)' }, - { bracket: '200억원 초과 ~ 3,000억원 이하', rate: '21%', formula: '37.62억원 + (200억원 초과분 × 21%)' }, - { bracket: '3,000억원 초과', rate: '24%', formula: '625.62억원 + (3,000억원 초과분 × 24%)' }, - ], - }, - }, - cm4: { - title: '대표자 종합소득세 예상 가중 상세', - summaryCards: [ - { label: '대표자 종합세 예상 가중', value: 3123000, unit: '원' }, - { label: '추가 세금', value: '+12.5%', isComparison: true, isPositive: false }, - { label: '가지급금', value: '4.5억원' }, - { label: '인정이자 4.6%', value: 6000000, unit: '원' }, - ], - comparisonSection: { - leftBox: { - title: '가지급금 인정이자가 반영된 종합소득세', - items: [ - { label: '현재 예상 과세표준 (근로소득+상여)', value: 6000000, unit: '원' }, - { label: '현재 적용 세율', value: '19%' }, - { label: '현재 예상 세액', value: 10000000, unit: '원' }, - ], - borderColor: 'orange', - }, - rightBox: { - title: '가지급금 인정이자가 정리된 종합소득세', - items: [ - { label: '가지급금 정리 시 예상 과세표준 (근로소득+상여)', value: 6000000, unit: '원' }, - { label: '가지급금 정리 시 적용 세율', value: '19%' }, - { label: '가지급금 정리 시 예상 세액', value: 10000000, unit: '원' }, - ], - borderColor: 'blue', - }, - vsLabel: '종합소득세 예상 절감', - vsValue: 3123000, - vsSubLabel: '감소 세금 -12.5%', - vsBreakdown: [ - { label: '종합소득세', value: -2000000, unit: '원' }, - { label: '지방소득세', value: -200000, unit: '원' }, - { label: '4대 보험', value: -1000000, unit: '원' }, - ], - }, - referenceTable: { - title: '종합소득세 과세표준 (2024년 기준)', - columns: [ - { key: 'bracket', label: '과세표준', align: 'left' }, - { key: 'rate', label: '세율', align: 'center' }, - { key: 'deduction', label: '누진공제', align: 'right' }, - { key: 'formula', label: '계산식', align: 'left' }, - ], - data: [ - { bracket: '1,400만원 이하', rate: '6%', deduction: '-', formula: '과세표준 × 6%' }, - { bracket: '1,400만원 초과 ~ 5,000만원 이하', rate: '15%', deduction: '126만원', formula: '과세표준 × 15% - 126만원' }, - { bracket: '5,000만원 초과 ~ 8,800만원 이하', rate: '24%', deduction: '576만원', formula: '과세표준 × 24% - 576만원' }, - { bracket: '8,800만원 초과 ~ 1.5억원 이하', rate: '35%', deduction: '1,544만원', formula: '과세표준 × 35% - 1,544만원' }, - { bracket: '1.5억원 초과 ~ 3억원 이하', rate: '38%', deduction: '1,994만원', formula: '과세표준 × 38% - 1,994만원' }, - { bracket: '3억원 초과 ~ 5억원 이하', rate: '40%', deduction: '2,594만원', formula: '과세표준 × 40% - 2,594만원' }, - { bracket: '5억원 초과 ~ 10억원 이하', rate: '42%', deduction: '3,594만원', formula: '과세표준 × 42% - 3,594만원' }, - { bracket: '10억원 초과', rate: '45%', deduction: '6,594만원', formula: '과세표준 × 45% - 6,594만원' }, - ], - }, - }, - }; - - return configs[cardId] || null; +export function getCardManagementModalConfig(_cardId: string): DetailModalConfig | null { + return null; } diff --git a/src/components/business/CEODashboard/modalConfigs/entertainmentConfigs.ts b/src/components/business/CEODashboard/modalConfigs/entertainmentConfigs.ts index 39164a5b..497e02f1 100644 --- a/src/components/business/CEODashboard/modalConfigs/entertainmentConfigs.ts +++ b/src/components/business/CEODashboard/modalConfigs/entertainmentConfigs.ts @@ -1,239 +1,10 @@ import type { DetailModalConfig } from '../types'; -/** - * 접대비 상세 공통 모달 config (et2, et3, et4 공통) - */ -const entertainmentDetailConfig: DetailModalConfig = { - title: '접대비 상세', - dateFilter: { - enabled: true, - defaultPreset: '당월', - showSearch: true, - }, - summaryCards: [ - // 첫 번째 줄: 당해년도 - { label: '당해년도 접대비 총 한도', value: 3123000, unit: '원' }, - { label: '당해년도 접대비 잔여한도', value: 6000000, unit: '원' }, - { label: '당해년도 접대비 사용금액', value: 6000000, unit: '원' }, - { label: '당해년도 접대비 초과 금액', value: 0, unit: '원' }, - ], - reviewCards: { - title: '접대비 검토 필요', - cards: [ - { label: '주말/심야', amount: 3123000, subLabel: '미증빙 5건' }, - { label: '기피업종 (유흥, 귀금속 등)', amount: 3123000, subLabel: '불인정 5건' }, - { label: '고액 결제', amount: 3123000, subLabel: '미증빙 5건' }, - { label: '증빙 미비', amount: 3123000, subLabel: '미증빙 5건' }, - ], - }, - barChart: { - title: '월별 접대비 사용 추이', - data: [ - { name: '1월', value: 3500000 }, - { name: '2월', value: 4200000 }, - { name: '3월', value: 2300000 }, - { name: '4월', value: 3800000 }, - { name: '5월', value: 4500000 }, - { name: '6월', value: 3200000 }, - { name: '7월', value: 2800000 }, - ], - dataKey: 'value', - xAxisKey: 'name', - color: '#60A5FA', - }, - pieChart: { - title: '사용자별 접대비 사용 비율', - data: [ - { name: '홍길동', value: 15000000, percentage: 53, color: '#60A5FA' }, - { name: '김철수', value: 10000000, percentage: 31, color: '#34D399' }, - { name: '이영희', value: 10000000, percentage: 10, color: '#FBBF24' }, - { name: '기타', value: 2000000, percentage: 6, color: '#F87171' }, - ], - }, - table: { - title: '월별 접대비 사용 내역', - columns: [ - { key: 'no', label: 'No.', align: 'center' }, - { key: 'cardName', label: '카드명', align: 'left' }, - { key: 'user', label: '사용자', align: 'center' }, - { key: 'useDate', label: '사용일시', align: 'center', format: 'date' }, - { key: 'transDate', label: '거래일시', align: 'center', format: 'date' }, - { key: 'amount', label: '사용금액', align: 'right', format: 'currency' }, - { key: 'content', label: '내용', align: 'left' }, - ], - data: [ - { cardName: '카드명', user: '홍길동', useDate: '2025-12-12 12:12', transDate: '가맹점명', amount: 1000000, content: '심야 카드 사용' }, - { cardName: '카드명', user: '홍길동', useDate: '2025-12-12 12:12', transDate: '가맹점명', amount: 1000000, content: '미증빙' }, - { cardName: '카드명', user: '홍길동', useDate: '2025-12-12 12:12', transDate: '가맹점명', amount: 1000000, content: '고액 결제' }, - { cardName: '카드명', user: '김철수', useDate: '2025-10-14 12:12', transDate: '가맹점명', amount: 1000000, content: '불인정 가맹점 (귀금속)' }, - { cardName: '카드명', user: '이영희', useDate: '2025-12-12 12:12', transDate: '가맹점명', amount: 1000000, content: '접대비 불인정' }, - ], - filters: [ - { - key: 'user', - options: [ - { value: 'all', label: '전체' }, - { value: '홍길동', label: '홍길동' }, - { value: '김철수', label: '김철수' }, - { value: '이영희', label: '이영희' }, - ], - defaultValue: 'all', - }, - { - key: 'content', - options: [ - { value: 'all', label: '전체' }, - { value: '주말/심야', label: '주말/심야' }, - { value: '기피업종', label: '기피업종' }, - { value: '고액 결제', label: '고액 결제' }, - { value: '증빙 미비', label: '증빙 미비' }, - ], - defaultValue: 'all', - }, - ], - showTotal: true, - totalLabel: '합계', - totalValue: 11000000, - totalColumnKey: 'amount', - }, - // 접대비 손금한도 계산 - 기본한도 / 수입금액별 추가한도 - referenceTables: [ - { - title: '접대비 손금한도 계산 - 기본한도', - columns: [ - { key: 'type', label: '법인 유형', align: 'left' }, - { key: 'annualLimit', label: '연간 기본한도', align: 'right' }, - { key: 'monthlyLimit', label: '월 환산', align: 'right' }, - ], - data: [ - { type: '일반법인', annualLimit: '12,000,000원', monthlyLimit: '1,000,000원' }, - { type: '중소기업', annualLimit: '36,000,000원', monthlyLimit: '3,000,000원' }, - ], - }, - { - title: '수입금액별 추가한도', - columns: [ - { key: 'range', label: '수입금액 구간', align: 'left' }, - { key: 'formula', label: '추가한도 계산식', align: 'left' }, - ], - data: [ - { range: '100억원 이하', formula: '수입금액 × 0.2%' }, - { range: '100억 초과 ~ 500억 이하', formula: '2,000만원 + (수입금액 - 100억) × 0.1%' }, - { range: '500억원 초과', formula: '6,000만원 + (수입금액 - 500억) × 0.03%' }, - ], - }, - ], - // 접대비 계산 - calculationCards: { - title: '접대비 계산', - cards: [ - { label: '중소기업 연간 기본한도', value: 36000000 }, - { label: '당해년도 수입금액별 추가한도', value: 16000000, operator: '+' }, - { label: '당해년도 접대비 총 한도', value: 52000000, operator: '=' }, - ], - }, - // 접대비 현황 (분기별) - quarterlyTable: { - title: '접대비 현황', - rows: [ - { label: '한도금액', q1: 13000000, q2: 13000000, q3: 13000000, q4: 13000000, total: 52000000 }, - { label: '이월금액', q1: 0, q2: '', q3: '', q4: '', total: '' }, - { label: '사용금액', q1: 1000000, q2: '', q3: '', q4: '', total: '' }, - { label: '잔여한도', q1: 11000000, q2: '', q3: '', q4: '', total: '' }, - { label: '초과금액', q1: '', q2: '', q3: '', q4: '', total: '' }, - ], - }, -}; - /** * 접대비 현황 모달 설정 - * et_sales: 당해 매출 상세 - * et_limit, et_remaining, et_used: 접대비 상세 (공통) + * API 연동 완료 — useEntertainmentDetail hook이 실제 데이터 반환 + * 이 함수는 하위 호환용으로 유지하되 null 반환 */ -export function getEntertainmentModalConfig(cardId: string): DetailModalConfig | null { - const configs: Record = { - et_sales: { - title: '당해 매출 상세', - summaryCards: [ - { label: '당해년도 매출', value: 600000000, unit: '원' }, - { label: '전년 대비', value: '-12.5%', isComparison: true, isPositive: false }, - { label: '당월 매출', value: 6000000, unit: '원' }, - ], - barChart: { - title: '월별 매출 추이', - data: [ - { name: '1월', value: 85000000 }, - { name: '2월', value: 92000000 }, - { name: '3월', value: 78000000 }, - { name: '4월', value: 95000000 }, - { name: '5월', value: 88000000 }, - { name: '6월', value: 102000000 }, - { name: '7월', value: 60000000 }, - ], - dataKey: 'value', - xAxisKey: 'name', - color: '#60A5FA', - }, - horizontalBarChart: { - title: '당해년도 거래처별 매출', - data: [ - { name: '(주)세우', value: 120000000 }, - { name: '대한건설', value: 95000000 }, - { name: '삼성테크', value: 78000000 }, - { name: '현대상사', value: 65000000 }, - { name: '기타', value: 42000000 }, - ], - color: '#60A5FA', - }, - table: { - title: '일별 매출 내역', - columns: [ - { key: 'no', label: 'No.', align: 'center' }, - { key: 'date', label: '매출일', align: 'center', format: 'date' }, - { key: 'vendor', label: '거래처', align: 'left' }, - { key: 'amount', label: '매출금액', align: 'right', format: 'currency' }, - { key: 'type', label: '매출유형', align: 'center', highlightValue: '미설정' }, - ], - data: [ - { date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '상품 매출' }, - { date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '부품 매출' }, - { date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '공사 매출' }, - { date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '미설정' }, - { date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '미설정' }, - { date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '미설정' }, - { date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '상품 매출' }, - ], - filters: [ - { - key: 'type', - options: [ - { value: 'all', label: '전체' }, - { value: '상품 매출', label: '상품 매출' }, - { value: '부품 매출', label: '부품 매출' }, - { value: '공사 매출', label: '공사 매출' }, - { value: '임대 수익', label: '임대 수익' }, - { value: '기타 매출', label: '기타 매출' }, - { value: '미설정', label: '미설정' }, - ], - defaultValue: 'all', - }, - ], - showTotal: true, - totalLabel: '합계', - totalValue: 111000000, - totalColumnKey: 'amount', - }, - }, - // et_limit, et_remaining, et_used는 모두 동일한 접대비 상세 모달 - et_limit: entertainmentDetailConfig, - et_remaining: entertainmentDetailConfig, - et_used: entertainmentDetailConfig, - // 대시보드 카드 ID (et1~et4) → 접대비 상세 모달 - et1: entertainmentDetailConfig, - et2: entertainmentDetailConfig, - et3: entertainmentDetailConfig, - et4: entertainmentDetailConfig, - }; - - return configs[cardId] || null; +export function getEntertainmentModalConfig(_cardId: string): DetailModalConfig | null { + return null; } \ No newline at end of file diff --git a/src/components/business/CEODashboard/modalConfigs/monthlyExpenseConfigs.ts b/src/components/business/CEODashboard/modalConfigs/monthlyExpenseConfigs.ts index 170b3ab4..01faf743 100644 --- a/src/components/business/CEODashboard/modalConfigs/monthlyExpenseConfigs.ts +++ b/src/components/business/CEODashboard/modalConfigs/monthlyExpenseConfigs.ts @@ -1,269 +1,10 @@ import type { DetailModalConfig } from '../types'; /** - * 당월 예상 지출 모달 설정 (D1.7 기획서 P48-51 반영) + * 당월 예상 지출 모달 설정 + * API 연동 완료 — useMonthlyExpenseDetail hook이 실제 데이터 반환 + * 이 함수는 하위 호환용으로 유지하되 null 반환 */ -export function getMonthlyExpenseModalConfig(cardId: string): DetailModalConfig | null { - const configs: Record = { - // P48: 매입 상세 - me1: { - title: '매입 상세', - dateFilter: { - enabled: true, - defaultPreset: '당월', - showSearch: true, - }, - summaryCards: [ - { label: '매입', value: 3123000, unit: '원' }, - { label: '이전 대비', value: '-12.5%', isComparison: true, isPositive: false }, - ], - barChart: { - title: '매입 추이', - data: [ - { name: '1월', value: 45000000 }, - { name: '2월', value: 52000000 }, - { name: '3월', value: 48000000 }, - { name: '4월', value: 61000000 }, - { name: '5월', value: 55000000 }, - { name: '6월', value: 58000000 }, - { name: '7월', value: 50000000 }, - ], - dataKey: 'value', - xAxisKey: 'name', - color: '#60A5FA', - }, - pieChart: { - title: '자재 유형별 구매 비율', - data: [ - { name: '원자재', value: 55000000, percentage: 55, color: '#60A5FA' }, - { name: '부자재', value: 35000000, percentage: 35, color: '#FBBF24' }, - { name: '포장재', value: 10000000, percentage: 10, color: '#F87171' }, - ], - }, - table: { - title: '일별 매입 내역', - columns: [ - { key: 'no', label: 'No.', align: 'center' }, - { key: 'date', label: '매입일', align: 'center', format: 'date' }, - { key: 'vendor', label: '거래처', align: 'left' }, - { key: 'amount', label: '매입금액', align: 'right', format: 'currency' }, - ], - data: [ - { date: '2025-12-01', vendor: '회사명', amount: 11000000 }, - { date: '2025-12-01', vendor: '회사명', amount: 11000000 }, - { date: '2025-12-01', vendor: '회사명', amount: 11000000 }, - { date: '2025-12-01', vendor: '회사명', amount: 11000000 }, - { date: '2025-12-01', vendor: '회사명', amount: 11000000 }, - { date: '2025-12-01', vendor: '회사명', amount: 11000000 }, - ], - showTotal: true, - totalLabel: '합계', - totalValue: 111000000, - totalColumnKey: 'amount', - }, - }, - // P49: 카드 상세 - me2: { - title: '카드 상세', - dateFilter: { - enabled: true, - defaultPreset: '당월', - showSearch: true, - }, - summaryCards: [ - { label: '카드 사용', value: 6000000, unit: '원' }, - { label: '이전 대비', value: '-12.5%', isComparison: true, isPositive: false }, - { label: '건수', value: '10건' }, - ], - barChart: { - title: '카드 사용 추이', - data: [ - { name: '1월', value: 4500000 }, - { name: '2월', value: 5200000 }, - { name: '3월', value: 4800000 }, - { name: '4월', value: 6100000 }, - { name: '5월', value: 5500000 }, - { name: '6월', value: 5800000 }, - { name: '7월', value: 6000000 }, - ], - dataKey: 'value', - xAxisKey: 'name', - color: '#60A5FA', - }, - pieChart: { - title: '사용자별 카드 사용 비율', - data: [ - { name: '홍길동', value: 55000000, percentage: 55, color: '#60A5FA' }, - { name: '김영희', value: 35000000, percentage: 35, color: '#FBBF24' }, - { name: '이정현', value: 10000000, percentage: 10, color: '#F87171' }, - ], - }, - table: { - title: '일별 카드 사용 내역', - columns: [ - { key: 'no', label: 'No.', align: 'center' }, - { key: 'cardName', label: '카드명', align: 'left' }, - { key: 'user', label: '사용자', align: 'center' }, - { key: 'date', label: '사용일자', align: 'center', format: 'date' }, - { key: 'store', label: '가맹점명', align: 'left' }, - { key: 'amount', label: '사용금액', align: 'right', format: 'currency' }, - { key: 'usageType', label: '계정과목', align: 'center', highlightValue: '미설정' }, - ], - data: [ - { cardName: '홍길동', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1000000, usageType: '복리후생비' }, - { cardName: '홍길동', user: '홍길동', date: '2025-12-11 14:30', store: '가맹점명', amount: 1000000, usageType: '접대비' }, - { cardName: '홍길동', user: '홍길동', date: '2025-12-10 09:45', store: '가맹점명', amount: 1000000, usageType: '미설정' }, - { cardName: '홍길동', user: '홍길동', date: '2025-12-09 18:20', store: '가맹점명', amount: 1000000, usageType: '미설정' }, - ], - filters: [ - { - key: 'user', - options: [ - { value: 'all', label: '전체' }, - { value: '홍길동', label: '홍길동' }, - { value: '김영희', label: '김영희' }, - { value: '이정현', label: '이정현' }, - ], - defaultValue: 'all', - }, - ], - showTotal: true, - totalLabel: '합계', - totalValue: 11000000, - totalColumnKey: 'amount', - }, - }, - // P50: 발행어음 상세 - me3: { - title: '발행어음 상세', - dateFilter: { - enabled: true, - presets: ['당해년도', '전전월', '전월', '당월', '어제'], - defaultPreset: '당월', - showSearch: true, - }, - summaryCards: [ - { label: '발행어음', value: 3123000, unit: '원' }, - { label: '이전 대비', value: '-12.5%', isComparison: true, isPositive: false }, - ], - barChart: { - title: '발행어음 추이', - data: [ - { name: '1월', value: 2000000 }, - { name: '2월', value: 2500000 }, - { name: '3월', value: 2200000 }, - { name: '4월', value: 2800000 }, - { name: '5월', value: 2600000 }, - { name: '6월', value: 3000000 }, - { name: '7월', value: 3123000 }, - ], - dataKey: 'value', - xAxisKey: 'name', - color: '#60A5FA', - }, - pieChart: { - title: '거래처별 발행어음', - data: [ - { name: '거래처1', value: 50000000, percentage: 45, color: '#60A5FA' }, - { name: '거래처2', value: 35000000, percentage: 32, color: '#FBBF24' }, - { name: '거래처3', value: 20000000, percentage: 18, color: '#F87171' }, - { name: '거래처4', value: 6000000, percentage: 5, color: '#34D399' }, - ], - }, - table: { - title: '일별 발행어음 내역', - columns: [ - { key: 'no', label: 'No.', align: 'center' }, - { key: 'vendor', label: '거래처', align: 'left' }, - { key: 'issueDate', label: '발행일', align: 'center', format: 'date' }, - { key: 'dueDate', label: '만기일', align: 'center', format: 'date' }, - { key: 'amount', label: '금액', align: 'right', format: 'currency' }, - { key: 'status', label: '상태', align: 'center' }, - ], - data: [ - { vendor: '회사명', issueDate: '2025-12-12', dueDate: '2025-12-12', amount: 1000000, status: '보관중' }, - { vendor: '회사명', issueDate: '2025-12-12', dueDate: '2025-12-12', amount: 1000000, status: '만기임박' }, - { vendor: '회사명', issueDate: '2025-12-12', dueDate: '2025-12-12', amount: 1000000, status: '보관중' }, - { vendor: '회사명', issueDate: '2025-12-12', dueDate: '2025-12-12', amount: 1000000, status: '만기임박' }, - { vendor: '회사명', issueDate: '2025-12-12', dueDate: '2025-12-12', amount: 1000000, status: '보관중' }, - { vendor: '회사명', issueDate: '2025-12-12', dueDate: '2025-12-12', amount: 1000000, status: '만기임박' }, - ], - filters: [ - { - key: 'vendor', - options: [ - { value: 'all', label: '전체' }, - { value: '회사명', label: '회사명' }, - ], - defaultValue: 'all', - }, - { - key: 'status', - options: [ - { value: 'all', label: '전체' }, - { value: '보관중', label: '보관중' }, - { value: '만기임박', label: '만기임박' }, - { value: '만기경과', label: '만기경과' }, - { value: '결제완료', label: '결제완료' }, - { value: '부도', label: '부도' }, - ], - defaultValue: 'all', - }, - ], - showTotal: true, - totalLabel: '합계', - totalValue: 111000000, - totalColumnKey: 'amount', - }, - }, - // P51: 당월 지출 예상 상세 - me4: { - title: '당월 지출 예상 상세', - summaryCards: [ - { label: '당월 지출 예상', value: 6000000, unit: '원' }, - { label: '전월 대비', value: '-12.5%', isComparison: true, isPositive: false }, - { label: '총 계좌 잔액', value: 10000000, unit: '원' }, - ], - table: { - title: '당월 지출 승인 내역서', - columns: [ - { key: 'paymentDate', label: '예상 지급일', align: 'center' }, - { key: 'item', label: '항목', align: 'left' }, - { key: 'amount', label: '지출금액', align: 'right', format: 'currency', highlightColor: 'red' }, - { key: 'vendor', label: '거래처', align: 'center' }, - { key: 'account', label: '계좌', align: 'center' }, - ], - data: [ - { paymentDate: '2025-12-12', item: '품의 사유...', amount: 1000000, vendor: '회사명', account: '국민은행 1234' }, - { paymentDate: '2025-12-12', item: '품의 사유...', amount: 1000000, vendor: '회사명', account: '국민은행 1234' }, - { paymentDate: '2025-12-12', item: '(발행 어음) 123123123', amount: 1000000, vendor: '회사명', account: '국민은행 1234' }, - { paymentDate: '2025-12-12', item: '품의 사유...', amount: 1000000, vendor: '회사명', account: '국민은행 1234' }, - { paymentDate: '2025-12-12', item: '거래처명 12월분', amount: 1000000, vendor: '회사명', account: '국민은행 1234' }, - { paymentDate: '2025-12-12', item: '품의 사유...', amount: 1000000, vendor: '회사명', account: '국민은행 1234' }, - { paymentDate: '2025-12-12', item: '적요 내용', amount: 1000000, vendor: '회사명', account: '국민은행 1234' }, - ], - filters: [ - { - key: 'vendor', - options: [ - { value: 'all', label: '전체' }, - { value: '회사명', label: '회사명' }, - ], - defaultValue: 'all', - }, - ], - showTotal: true, - totalLabel: '2025/12 계', - totalValue: 6000000, - totalColumnKey: 'amount', - footerSummary: [ - { label: '지출 합계', value: 6000000 }, - { label: '계좌 잔액', value: 10000000 }, - { label: '최종 차액', value: 4000000 }, - ], - }, - }, - }; - - return configs[cardId] || null; +export function getMonthlyExpenseModalConfig(_cardId: string): DetailModalConfig | null { + return null; } diff --git a/src/components/business/CEODashboard/modalConfigs/vatConfigs.ts b/src/components/business/CEODashboard/modalConfigs/vatConfigs.ts index 56da1940..9648d1e8 100644 --- a/src/components/business/CEODashboard/modalConfigs/vatConfigs.ts +++ b/src/components/business/CEODashboard/modalConfigs/vatConfigs.ts @@ -2,76 +2,9 @@ import type { DetailModalConfig } from '../types'; /** * 부가세 모달 설정 - * 모든 카드가 동일한 상세 모달 + * API 연동 완료 — useVatDetail hook이 실제 데이터 반환 + * 이 함수는 하위 호환용으로 유지하되 null 반환 */ -export function getVatModalConfig(): DetailModalConfig { - return { - title: '예상 납부세액', - periodSelect: { - enabled: true, - options: [ - { value: '2026-1-expected', label: '2026년 1기 예정' }, - { value: '2025-2-confirmed', label: '2025년 2기 확정' }, - { value: '2025-2-expected', label: '2025년 2기 예정' }, - { value: '2025-1-confirmed', label: '2025년 1기 확정' }, - ], - defaultValue: '2026-1-expected', - }, - summaryCards: [ - { label: '예상매출', value: '30.5억원' }, - { label: '예상매입', value: '20.5억원' }, - { label: '예상 납부세액', value: '1.1억원' }, - ], - // 부가세 요약 테이블 - referenceTable: { - title: '2026년 1기 예정 부가세 요약', - columns: [ - { key: 'category', label: '구분', align: 'left' }, - { key: 'supplyAmount', label: '공급가액', align: 'right' }, - { key: 'taxAmount', label: '세액', align: 'right' }, - ], - data: [ - { category: '매출(전자세금계산서)', supplyAmount: '100,000,000', taxAmount: '10,000,000' }, - { category: '매입(전자세금계산서)', supplyAmount: '10,000,000', taxAmount: '1,000,000' }, - { category: '매입(종이세금계산서)', supplyAmount: '10,000,000', taxAmount: '1,000,000' }, - { category: '매입(계산서)', supplyAmount: '10,000,000', taxAmount: '1,000,000' }, - { category: '매입(신용카드)', supplyAmount: '10,000,000', taxAmount: '1,000,000' }, - { category: '납부세액', supplyAmount: '', taxAmount: '6,000,000' }, - ], - }, - // 세금계산서 미발행/미수취 내역 - table: { - title: '세금계산서 미발행/미수취 내역', - columns: [ - { key: 'no', label: 'No.', align: 'center' }, - { key: 'type', label: '구분', align: 'center' }, - { key: 'issueDate', label: '발생일자', align: 'center', format: 'date' }, - { key: 'vendor', label: '거래처', align: 'left' }, - { key: 'vat', label: '부가세', align: 'right', format: 'currency' }, - { key: 'invoiceStatus', label: '세금계산서 미발행/미수취', align: 'center' }, - ], - data: [ - { type: '매출', issueDate: '2025-12-12', vendor: '회사명', vat: 11000000, invoiceStatus: '미발행' }, - { type: '매입', issueDate: '2025-12-12', vendor: '회사명', vat: 11000000, invoiceStatus: '미수취' }, - { type: '매출', issueDate: '2025-12-12', vendor: '회사명', vat: 11000000, invoiceStatus: '미발행' }, - { type: '매입', issueDate: '2025-12-12', vendor: '회사명', vat: 11000000, invoiceStatus: '미수취' }, - { type: '매출', issueDate: '2025-12-12', vendor: '회사명', vat: 11000000, invoiceStatus: '미발행' }, - ], - filters: [ - { - key: 'type', - options: [ - { value: 'all', label: '전체' }, - { value: '매출', label: '매출' }, - { value: '매입', label: '매입' }, - ], - defaultValue: 'all', - }, - ], - showTotal: true, - totalLabel: '합계', - totalValue: 111000000, - totalColumnKey: 'vat', - }, - }; +export function getVatModalConfig(): DetailModalConfig | null { + return null; } diff --git a/src/components/business/CEODashboard/modalConfigs/welfareConfigs.ts b/src/components/business/CEODashboard/modalConfigs/welfareConfigs.ts index 8089411f..af3147c5 100644 --- a/src/components/business/CEODashboard/modalConfigs/welfareConfigs.ts +++ b/src/components/business/CEODashboard/modalConfigs/welfareConfigs.ts @@ -2,149 +2,9 @@ import type { DetailModalConfig } from '../types'; /** * 복리후생비 현황 모달 설정 - * - * @deprecated 정적 목업 데이터 - API 연동 후에는 useWelfareDetail hook 사용 권장 - * - * API 연동 방법: - * 1. useWelfareDetail hook 호출하여 modalConfig 가져오기 - * 2. API 호출 실패 시 이 fallback config 사용 - * - * @example - * const { modalConfig, loading, error, refetch } = useWelfareDetail({ - * calculationType: 'fixed', - * year: 2026, - * quarter: 1, - * }); - * const config = modalConfig ?? getWelfareModalConfig('fixed'); // fallback - * - * @param calculationType - 계산 방식 ('fixed': 직원당 정액 금액/월, 'ratio': 연봉 총액 비율) + * API 연동 완료 — useWelfareDetail hook이 실제 데이터 반환 + * 이 함수는 하위 호환용으로 유지하되 null 반환 */ -export function getWelfareModalConfig(calculationType: 'fixed' | 'ratio'): DetailModalConfig { - // 계산 방식에 따른 조건부 calculationCards 생성 - const calculationCards = calculationType === 'fixed' - ? { - // 직원당 정액 금액/월 방식 - title: '복리후생비 계산', - subtitle: '직원당 정액 금액/월 200,000원', - cards: [ - { label: '직원 수', value: 20, unit: '명' }, - { label: '연간 직원당 월급 금액', value: 2400000, unit: '원', operator: '×' as const }, - { label: '당해년도 복리후생비 총 한도', value: 48000000, unit: '원', operator: '=' as const }, - ], - } - : { - // 연봉 총액 비율 방식 - title: '복리후생비 계산', - subtitle: '연봉 총액 기준 비율 20.5%', - cards: [ - { label: '연봉 총액', value: 1000000000, unit: '원' }, - { label: '비율', value: 20.5, unit: '%', operator: '×' as const }, - { label: '당해년도 복리후생비 총 한도', value: 205000000, unit: '원', operator: '=' as const }, - ], - }; - - return { - title: '복리후생비 상세', - dateFilter: { - enabled: true, - defaultPreset: '당월', - showSearch: true, - }, - summaryCards: [ - // 1행: 당해년도 기준 - { label: '당해년도 복리후생비 총 한도', value: 3123000, unit: '원' }, - { label: '당해년도 복리후생비 잔여한도', value: 6000000, unit: '원' }, - { label: '당해년도 복리후생비 사용금액', value: 6000000, unit: '원' }, - { label: '당해년도 복리후생비 초과 금액', value: 0, unit: '원' }, - ], - reviewCards: { - title: '복리후생비 검토 필요', - cards: [ - { label: '비과세 한도 초과', amount: 3123000, subLabel: '5건' }, - { label: '사적 사용 의심', amount: 3123000, subLabel: '5건' }, - { label: '특정인 편중', amount: 3123000, subLabel: '5건' }, - { label: '항목별 한도 초과', amount: 3123000, subLabel: '5건' }, - ], - }, - barChart: { - title: '월별 복리후생비 사용 추이', - data: [ - { name: '1월', value: 1500000 }, - { name: '2월', value: 1800000 }, - { name: '3월', value: 2200000 }, - { name: '4월', value: 1900000 }, - { name: '5월', value: 2100000 }, - { name: '6월', value: 1700000 }, - ], - dataKey: 'value', - xAxisKey: 'name', - color: '#60A5FA', - }, - pieChart: { - title: '항목별 사용 비율', - data: [ - { name: '식비', value: 55000000, percentage: 55, color: '#FBBF24' }, - { name: '건강검진', value: 25000000, percentage: 5, color: '#60A5FA' }, - { name: '경조사비', value: 10000000, percentage: 10, color: '#F87171' }, - { name: '기타', value: 10000000, percentage: 30, color: '#34D399' }, - ], - }, - table: { - title: '일별 복리후생비 사용 내역', - columns: [ - { key: 'no', label: 'No.', align: 'center' }, - { key: 'cardName', label: '카드명', align: 'left' }, - { key: 'user', label: '사용자', align: 'center' }, - { key: 'date', label: '사용일자', align: 'center', format: 'date' }, - { key: 'store', label: '가맹점명', align: 'left' }, - { key: 'amount', label: '사용금액', align: 'right', format: 'currency' }, - { key: 'content', label: '내용', align: 'left' }, - ], - data: [ - { cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1000000, content: '비과세 한도 초과' }, - { cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1200000, content: '사적 사용 의심' }, - { cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1500000, content: '특정인 편중' }, - { cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1300000, content: '항목별 한도 초과' }, - { cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 6000000, content: '비과세 한도 초과' }, - ], - filters: [ - { - key: 'user', - options: [ - { value: 'all', label: '전체' }, - { value: '홍길동', label: '홍길동' }, - ], - defaultValue: 'all', - }, - { - key: 'content', - options: [ - { value: 'all', label: '전체' }, - { value: '비과세 한도 초과', label: '비과세 한도 초과' }, - { value: '사적 사용 의심', label: '사적 사용 의심' }, - { value: '특정인 편중', label: '특정인 편중' }, - { value: '항목별 한도 초과', label: '항목별 한도 초과' }, - ], - defaultValue: 'all', - }, - ], - showTotal: true, - totalLabel: '합계', - totalValue: 11000000, - totalColumnKey: 'amount', - }, - // 복리후생비 계산 (조건부 - calculationType에 따라) - calculationCards, - // 복리후생비 현황 (분기별 테이블) - quarterlyTable: { - title: '복리후생비 현황', - rows: [ - { label: '한도금액', q1: 12000000, q2: 12000000, q3: 12000000, q4: 12000000, total: 48000000 }, - { label: '이월금액', q1: 0, q2: '', q3: '', q4: '', total: '' }, - { label: '사용금액', q1: 1000000, q2: '', q3: '', q4: '', total: '' }, - { label: '잔여한도', q1: 11000000, q2: '', q3: '', q4: '', total: '' }, - { label: '초과금액', q1: '', q2: '', q3: '', q4: '', total: '' }, - ], - }, - }; +export function getWelfareModalConfig(_calculationType: 'fixed' | 'ratio'): DetailModalConfig | null { + return null; } \ No newline at end of file diff --git a/src/components/business/CEODashboard/modals/DetailModal.tsx b/src/components/business/CEODashboard/modals/DetailModal.tsx index acb80a11..7dce6055 100644 --- a/src/components/business/CEODashboard/modals/DetailModal.tsx +++ b/src/components/business/CEODashboard/modals/DetailModal.tsx @@ -28,9 +28,10 @@ interface DetailModalProps { isOpen: boolean; onClose: () => void; config: DetailModalConfig; + onDateFilterChange?: (params: { startDate: string; endDate: string; search: string }) => void; } -export function DetailModal({ isOpen, onClose, config }: DetailModalProps) { +export function DetailModal({ isOpen, onClose, config, onDateFilterChange }: DetailModalProps) { return ( !open && onClose()} > @@ -51,7 +52,7 @@ export function DetailModal({ isOpen, onClose, config }: DetailModalProps) {
{/* 기간선택기 영역 */} {config.dateFilter?.enabled && ( - + )} {/* 신고기간 셀렉트 영역 */} diff --git a/src/components/business/CEODashboard/modals/DetailModalSections.tsx b/src/components/business/CEODashboard/modals/DetailModalSections.tsx index 0e8d773f..d4c3a227 100644 --- a/src/components/business/CEODashboard/modals/DetailModalSections.tsx +++ b/src/components/business/CEODashboard/modals/DetailModalSections.tsx @@ -1,7 +1,8 @@ 'use client'; -import { useState, useCallback, useMemo } from 'react'; -import { Search } from 'lucide-react'; +import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; +import { Search as SearchIcon } from 'lucide-react'; +import { Button } from '@/components/ui/button'; import { Select, SelectContent, @@ -46,7 +47,7 @@ import type { // 필터 섹션 // ============================================ -export const DateFilterSection = ({ config }: { config: DateFilterConfig }) => { +export const DateFilterSection = ({ config, onFilterChange }: { config: DateFilterConfig; onFilterChange?: (params: { startDate: string; endDate: string; search: string }) => void }) => { const today = new Date(); const [startDate, setStartDate] = useState(() => { const d = new Date(today.getFullYear(), today.getMonth(), 1); @@ -57,6 +58,24 @@ export const DateFilterSection = ({ config }: { config: DateFilterConfig }) => { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }); const [searchText, setSearchText] = useState(''); + const isInitialMount = useRef(true); + + // 날짜 변경 시 자동 조회 (다른 페이지와 동일한 UX) + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + return; + } + onFilterChange?.({ startDate, endDate, search: searchText }); + }, [startDate, endDate]); + + const handleSearch = useCallback(() => { + onFilterChange?.({ startDate, endDate, search: searchText }); + }, [startDate, endDate, searchText, onFilterChange]); + + const handleSearchKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter') handleSearch(); + }, [handleSearch]); return (
@@ -66,17 +85,20 @@ export const DateFilterSection = ({ config }: { config: DateFilterConfig }) => { onStartDateChange={setStartDate} onEndDateChange={setEndDate} extraActions={ - config.showSearch !== false ? ( -
- - setSearchText(e.target.value)} - placeholder="검색" - className="h-8 pl-7 pr-3 text-xs w-[140px]" - /> -
- ) : undefined +
+ {config.showSearch !== false && ( +
+ + setSearchText(e.target.value)} + onKeyDown={handleSearchKeyDown} + placeholder="검색" + className="h-8 pl-7 pr-3 text-xs w-[140px]" + /> +
+ )} +
} />
@@ -86,10 +108,15 @@ export const DateFilterSection = ({ config }: { config: DateFilterConfig }) => { export const PeriodSelectSection = ({ config }: { config: PeriodSelectConfig }) => { const [selected, setSelected] = useState(config.defaultValue || config.options[0]?.value || ''); + const handleChange = useCallback((value: string) => { + setSelected(value); + config.onPeriodChange?.(value); + }, [config]); + return (
신고기간 - diff --git a/src/components/business/CEODashboard/modals/ScheduleDetailModal.tsx b/src/components/business/CEODashboard/modals/ScheduleDetailModal.tsx index 976d385c..7131661f 100644 --- a/src/components/business/CEODashboard/modals/ScheduleDetailModal.tsx +++ b/src/components/business/CEODashboard/modals/ScheduleDetailModal.tsx @@ -6,7 +6,7 @@ import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Checkbox } from '@/components/ui/checkbox'; import { TimePicker } from '@/components/ui/time-picker'; -import { DatePicker } from '@/components/ui/date-picker'; +import { DateRangePicker } from '@/components/ui/date-range-picker'; import { Dialog, DialogContent, @@ -21,6 +21,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { Badge } from '@/components/ui/badge'; import type { CalendarScheduleItem } from '../types'; // 색상 옵션 @@ -59,6 +60,7 @@ interface ScheduleDetailModalProps { schedule: CalendarScheduleItem | null; onSave: (data: ScheduleFormData) => void; onDelete?: (id: string) => void; + isEditable?: boolean; } export function ScheduleDetailModal({ @@ -67,6 +69,7 @@ export function ScheduleDetailModal({ schedule, onSave, onDelete, + isEditable = true, }: ScheduleDetailModalProps) { const isEditMode = schedule && schedule.id !== ''; @@ -128,7 +131,14 @@ export function ScheduleDetailModal({ !open && handleCancel()}> - 일정 상세 + + 일정 상세 + {!isEditable && ( + + 읽기전용 + + )} +
@@ -139,6 +149,7 @@ export function ScheduleDetailModal({ value={formData.title} onChange={(e) => handleFieldChange('title', e.target.value)} placeholder="제목" + disabled={!isEditable} />
@@ -148,6 +159,7 @@ export function ScheduleDetailModal({