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 사이 유지)