refactor: [Phase 0] 공통→테넌트 모듈 의존성 해소
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
// 진행 단계 컴포넌트
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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: () => (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
),
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export { AssigneeSelectModalImpl as AssigneeSelectModal };
|
||||
@@ -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: () => (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
),
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export { InspectionReportModalImpl as InspectionReportModal };
|
||||
29
src/components/document-system/modals/WorkLogModal.tsx
Normal file
29
src/components/document-system/modals/WorkLogModal.tsx
Normal file
@@ -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: () => (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
),
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export { WorkLogModalImpl as WorkLogModal };
|
||||
9
src/components/document-system/modals/index.ts
Normal file
9
src/components/document-system/modals/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* document-system/modals — 모듈 경계를 넘는 공유 모달 래퍼
|
||||
*
|
||||
* 공통 ERP 코드에서 테넌트 전용 모달을 사용할 때
|
||||
* 직접 import 대신 이 래퍼를 통해 dynamic import로 접근
|
||||
*/
|
||||
export { InspectionReportModal } from './InspectionReportModal';
|
||||
export { WorkLogModal } from './WorkLogModal';
|
||||
export { AssigneeSelectModal } from './AssigneeSelectModal';
|
||||
27
src/lib/api/production-orders/actions.ts
Normal file
27
src/lib/api/production-orders/actions.ts
Normal file
@@ -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);
|
||||
}
|
||||
22
src/lib/api/production-orders/index.ts
Normal file
22
src/lib/api/production-orders/index.ts
Normal file
@@ -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';
|
||||
22
src/lib/api/production-orders/types.ts
Normal file
22
src/lib/api/production-orders/types.ts
Normal file
@@ -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';
|
||||
@@ -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<string, DashboardSectionKey[]>();
|
||||
|
||||
const DOMAIN_SECTION_MAP: Record<DomainKey, DashboardSectionKey[]> = {
|
||||
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 사이 유지)
|
||||
|
||||
Reference in New Issue
Block a user