From a99c3b3908de033c1a30c1ce368b62837fba103d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Wed, 18 Mar 2026 14:40:28 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20[Phase=200]=20=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=E2=86=92=ED=85=8C=EB=84=8C=ED=8A=B8=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=ED=95=B4=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - InspectionReportModal/WorkLogModal/AssigneeSelectModal → document-system/modals/ dynamic import 래퍼 - ProductionOrders 타입/액션 → lib/api/production-orders/ 공유 영역 분리 - 결재(ApprovalBox), 품질(QMS), 영업(production-orders) import 경로 수정 - 하드코딩 경로 /production/work-orders → 영업 내부 경로로 변경 - dashboard-invalidation DomainKey 하드코딩 → registerDashboardDomain() 동적 레지스트리 공통 ERP에서 테넌트(생산) 직접 import 0건 달성 Co-Authored-By: Claude Opus 4.6 --- .../[locale]/(protected)/quality/qms/page.tsx | 3 +- .../[id]/production-order/page.tsx | 2 +- .../production-orders/[id]/page.tsx | 8 +- .../production-orders/page.tsx | 4 +- src/components/approval/ApprovalBox/index.tsx | 2 +- .../modals/AssigneeSelectModal.tsx | 29 ++++++ .../modals/InspectionReportModal.tsx | 29 ++++++ .../document-system/modals/WorkLogModal.tsx | 29 ++++++ .../document-system/modals/index.ts | 9 ++ src/lib/api/production-orders/actions.ts | 27 ++++++ src/lib/api/production-orders/index.ts | 22 +++++ src/lib/api/production-orders/types.ts | 22 +++++ src/lib/dashboard-invalidation.ts | 89 ++++++++++--------- 13 files changed, 221 insertions(+), 54 deletions(-) create mode 100644 src/components/document-system/modals/AssigneeSelectModal.tsx create mode 100644 src/components/document-system/modals/InspectionReportModal.tsx create mode 100644 src/components/document-system/modals/WorkLogModal.tsx create mode 100644 src/components/document-system/modals/index.ts create mode 100644 src/lib/api/production-orders/actions.ts create mode 100644 src/lib/api/production-orders/index.ts create mode 100644 src/lib/api/production-orders/types.ts diff --git a/src/app/[locale]/(protected)/quality/qms/page.tsx b/src/app/[locale]/(protected)/quality/qms/page.tsx index dece05ae..af0c5ac8 100644 --- a/src/app/[locale]/(protected)/quality/qms/page.tsx +++ b/src/app/[locale]/(protected)/quality/qms/page.tsx @@ -8,8 +8,7 @@ import { ReportList } from './components/ReportList'; import { RouteList } from './components/RouteList'; import { DocumentList } from './components/DocumentList'; import { InspectionModal } from './components/InspectionModal'; -import { InspectionReportModal } from '@/components/production/WorkOrders/documents'; -import { WorkLogModal } from '@/components/production/WorkOrders/documents'; +import { InspectionReportModal, WorkLogModal } from '@/components/document-system/modals'; import { ProductInspectionViewModal } from '@/components/quality/InspectionManagement/ProductInspectionViewModal'; import { getDocumentDetail } from './actions'; import { DayTabs } from './components/DayTabs'; diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx index c32ba1bf..b7e20aba 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx @@ -29,7 +29,7 @@ import { import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Factory, ArrowLeft, BarChart3, CheckCircle2, AlertCircle, User } from "lucide-react"; -import { AssigneeSelectModal } from "@/components/production/WorkOrders/AssigneeSelectModal"; +import { AssigneeSelectModal } from "@/components/document-system/modals"; import { PageLayout } from "@/components/organisms/PageLayout"; import { AlertDialog, diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx index c400eaf3..0e0c97f8 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx @@ -48,13 +48,13 @@ import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { toast } from "sonner"; import { ServerErrorPage } from "@/components/common/ServerErrorPage"; import { formatNumber } from '@/lib/utils/amount'; -import { getProductionOrderDetail } from "@/components/production/ProductionOrders/actions"; +import { getProductionOrderDetail } from "@/lib/api/production-orders"; import { createProductionOrder } from "@/components/orders/actions"; import type { ProductionOrderDetail, ProductionStatus, ProductionWorkOrder, -} from "@/components/production/ProductionOrders/types"; +} from "@/lib/api/production-orders"; // 공정 진행 현황 컴포넌트 function ProcessProgress({ workOrders }: { workOrders: ProductionWorkOrder[] }) { @@ -243,8 +243,8 @@ export default function ProductionOrderDetailPage() { const handleSuccessDialogClose = () => { setIsSuccessDialogOpen(false); - // 작업지시 관리 페이지로 이동 - router.push("/production/work-orders"); + // 생산지시 목록으로 이동 (공통 ERP 영역 내 경로) + router.push("/sales/order-management-sales/production-orders"); }; if (loading) { diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx index c2214458..da143646 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx @@ -37,12 +37,12 @@ import { ListMobileCard, InfoField } from "@/components/organisms/MobileCard"; import { getProductionOrders, getProductionOrderStats, -} from "@/components/production/ProductionOrders/actions"; +} from "@/lib/api/production-orders"; import type { ProductionOrder, ProductionStatus, ProductionOrderStats, -} from "@/components/production/ProductionOrders/types"; +} from "@/lib/api/production-orders"; import { formatNumber } from '@/lib/utils/amount'; // 진행 단계 컴포넌트 diff --git a/src/components/approval/ApprovalBox/index.tsx b/src/components/approval/ApprovalBox/index.tsx index 3556c4db..7efc9bdd 100644 --- a/src/components/approval/ApprovalBox/index.tsx +++ b/src/components/approval/ApprovalBox/index.tsx @@ -73,7 +73,7 @@ import { } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { usePermission } from '@/hooks/usePermission'; -import { InspectionReportModal } from '@/components/production/WorkOrders/documents/InspectionReportModal'; +import { InspectionReportModal } from '@/components/document-system/modals'; export function ApprovalBox() { const router = useRouter(); diff --git a/src/components/document-system/modals/AssigneeSelectModal.tsx b/src/components/document-system/modals/AssigneeSelectModal.tsx new file mode 100644 index 00000000..0df27bea --- /dev/null +++ b/src/components/document-system/modals/AssigneeSelectModal.tsx @@ -0,0 +1,29 @@ +'use client'; + +/** + * AssigneeSelectModal — 공유 래퍼 + * + * 원본: @/components/production/WorkOrders/AssigneeSelectModal + * 목적: 공통 ERP(영업)에서 생산 모듈을 직접 import하지 않도록 + * dynamic import로 정적 의존성 체인을 끊음 + */ + +import dynamic from 'next/dynamic'; +import { Loader2 } from 'lucide-react'; + +const AssigneeSelectModalImpl = dynamic( + () => + import('@/components/production/WorkOrders/AssigneeSelectModal').then( + (mod) => mod.AssigneeSelectModal, + ), + { + loading: () => ( +
+ +
+ ), + ssr: false, + }, +); + +export { AssigneeSelectModalImpl as AssigneeSelectModal }; diff --git a/src/components/document-system/modals/InspectionReportModal.tsx b/src/components/document-system/modals/InspectionReportModal.tsx new file mode 100644 index 00000000..bad3100f --- /dev/null +++ b/src/components/document-system/modals/InspectionReportModal.tsx @@ -0,0 +1,29 @@ +'use client'; + +/** + * InspectionReportModal — 공유 래퍼 + * + * 원본: @/components/production/WorkOrders/documents/InspectionReportModal + * 목적: 공통 ERP(결재, 품질)에서 생산 모듈을 직접 import하지 않도록 + * dynamic import로 정적 의존성 체인을 끊음 + */ + +import dynamic from 'next/dynamic'; +import { Loader2 } from 'lucide-react'; + +const InspectionReportModalImpl = dynamic( + () => + import('@/components/production/WorkOrders/documents/InspectionReportModal').then( + (mod) => mod.InspectionReportModal, + ), + { + loading: () => ( +
+ +
+ ), + ssr: false, + }, +); + +export { InspectionReportModalImpl as InspectionReportModal }; diff --git a/src/components/document-system/modals/WorkLogModal.tsx b/src/components/document-system/modals/WorkLogModal.tsx new file mode 100644 index 00000000..961e750a --- /dev/null +++ b/src/components/document-system/modals/WorkLogModal.tsx @@ -0,0 +1,29 @@ +'use client'; + +/** + * WorkLogModal — 공유 래퍼 + * + * 원본: @/components/production/WorkOrders/documents/WorkLogModal + * 목적: 공통 ERP(품질 QMS)에서 생산 모듈을 직접 import하지 않도록 + * dynamic import로 정적 의존성 체인을 끊음 + */ + +import dynamic from 'next/dynamic'; +import { Loader2 } from 'lucide-react'; + +const WorkLogModalImpl = dynamic( + () => + import('@/components/production/WorkOrders/documents/WorkLogModal').then( + (mod) => mod.WorkLogModal, + ), + { + loading: () => ( +
+ +
+ ), + ssr: false, + }, +); + +export { WorkLogModalImpl as WorkLogModal }; diff --git a/src/components/document-system/modals/index.ts b/src/components/document-system/modals/index.ts new file mode 100644 index 00000000..a67412a4 --- /dev/null +++ b/src/components/document-system/modals/index.ts @@ -0,0 +1,9 @@ +/** + * document-system/modals — 모듈 경계를 넘는 공유 모달 래퍼 + * + * 공통 ERP 코드에서 테넌트 전용 모달을 사용할 때 + * 직접 import 대신 이 래퍼를 통해 dynamic import로 접근 + */ +export { InspectionReportModal } from './InspectionReportModal'; +export { WorkLogModal } from './WorkLogModal'; +export { AssigneeSelectModal } from './AssigneeSelectModal'; diff --git a/src/lib/api/production-orders/actions.ts b/src/lib/api/production-orders/actions.ts new file mode 100644 index 00000000..e863b576 --- /dev/null +++ b/src/lib/api/production-orders/actions.ts @@ -0,0 +1,27 @@ +'use server'; + +/** + * 생산지시 공유 액션 — 모듈 경계용 래퍼 + * + * 원본: @/components/production/ProductionOrders/actions + * 'use server' 파일에서는 re-export 불가 → async 래퍼 함수로 위임 + */ + +import { + getProductionOrders as _getProductionOrders, + getProductionOrderStats as _getProductionOrderStats, + getProductionOrderDetail as _getProductionOrderDetail, +} from '@/components/production/ProductionOrders/actions'; +import type { ProductionOrderListParams } from './types'; + +export async function getProductionOrders(params: ProductionOrderListParams) { + return _getProductionOrders(params); +} + +export async function getProductionOrderStats() { + return _getProductionOrderStats(); +} + +export async function getProductionOrderDetail(orderId: string) { + return _getProductionOrderDetail(orderId); +} diff --git a/src/lib/api/production-orders/index.ts b/src/lib/api/production-orders/index.ts new file mode 100644 index 00000000..1ee90d81 --- /dev/null +++ b/src/lib/api/production-orders/index.ts @@ -0,0 +1,22 @@ +/** + * 생산지시 공유 API — 공통 ERP에서 접근하는 진입점 + * + * 공통 ERP(영업 등)에서는 이 경로로 import: + * import { getProductionOrders } from '@/lib/api/production-orders'; + * import type { ProductionOrder } from '@/lib/api/production-orders'; + * + * 생산 모듈 내부에서는 기존 경로 유지: + * import { ... } from './actions'; + * import type { ... } from './types'; + */ +export { getProductionOrders, getProductionOrderStats, getProductionOrderDetail } from './actions'; +export type { + ProductionStatus, + ProductionOrder, + ProductionOrderStats, + ProductionOrderDetail, + ProductionWorkOrder, + BomProcessGroup, + BomItem, + ProductionOrderListParams, +} from './types'; diff --git a/src/lib/api/production-orders/types.ts b/src/lib/api/production-orders/types.ts new file mode 100644 index 00000000..95c61370 --- /dev/null +++ b/src/lib/api/production-orders/types.ts @@ -0,0 +1,22 @@ +/** + * 생산지시 공유 타입 — 모듈 경계용 re-export + * + * 원본: @/components/production/ProductionOrders/types + * 영업(공통 ERP)에서 생산(테넌트) 타입을 직접 import하지 않도록 + * 공유 영역에서 re-export + */ +export type { + ProductionStatus, + ProductionOrder, + ProductionOrderStats, + ProductionOrderDetail, + ProductionWorkOrder, + BomProcessGroup, + BomItem, + ProductionOrderListParams, + ApiProductionOrder, + ApiProductionOrderDetail, + ApiProductionWorkOrder, + ApiBomProcessGroup, + ApiBomItem, +} from '@/components/production/ProductionOrders/types'; diff --git a/src/lib/dashboard-invalidation.ts b/src/lib/dashboard-invalidation.ts index 6ce78414..b5f0b166 100644 --- a/src/lib/dashboard-invalidation.ts +++ b/src/lib/dashboard-invalidation.ts @@ -2,6 +2,10 @@ * CEO 대시보드 targeted refetch 시스템 * * CUD 발생 시 sessionStorage + CustomEvent로 대시보드 섹션별 갱신 트리거 + * + * 도메인→섹션 매핑은 동적 레지스트리 패턴: + * - 공통 ERP 도메인은 여기서 직접 등록 + * - 테넌트 전용 도메인은 각 모듈에서 registerDashboardDomain()으로 자기 등록 */ // 대시보드 섹션 키 (useCEODashboard의 refetchMap과 1:1 매핑) @@ -21,49 +25,46 @@ export type DashboardSectionKey = | '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 domainSectionRegistry = new Map(); -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'], -}; +/** + * 도메인→섹션 매핑 등록 (각 모듈에서 호출) + * + * @example + * // 생산 모듈 초기화 시 + * registerDashboardDomain('production', ['statusBoard', 'dailyProduction']); + * + * // 건설 모듈 초기화 시 + * registerDashboardDomain('construction', ['statusBoard', 'construction']); + */ +export function registerDashboardDomain(domain: string, sections: DashboardSectionKey[]): void { + domainSectionRegistry.set(domain, sections); +} + +// ===== 공통 ERP 도메인 (테넌트 무관, 항상 등록) ===== +registerDashboardDomain('deposit', ['dailyReport', 'receivable']); +registerDashboardDomain('withdrawal', ['dailyReport', 'monthlyExpense']); +registerDashboardDomain('sales', ['dailyReport', 'salesStatus', 'receivable']); +registerDashboardDomain('purchase', ['dailyReport', 'purchaseStatus', 'monthlyExpense']); +registerDashboardDomain('badDebt', ['debtCollection', 'receivable']); +registerDashboardDomain('expectedExpense', ['monthlyExpense']); +registerDashboardDomain('bill', ['dailyReport', 'receivable']); +registerDashboardDomain('giftCertificate', ['entertainment', 'cardManagement']); +registerDashboardDomain('journalEntry', ['entertainment', 'welfare', 'monthlyExpense']); +registerDashboardDomain('order', ['statusBoard', 'salesStatus']); +registerDashboardDomain('stock', ['statusBoard']); +registerDashboardDomain('schedule', ['statusBoard']); +registerDashboardDomain('client', ['statusBoard']); +registerDashboardDomain('leave', ['statusBoard', 'dailyAttendance']); +registerDashboardDomain('approval', ['statusBoard']); +registerDashboardDomain('attendance', ['statusBoard', 'dailyAttendance']); +registerDashboardDomain('shipment', ['statusBoard', 'unshipped']); + +// ===== 테넌트 전용 도메인 (각 모듈에서 등록 — 현재는 하위 호환을 위해 여기서도 등록) ===== +// TODO: Phase 1 완료 후 각 모듈의 초기화 코드로 이동 +registerDashboardDomain('production', ['statusBoard', 'dailyProduction']); +registerDashboardDomain('construction', ['statusBoard', 'construction']); const STORAGE_KEY = 'dashboard:stale-sections'; const EVENT_NAME = 'dashboard:invalidate'; @@ -71,8 +72,8 @@ const EVENT_NAME = 'dashboard:invalidate'; /** * CUD 성공 후 호출 — 해당 도메인이 영향 주는 대시보드 섹션을 stale 처리 */ -export function invalidateDashboard(domain: DomainKey): void { - const sections = DOMAIN_SECTION_MAP[domain]; +export function invalidateDashboard(domain: string): void { + const sections = domainSectionRegistry.get(domain); if (!sections || sections.length === 0) return; // 1. sessionStorage에 stale 섹션 저장 (navigation 사이 유지)