diff --git a/src/components/approval/DocumentCreate/index.tsx b/src/components/approval/DocumentCreate/index.tsx index f7335658..5cad414f 100644 --- a/src/components/approval/DocumentCreate/index.tsx +++ b/src/components/approval/DocumentCreate/index.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useCallback, useEffect, useTransition, useRef, useMemo } from 'react'; +import { invalidateDashboard } from '@/lib/dashboard-invalidation'; import { useRouter, useSearchParams } from 'next/navigation'; import { usePermission } from '@/hooks/usePermission'; import { format } from 'date-fns'; @@ -50,7 +51,7 @@ import { getClients } from '@/components/accounting/VendorManagement/actions'; // 초기 데이터 - SSR에서는 빈 문자열, 클라이언트에서 날짜 설정 const getInitialBasicInfo = (): BasicInfo => ({ - drafter: '홍길동', + drafter: '', // 클라이언트에서 currentUser로 설정 draftDate: '', // 클라이언트에서 설정 documentNo: '', documentType: 'proposal', @@ -118,14 +119,22 @@ export function DocumentCreate() { const today = format(new Date(), 'yyyy-MM-dd'); const now = format(new Date(), 'yyyy-MM-dd HH:mm'); - setBasicInfo(prev => ({ ...prev, draftDate: prev.draftDate || now })); + // localStorage 'user' 키에서 사용자 이름 가져오기 (로그인 시 저장됨) + const userDataStr = typeof window !== 'undefined' ? localStorage.getItem('user') : null; + const userName = userDataStr ? JSON.parse(userDataStr).name : currentUser?.name || ''; + + setBasicInfo(prev => ({ + ...prev, + drafter: prev.drafter || userName, + draftDate: prev.draftDate || now, + })); setProposalData(prev => ({ ...prev, vendorPaymentDate: prev.vendorPaymentDate || today })); setExpenseReportData(prev => ({ ...prev, requestDate: prev.requestDate || today, paymentDate: prev.paymentDate || today, })); - }, []); + }, [currentUser?.name]); // 미리보기 모달 상태 const [isPreviewOpen, setIsPreviewOpen] = useState(false); @@ -172,6 +181,7 @@ export function DocumentCreate() { setBasicInfo(prev => ({ ...prev, ...mockData.basicInfo, + drafter: currentUserName || prev.drafter, draftDate: prev.draftDate || mockData.basicInfo.draftDate, documentType: (mockData.basicInfo.documentType || prev.documentType) as BasicInfo['documentType'], })); @@ -343,6 +353,7 @@ export function DocumentCreate() { try { const result = await deleteApproval(parseInt(documentId)); if (result.success) { + invalidateDashboard('approval'); toast.success('문서가 삭제되었습니다.'); router.back(); } else { @@ -375,6 +386,7 @@ export function DocumentCreate() { if (isEditMode && documentId) { const result = await updateAndSubmitApproval(parseInt(documentId), formData); if (result.success) { + invalidateDashboard('approval'); toast.success('수정 및 상신 완료', { description: `문서번호: ${result.data?.documentNo}`, }); @@ -386,6 +398,7 @@ export function DocumentCreate() { // 새 문서: 생성 후 상신 const result = await createAndSubmitApproval(formData); if (result.success) { + invalidateDashboard('approval'); toast.success('상신 완료', { description: `문서번호: ${result.data?.documentNo}`, }); @@ -411,6 +424,7 @@ export function DocumentCreate() { if (isEditMode && documentId) { const result = await updateApproval(parseInt(documentId), formData); if (result.success) { + invalidateDashboard('approval'); toast.success('저장 완료', { description: `문서번호: ${result.data?.documentNo}`, }); @@ -421,6 +435,7 @@ export function DocumentCreate() { // 새 문서: 임시저장 const result = await createApproval(formData); if (result.success) { + invalidateDashboard('approval'); toast.success('임시저장 완료', { description: `문서번호: ${result.data?.documentNo}`, }); diff --git a/src/components/approval/DraftBox/index.tsx b/src/components/approval/DraftBox/index.tsx index 1fe37dad..7ff81c26 100644 --- a/src/components/approval/DraftBox/index.tsx +++ b/src/components/approval/DraftBox/index.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useMemo, useCallback, useEffect, useTransition, useRef } from 'react'; +import { invalidateDashboard } from '@/lib/dashboard-invalidation'; import { useRouter } from 'next/navigation'; import { useDateRange } from '@/hooks'; import { @@ -175,6 +176,7 @@ export function DraftBox() { try { const result = await submitDrafts(ids); if (result.success) { + invalidateDashboard('approval'); toast.success(`${ids.length}건의 문서를 상신했습니다.`); loadData(); loadSummary(); @@ -200,6 +202,7 @@ export function DraftBox() { try { const result = await deleteDrafts(ids); if (result.success) { + invalidateDashboard('approval'); toast.success(`${ids.length}건의 문서를 삭제했습니다.`); loadData(); loadSummary(); @@ -222,6 +225,7 @@ export function DraftBox() { try { const result = await deleteDraft(id); if (result.success) { + invalidateDashboard('approval'); toast.success('문서를 삭제했습니다.'); loadData(); loadSummary(); @@ -298,6 +302,7 @@ export function DraftBox() { try { const result = await submitDraft(selectedDocument.id); if (result.success) { + invalidateDashboard('approval'); toast.success('문서를 상신했습니다.'); setIsModalOpen(false); setSelectedDocument(null); diff --git a/src/components/auth/LoginPage.tsx b/src/components/auth/LoginPage.tsx index 24882ab9..aab81288 100644 --- a/src/components/auth/LoginPage.tsx +++ b/src/components/auth/LoginPage.tsx @@ -4,6 +4,7 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import Image from "next/image"; +import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -268,7 +269,11 @@ export function LoginPage() { /> {t('rememberMe')} - diff --git a/src/components/business/CEODashboard/CEODashboard.tsx b/src/components/business/CEODashboard/CEODashboard.tsx index 8ee91867..240e8a32 100644 --- a/src/components/business/CEODashboard/CEODashboard.tsx +++ b/src/components/business/CEODashboard/CEODashboard.tsx @@ -41,6 +41,7 @@ import { useCardManagementModals } from '@/hooks/useCardManagementModals'; import { getCardManagementModalConfigWithData } from './modalConfigs'; import { transformEntertainmentDetailResponse, transformWelfareDetailResponse, transformVatDetailResponse } from '@/lib/api/dashboard/transformers'; import { toast } from 'sonner'; +import { consumeStaleSections, DASHBOARD_INVALIDATE_EVENT, type DashboardSectionKey } from '@/lib/dashboard-invalidation'; export function CEODashboard() { const router = useRouter(); @@ -70,6 +71,27 @@ export function CEODashboard() { // Welfare API Hook (Phase 2) const welfareData = useWelfare(); + // 대시보드 targeted refetch: CUD 후 stale 섹션만 갱신 + useEffect(() => { + const refetchSection = (key: string) => { + if (key === 'entertainment') entertainmentData.refetch(); + else if (key === 'welfare') welfareData.refetch(); + else apiData.refetchMap[key as DashboardSectionKey]?.(); + }; + const stale = consumeStaleSections(); + if (stale.length > 0) { + for (const key of stale) refetchSection(key); + } + const handler = (e: Event) => { + const sections = (e as CustomEvent).detail?.sections as string[] | undefined; + if (sections) { + for (const key of sections) refetchSection(key); + } + }; + window.addEventListener(DASHBOARD_INVALIDATE_EVENT, handler); + return () => window.removeEventListener(DASHBOARD_INVALIDATE_EVENT, handler); + }, [apiData.refetchMap, entertainmentData.refetch, welfareData.refetch]); + // Card Management Modal API Hook (Phase 3) const cardManagementModals = useCardManagementModals(); diff --git a/src/components/business/CEODashboard/sections/PurchaseStatusSection.tsx b/src/components/business/CEODashboard/sections/PurchaseStatusSection.tsx index 713bcfa3..aa276257 100644 --- a/src/components/business/CEODashboard/sections/PurchaseStatusSection.tsx +++ b/src/components/business/CEODashboard/sections/PurchaseStatusSection.tsx @@ -47,12 +47,7 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) { } title="매입 현황" - subtitle="당월 매입 실적" - rightElement={ - - 당월 - - } + subtitle="매입 실적" > {/* 통계카드 3개 - 가로 배치 */}
@@ -158,8 +153,8 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) { {/* 당월 매입 내역 (별도 카드) */} } - title="당월 매입 내역" - subtitle="당월 매입 거래 상세" + title="최근 매입 내역" + subtitle="매입 거래 상세" bodyClassName="p-0" >
diff --git a/src/components/business/CEODashboard/types.ts b/src/components/business/CEODashboard/types.ts index 6405def2..80434976 100644 --- a/src/components/business/CEODashboard/types.ts +++ b/src/components/business/CEODashboard/types.ts @@ -725,13 +725,13 @@ export const DEFAULT_DASHBOARD_SETTINGS: DashboardSettings = { orders: true, debtCollection: true, safetyStock: true, - taxReport: false, - newVendor: false, + taxReport: true, + newVendor: true, annualLeave: true, vehicle: false, equipment: false, purchase: false, - approvalRequest: false, + approvalRequest: true, fundStatus: true, }, }, @@ -774,13 +774,13 @@ export const DEFAULT_DASHBOARD_SETTINGS: DashboardSettings = { orders: true, debtCollection: true, safetyStock: true, - taxReport: false, - newVendor: false, + taxReport: true, + newVendor: true, annualLeave: true, vehicle: false, equipment: false, purchase: false, - approvalRequest: false, + approvalRequest: true, fundStatus: true, }, }, diff --git a/src/components/business/construction/management/ConstructionDetailClient.tsx b/src/components/business/construction/management/ConstructionDetailClient.tsx index 986e6321..02df109a 100644 --- a/src/components/business/construction/management/ConstructionDetailClient.tsx +++ b/src/components/business/construction/management/ConstructionDetailClient.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useEffect, useCallback, useMemo } from 'react'; +import { invalidateDashboard } from '@/lib/dashboard-invalidation'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/navigation'; import { getTodayString, formatDate } from '@/lib/utils/date'; @@ -243,6 +244,7 @@ export default function ConstructionDetailClient({ id, mode }: ConstructionDetai try { const result = await updateConstructionManagementDetail(id, formData); if (result.success) { + invalidateDashboard('construction'); toast.success('저장되었습니다.'); return { success: true }; } else { @@ -265,6 +267,7 @@ export default function ConstructionDetailClient({ id, mode }: ConstructionDetai try { const result = await completeConstruction(id); if (result.success) { + invalidateDashboard('construction'); toast.success('시공이 완료되었습니다.'); router.push('/ko/construction/project/construction-management'); } else { diff --git a/src/components/hr/AttendanceManagement/index.tsx b/src/components/hr/AttendanceManagement/index.tsx index 30fe53e7..7e01e403 100644 --- a/src/components/hr/AttendanceManagement/index.tsx +++ b/src/components/hr/AttendanceManagement/index.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; +import { invalidateDashboard } from '@/lib/dashboard-invalidation'; import { useRouter } from 'next/navigation'; import { Clock, @@ -310,6 +311,7 @@ export function AttendanceManagement() { if (attendanceDialogMode === 'create') { const result = await createAttendance(data); if (result.success && result.data) { + invalidateDashboard('attendance'); setAttendanceRecords(prev => [result.data!, ...prev]); } else { console.error('Create failed:', result.error); @@ -317,6 +319,7 @@ export function AttendanceManagement() { } else if (selectedAttendance) { const result = await updateAttendance(selectedAttendance.id, data); if (result.success && result.data) { + invalidateDashboard('attendance'); setAttendanceRecords(prev => prev.map(r => r.id === selectedAttendance.id ? result.data! : r) ); diff --git a/src/components/hr/VacationManagement/index.tsx b/src/components/hr/VacationManagement/index.tsx index f67e9eb1..5276d9e1 100644 --- a/src/components/hr/VacationManagement/index.tsx +++ b/src/components/hr/VacationManagement/index.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; +import { invalidateDashboard } from '@/lib/dashboard-invalidation'; import { format } from 'date-fns'; import { useDateRange } from '@/hooks'; import { @@ -312,6 +313,7 @@ export function VacationManagement() { const ids = Array.from(selectedItems).map((id) => parseInt(id, 10)); const result = await approveLeavesMany(ids); if (result.success) { + invalidateDashboard('leave'); await fetchLeaveRequests(); await fetchUsageData(); // 휴가 사용현황도 갱신 } else { @@ -340,6 +342,7 @@ export function VacationManagement() { const ids = Array.from(selectedItems).map((id) => parseInt(id, 10)); const result = await rejectLeavesMany(ids, '관리자에 의해 반려됨'); if (result.success) { + invalidateDashboard('leave'); await fetchLeaveRequests(); } else { console.error('[VacationManagement] 반려 실패:', result.error); @@ -750,6 +753,7 @@ export function VacationManagement() { reason: data.reason, }); if (result.success) { + invalidateDashboard('leave'); await fetchGrantData(); await fetchUsageData(); } else { @@ -780,6 +784,7 @@ export function VacationManagement() { days: data.vacationDays, }); if (result.success) { + invalidateDashboard('leave'); await fetchLeaveRequests(); await fetchUsageData(); } else { diff --git a/src/components/layout/HeaderFavoritesBar.tsx b/src/components/layout/HeaderFavoritesBar.tsx index d4421391..9600e0aa 100644 --- a/src/components/layout/HeaderFavoritesBar.tsx +++ b/src/components/layout/HeaderFavoritesBar.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { Bookmark, MoreHorizontal } from 'lucide-react'; +import { Pin, MoreHorizontal } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Tooltip, @@ -51,7 +51,7 @@ function StarDropdown({ className={`p-0 rounded-xl bg-blue-600 hover:bg-blue-700 text-white flex items-center justify-center ${className ?? 'w-10 h-10'}`} title="즐겨찾기" > - + diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index fe7212f7..c065ba54 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,4 +1,4 @@ -import { Bookmark, ChevronRight, ChevronsDownUp, ChevronsUpDown, Circle } from 'lucide-react'; +import { Pin, ChevronRight, ChevronsDownUp, ChevronsUpDown, Circle } from 'lucide-react'; import type { MenuItem } from '@/stores/menuStore'; import { useEffect, useRef, useCallback } from 'react'; import { useFavoritesStore, MAX_FAVORITES } from '@/stores/favoritesStore'; @@ -159,7 +159,7 @@ function MenuItemComponent({ }`} title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'} > - + )}
@@ -224,7 +224,7 @@ function MenuItemComponent({ }`} title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'} > - + )}
@@ -291,7 +291,7 @@ function MenuItemComponent({ }`} title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'} > - + )} diff --git a/src/hooks/useCEODashboard.ts b/src/hooks/useCEODashboard.ts index 3e83a3fd..f2d1611d 100644 --- a/src/hooks/useCEODashboard.ts +++ b/src/hooks/useCEODashboard.ts @@ -59,6 +59,8 @@ import { transformDailyAttendanceResponse, } from '@/lib/api/dashboard/transformers'; +import type { DashboardSectionKey } from '@/lib/dashboard-invalidation'; + import type { DailyReportData, ReceivableData, @@ -664,6 +666,7 @@ export interface CEODashboardState { construction: SectionState; dailyAttendance: SectionState; refetchAll: () => void; + refetchMap: Partial void>>; } export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashboardState { @@ -782,6 +785,22 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo // eslint-disable-next-line react-hooks/exhaustive-deps }, [dr.refetch, rv.refetch, dc.refetch, me.refetch, fetchCM, sb.refetch, ss.refetch, ps.refetch, dp.refetch, us.refetch, cs.refetch, da.refetch]); + // 섹션별 refetch 함수 맵 (targeted invalidation용) + const refetchMap = useMemo void>>>(() => ({ + dailyReport: dr.refetch, + receivable: rv.refetch, + debtCollection: dc.refetch, + monthlyExpense: me.refetch, + cardManagement: fetchCM, + statusBoard: sb.refetch, + salesStatus: ss.refetch, + purchaseStatus: ps.refetch, + dailyProduction: dp.refetch, + unshipped: us.refetch, + construction: cs.refetch, + dailyAttendance: da.refetch, + }), [dr.refetch, rv.refetch, dc.refetch, me.refetch, fetchCM, sb.refetch, ss.refetch, ps.refetch, dp.refetch, us.refetch, cs.refetch, da.refetch]); + return { dailyReport: { data: dr.data, loading: dr.loading, error: dr.error }, receivable: { data: rv.data, loading: rv.loading, error: rv.error }, @@ -796,5 +815,6 @@ export function useCEODashboard(options: UseCEODashboardOptions = {}): CEODashbo construction: { data: cs.data, loading: cs.loading, error: cs.error }, dailyAttendance: { data: da.data, loading: da.loading, error: da.error }, refetchAll, + refetchMap, }; } \ No newline at end of file diff --git a/src/lib/api/dashboard/transformers/receivable.ts b/src/lib/api/dashboard/transformers/receivable.ts index ec77c1d7..63d48625 100644 --- a/src/lib/api/dashboard/transformers/receivable.ts +++ b/src/lib/api/dashboard/transformers/receivable.ts @@ -93,31 +93,11 @@ function generateDebtCollectionCheckPoints(api: BadDebtApiResponse): CheckPoint[ return checkPoints; } -// TODO: 백엔드 per-card sub_label/count 제공 시 더미값 제거 -// 채권추심 카드별 더미 서브라벨 (회사명 + 건수) -const DEBT_COLLECTION_FALLBACK_SUB_LABELS: Record = { - dc1: { company: '(주)부산화학 외', count: 5 }, - dc2: { company: '(주)삼성테크 외', count: 3 }, - dc3: { company: '(주)대한전자 외', count: 2 }, - dc4: { company: '(주)한국정밀 외', count: 3 }, -}; - /** - * 채권추심 subLabel 생성 헬퍼 - * dc1(누적)은 API client_count 사용, 나머지는 더미값 + * 채권추심 subLabel: 백엔드 sub_labels 필드 직접 사용 */ -function buildDebtSubLabel(cardId: string, clientCount?: number): string | undefined { - const fallback = DEBT_COLLECTION_FALLBACK_SUB_LABELS[cardId]; - if (!fallback) return undefined; - - const count = cardId === 'dc1' && clientCount !== undefined ? clientCount : fallback.count; - if (count <= 0) return undefined; - - const remaining = count - 1; - if (remaining > 0) { - return `${fallback.company} ${remaining}건`; - } - return fallback.company.replace(/ 외$/, ''); +function buildDebtSubLabel(cardId: string, subLabels?: Record): string | undefined { + return subLabels?.[cardId] || undefined; } /** @@ -130,25 +110,25 @@ export function transformDebtCollectionResponse(api: BadDebtApiResponse): DebtCo id: 'dc1', label: '누적 악성채권', amount: api.total_amount, - subLabel: buildDebtSubLabel('dc1', api.client_count), + subLabel: buildDebtSubLabel('dc1', api.sub_labels), }, { id: 'dc2', label: '추심중', amount: api.collecting_amount, - subLabel: buildDebtSubLabel('dc2'), + subLabel: buildDebtSubLabel('dc2', api.sub_labels), }, { id: 'dc3', label: '법적조치', amount: api.legal_action_amount, - subLabel: buildDebtSubLabel('dc3'), + subLabel: buildDebtSubLabel('dc3', api.sub_labels), }, { id: 'dc4', label: '회수완료', amount: api.recovered_amount, - subLabel: buildDebtSubLabel('dc4'), + subLabel: buildDebtSubLabel('dc4', api.sub_labels), }, ], checkPoints: generateDebtCollectionCheckPoints(api), diff --git a/src/lib/api/dashboard/transformers/status-issue.ts b/src/lib/api/dashboard/transformers/status-issue.ts index d1192839..b61b434d 100644 --- a/src/lib/api/dashboard/transformers/status-issue.ts +++ b/src/lib/api/dashboard/transformers/status-issue.ts @@ -14,42 +14,26 @@ import { normalizePath } from './common'; // ============================================ // 현황판 (StatusBoard) // ============================================ - -// TODO: 백엔드 sub_label 필드 제공 시 더미값 제거 -// API id 기준: orders, bad_debts, safety_stock, tax_deadline, new_clients, leaves, purchases, approvals -const STATUS_BOARD_FALLBACK_SUB_LABELS: Record = { - orders: '(주)삼성전자 외', - bad_debts: '주식회사 부산화학 외', - safety_stock: '', - tax_deadline: '', - new_clients: '대한철강 외', - leaves: '', - // purchases: '(유)한국정밀 외', // [2026-03-03] 비활성화 — 백엔드 path 오류 + 데이터 정합성 이슈 (N4 참조) - approvals: '구매 결재 외', -}; +// +// [대시보드 vs 원본 페이지 쿼리 조건 차이 — 건수 불일치는 버그 아님] +// +// | 항목 | 대시보드 조건 | 원본 페이지 | +// |----------------|---------------------------------------------------|------------------------------------------| +// | 수주 현황 | 오늘 날짜 + status=confirmed만 | /sales/order-management-sales (전체 기간) | +// | 채권 추심 | status=collecting + is_active=true만 | /accounting/bad-debt-collection (전체) | +// | 안전 재고 | safety_stock>0 && stock_qty 0) { - return `${fallback} ${remaining}건`; - } - // 1건이면 "외" 제거하고 이름만 - return fallback.replace(/ 외$/, ''); +function buildStatusSubLabel(item: { sub_label?: string }): string | undefined { + return item.sub_label || undefined; } /** diff --git a/src/lib/api/dashboard/types.ts b/src/lib/api/dashboard/types.ts index 8b9c5cb8..4b5791b2 100644 --- a/src/lib/api/dashboard/types.ts +++ b/src/lib/api/dashboard/types.ts @@ -107,6 +107,7 @@ export interface BadDebtApiResponse { recovered_amount: number; // 회수완료 bad_debt_amount: number; // 대손처리 client_count?: number; // 거래처 수 + sub_labels?: Record; // 카드별 거래처 sub_label (dc1~dc4) } // ============================================ diff --git a/src/lib/api/shared-lookups.ts b/src/lib/api/shared-lookups.ts index b3aee95f..f01369b9 100644 --- a/src/lib/api/shared-lookups.ts +++ b/src/lib/api/shared-lookups.ts @@ -42,7 +42,7 @@ function extractArray(data: PaginatedOrArray): T[] { export async function fetchVendorOptions(): Promise> { const API_URL = process.env.NEXT_PUBLIC_API_URL; const result = await executeServerAction({ - url: `${API_URL}/api/v1/clients?per_page=100`, + url: `${API_URL}/api/v1/clients?size=1000`, transform: (data: PaginatedOrArray) => { const clients = extractArray(data); return clients.map(c => ({ id: String(c.id), name: c.name })); diff --git a/src/lib/dashboard-invalidation.ts b/src/lib/dashboard-invalidation.ts new file mode 100644 index 00000000..6ce78414 --- /dev/null +++ b/src/lib/dashboard-invalidation.ts @@ -0,0 +1,111 @@ +/** + * CEO 대시보드 targeted refetch 시스템 + * + * CUD 발생 시 sessionStorage + CustomEvent로 대시보드 섹션별 갱신 트리거 + */ + +// 대시보드 섹션 키 (useCEODashboard의 refetchMap과 1:1 매핑) +export type DashboardSectionKey = + | 'dailyReport' + | 'receivable' + | 'debtCollection' + | 'monthlyExpense' + | 'cardManagement' + | 'statusBoard' + | 'salesStatus' + | 'purchaseStatus' + | 'dailyProduction' + | 'unshipped' + | 'construction' + | 'dailyAttendance' + | 'entertainment' + | 'welfare'; + +// CUD 도메인 → 영향받는 대시보드 섹션 매핑 +type DomainKey = + | 'deposit' + | 'withdrawal' + | 'sales' + | 'purchase' + | 'badDebt' + | 'expectedExpense' + | 'bill' + | 'giftCertificate' + | 'journalEntry' + | 'order' + | 'stock' + | 'schedule' + | 'client' + | 'leave' + | 'approval' + | 'attendance' + | 'production' + | 'shipment' + | 'construction'; + +const DOMAIN_SECTION_MAP: Record = { + deposit: ['dailyReport', 'receivable'], + withdrawal: ['dailyReport', 'monthlyExpense'], + sales: ['dailyReport', 'salesStatus', 'receivable'], + purchase: ['dailyReport', 'purchaseStatus', 'monthlyExpense'], + badDebt: ['debtCollection', 'receivable'], + expectedExpense: ['monthlyExpense'], + bill: ['dailyReport', 'receivable'], + giftCertificate: ['entertainment', 'cardManagement'], + journalEntry: ['entertainment', 'welfare', 'monthlyExpense'], + order: ['statusBoard', 'salesStatus'], + stock: ['statusBoard'], + schedule: ['statusBoard'], + client: ['statusBoard'], + leave: ['statusBoard', 'dailyAttendance'], + approval: ['statusBoard'], + attendance: ['statusBoard', 'dailyAttendance'], + production: ['statusBoard', 'dailyProduction'], + shipment: ['statusBoard', 'unshipped'], + construction: ['statusBoard', 'construction'], +}; + +const STORAGE_KEY = 'dashboard:stale-sections'; +const EVENT_NAME = 'dashboard:invalidate'; + +/** + * CUD 성공 후 호출 — 해당 도메인이 영향 주는 대시보드 섹션을 stale 처리 + */ +export function invalidateDashboard(domain: DomainKey): void { + const sections = DOMAIN_SECTION_MAP[domain]; + if (!sections || sections.length === 0) return; + + // 1. sessionStorage에 stale 섹션 저장 (navigation 사이 유지) + try { + const existing = sessionStorage.getItem(STORAGE_KEY); + const current: string[] = existing ? JSON.parse(existing) : []; + const merged = Array.from(new Set([...current, ...sections])); + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(merged)); + } catch { + // sessionStorage 접근 불가 시 무시 + } + + // 2. CustomEvent 발행 (대시보드가 마운트 중이면 즉시 처리) + if (typeof window !== 'undefined') { + window.dispatchEvent( + new CustomEvent(EVENT_NAME, { detail: { sections } }), + ); + } +} + +/** + * 대시보드 마운트 시 호출 — stale 섹션 읽고 클리어 + */ +export function consumeStaleSections(): DashboardSectionKey[] { + try { + const raw = sessionStorage.getItem(STORAGE_KEY); + if (!raw) return []; + sessionStorage.removeItem(STORAGE_KEY); + return JSON.parse(raw) as DashboardSectionKey[]; + } catch { + return []; + } +} + +/** CustomEvent 이름 (리스너 등록용) */ +export const DASHBOARD_INVALIDATE_EVENT = EVENT_NAME;