diff --git a/claudedocs/architecture/[ANALYSIS-2026-03-17] tenant-module-separation-dependency-audit.md b/claudedocs/architecture/[ANALYSIS-2026-03-17] tenant-module-separation-dependency-audit.md new file mode 100644 index 00000000..af4f9a18 --- /dev/null +++ b/claudedocs/architecture/[ANALYSIS-2026-03-17] tenant-module-separation-dependency-audit.md @@ -0,0 +1,437 @@ +# Tenant-Based Module Separation: Cross-Module Dependency Audit + +**Date**: 2026-03-17 +**Scope**: Full dependency analysis for tenant-based module separation +**Status**: COMPLETE + +--- + +## Module Separation Plan + +| Tenant Package | Modules | File Count | +|---|---|---| +| Common ERP | dashboard, accounting, sales, HR, approval, board, customer-center, settings, master-data, material, outbound | ~majority | +| Kyungdong (Shutter MES) | production (56 files), quality (35 files) | 91 files | +| Juil (Construction) | construction (161 files) | 161 files | +| Optional | vehicle-management (13 files), vehicle (10 files) | 23 files | + +--- + +## CRITICAL RISK (Will break immediately on separation) + +### C1. Approval Module -> Production Component Import +**Files affected**: 1 file, blocks entire approval workflow +``` +src/components/approval/ApprovalBox/index.tsx:76 + import { InspectionReportModal } from '@/components/production/WorkOrders/documents/InspectionReportModal'; +``` +**Impact**: The approval inbox (Common ERP) directly imports a production document modal. If production module is removed, the approval box crashes entirely. +**Fix**: Extract `InspectionReportModal` to a shared `@/components/document-system/` or a shared document viewer module. Alternatively, use dynamic import with fallback. + +--- + +### C2. Sales Module -> Production Component Imports (3 page files) +**Files affected**: 3 files under `src/app/[locale]/(protected)/sales/` + +``` +sales/order-management-sales/production-orders/page.tsx + import { getProductionOrders, getProductionOrderStats } from "@/components/production/ProductionOrders/actions"; + import { ProductionOrder, ProductionOrderStats } from "@/components/production/ProductionOrders/types"; + +sales/order-management-sales/production-orders/[id]/page.tsx + import { getProductionOrderDetail } from "@/components/production/ProductionOrders/actions"; + import { ProductionOrderDetail, ProductionOrderStats } from "@/components/production/ProductionOrders/types"; + +sales/order-management-sales/[id]/production-order/page.tsx + import { AssigneeSelectModal } from "@/components/production/WorkOrders/AssigneeSelectModal"; + import { getProcessList } from "@/components/process-management/actions"; + import { createProductionOrder } from "@/components/orders/actions"; +``` +**Impact**: The sales module has a "production orders" sub-page that directly imports production actions, types, and UI components. This entire sub-section becomes non-functional without the production module. +**Fix strategy**: +1. The `production-orders` sub-pages under sales should be conditionally loaded (tenant feature flag) +2. `ProductionOrders/actions.ts` and `types.ts` should be extracted to a shared interface package +3. `AssigneeSelectModal` should be moved to a shared component or lazy-loaded with fallback + +--- + +### C3. Sales -> Production Route Navigation +**File**: `sales/order-management-sales/production-orders/[id]/page.tsx:247` +``` +router.push("/production/work-orders"); +``` +**Impact**: Hard navigation to production route from sales. Will 404 if production routes don't exist. +**Fix**: Conditional navigation wrapped in tenant feature check. + +--- + +### C4. QMS Page (Quality) -> Production + Outbound + Orders Imports +**File**: `src/app/[locale]/(protected)/quality/qms/page.tsx` +``` +import { InspectionReportModal } from '@/components/production/WorkOrders/documents'; +import { WorkLogModal } from '@/components/production/WorkOrders/documents'; +import { ProductInspectionViewModal } from '@/components/quality/InspectionManagement/ProductInspectionViewModal'; +``` +**File**: `src/app/[locale]/(protected)/quality/qms/mockData.ts` +``` +import type { WorkOrder } from '@/components/production/ProductionDashboard/types'; +import type { ShipmentDetail } from '@/components/outbound/ShipmentManagement/types'; +``` +**File**: `src/app/[locale]/(protected)/quality/qms/components/InspectionModal.tsx` +``` +import { DeliveryConfirmation } from '@/components/outbound/ShipmentManagement/documents/DeliveryConfirmation'; +import { ShippingSlip } from '@/components/outbound/ShipmentManagement/documents/ShippingSlip'; +import { SalesOrderDocument } from '@/components/orders/documents/SalesOrderDocument'; +import type { ShipmentDetail } from '@/components/outbound/ShipmentManagement/types'; +``` +**Impact**: QMS (assigned to Kyungdong tenant with quality) imports from production (same tenant -- OK), but also from outbound and orders (Common ERP). If QMS is extracted WITH quality, it will still need access to outbound/orders document components from Common ERP. +**Fix**: This is actually a reverse dependency -- quality module needs Common ERP's outbound/orders docs. This direction is acceptable (tenant module depends on common). However, the production type imports need a shared types interface. + +--- + +### C5. CEO Dashboard -> Production + Construction Data Sections +**Files affected**: Multiple files in `src/components/business/CEODashboard/` +``` +CEODashboard.tsx: 'production' and 'construction' section rendering +sections/DailyProductionSection.tsx: production data display +sections/ConstructionSection.tsx: construction data display +sections/CalendarSection.tsx: '/production/work-orders' and '/construction/project/contract' route references +types.ts: DailyProductionData, ConstructionData, production/construction settings flags +useSectionSummary.ts: production and construction summary logic +``` +**File**: `src/lib/api/dashboard/transformers/production-logistics.ts` +``` +import type { DailyProductionData, UnshippedData, ConstructionData } from '@/components/business/CEODashboard/types'; +``` +**File**: `src/hooks/useCEODashboard.ts` +``` +useDashboardFetch('dashboard/production/summary', ...) +useDashboardFetch('dashboard/construction/summary', ...) +``` +**Impact**: The CEO Dashboard (Common ERP) renders production and construction sections. If modules are removed, these sections will fail. The dashboard types contain production/construction data structures hardcoded. +**Fix**: +1. Dashboard sections must be conditionally rendered based on tenant configuration +2. Dashboard types need module-awareness (optional types, feature flags) +3. Dashboard API hooks should gracefully handle missing endpoints +4. Route references in CalendarSection need tenant-aware navigation + +--- + +### C6. Dashboard Invalidation System +**File**: `src/lib/dashboard-invalidation.ts` +```typescript +type DomainKey = '...' | 'production' | 'shipment' | 'construction'; +const DOMAIN_SECTION_MAP = { + production: ['statusBoard', 'dailyProduction'], + construction: ['statusBoard', 'construction'], +}; +``` +**Impact**: The dashboard invalidation system in `@/lib/` (Common ERP) has hardcoded production and construction domains. If production/construction modules call `invalidateDashboard('production')`, this works cross-module. If they are removed, stale keys remain. +**Fix**: Make domain-section mapping configurable/dynamic. Register domains at module initialization. + +--- + +## HIGH RISK (Will cause issues during development/testing) + +### H1. Construction -> HR Module Import +**File**: `src/components/business/construction/site-briefings/SiteBriefingForm.tsx:61-64` +``` +import { getEmployees } from '@/components/hr/EmployeeManagement/actions'; +import type { Employee } from '@/components/hr/EmployeeManagement/types'; +``` +**Impact**: Construction module (Juil tenant) depends on HR module (Common ERP). This direction (tenant -> common) is acceptable for single-binary, but if construction is extracted to a separate package, it needs access to HR's interface. +**Fix**: Extract employee lookup to a shared API interface. Or accept that tenant modules depend on Common ERP (allowed direction). + +--- + +### H2. Production -> Process-Management Import +**File**: `src/components/production/WorkerScreen/index.tsx:47` +``` +import { getProcessList } from '@/components/process-management/actions'; +``` +**Impact**: Process management is under `master-data` (Common ERP). Production depends on it. +**Fix**: This is allowed (tenant -> common), but if extracting to separate package, needs clear interface. + +--- + +### H3. Shared Type: `@/types/process.ts` +**Files**: 8 production files import from this shared type file +``` +src/components/production/WorkerScreen/index.tsx +src/components/production/WorkOrders/documents/BendingInspectionContent.tsx +src/components/production/WorkOrders/documents/BendingWipInspectionContent.tsx +src/components/production/WorkOrders/documents/InspectionReportModal.tsx +src/components/production/WorkOrders/documents/ScreenInspectionContent.tsx +src/components/production/WorkOrders/documents/SlatInspectionContent.tsx +src/components/production/WorkOrders/documents/SlatJointBarInspectionContent.tsx +src/components/production/WorkOrders/documents/inspection-shared.tsx +``` +**Impact**: `@/types/process.ts` (296 lines) contains `InspectionSetting`, `InspectionScope`, `Process`, `ProcessStep` types. These are used heavily by production but defined at the project root level. +**Fix**: This file should remain in Common ERP (it's process master-data definition). Production depends on it -- that direction is acceptable. + +--- + +### H4. Dev Generator -> Production Type Import +**File**: `src/components/dev/generators/workOrderData.ts:13` +``` +import type { ProcessOption } from '@/components/production/WorkOrders/actions'; +``` +**Impact**: Dev tooling imports a type from production. Low runtime risk (dev only) but will cause TS errors if production module is absent. +**Fix**: Move `ProcessOption` type to a shared types location, or make dev generators tenant-aware. + +--- + +### H5. Production -> Dashboard Invalidation (Bidirectional) +**Files**: +``` +src/components/production/WorkOrders/WorkOrderCreate.tsx:10 -> import { invalidateDashboard } from '@/lib/dashboard-invalidation'; +src/components/production/WorkOrders/WorkOrderDetail.tsx:10 -> same +src/components/production/WorkOrders/WorkOrderEdit.tsx:11 -> same +src/components/business/construction/management/ConstructionDetailClient.tsx:4 -> same +``` +**Impact**: Production and construction call `invalidateDashboard()` from `@/lib/` (Common ERP). This is allowed direction (tenant -> common). But the function's domain keys include `'production'` and `'construction'` -- if those modules are absent, orphan event listeners remain. +**Fix**: Register domain keys dynamically. Modules register their dashboard sections at init. + +--- + +### H6. CEO Dashboard CalendarSection Route References +**File**: `src/components/business/CEODashboard/sections/CalendarSection.tsx` +``` +order: '/production/work-orders', +construction: '/construction/project/contract', +``` +**Impact**: Clicking calendar items navigates to production/construction routes. Will 404 if those tenant routes don't exist. +**Fix**: Tenant-aware route resolution with fallback or hidden navigation for unavailable modules. + +--- + +### H7. Menu Transform Production Icon Mapping +**File**: `src/lib/utils/menuTransform.ts:89` +``` +production: Factory, +``` +**Impact**: Menu icon mapping contains production key. Low risk (backend controls menu visibility), but vestigial code remains. +**Fix**: Menu rendering is already dynamic from backend. Icon map can safely retain unused entries. + +--- + +## MEDIUM RISK (Requires attention but not immediately breaking) + +### M1. Shared Component Dependencies (template/document-system) + +All three target modules (production, quality, construction) heavily depend on these shared components: +- `@/components/templates/UniversalListPage` -- used by all list pages +- `@/components/templates/IntegratedDetailTemplate` -- used by all detail pages +- `@/components/document-system/` -- DocumentViewer, SectionHeader, ConstructionApprovalTable +- `@/components/common/ServerErrorPage` +- `@/components/common/ScheduleCalendar` +- `@/components/organisms/` -- PageLayout, PageHeader, MobileCard + +**Impact**: These are all Common ERP components. The dependency direction (tenant -> common) is allowed. No breakage on separation. +**Risk**: If extracting to separate npm packages, these become peer dependencies. + +--- + +### M2. Zustand Store Dependencies + +Target modules use these stores (all Common ERP): +``` +production/WorkerScreen -> menuStore (useSidebarCollapsed) +quality/EquipmentRepair -> menuStore (useMenuStore) +quality/EquipmentManagement -> menuStore (useMenuStore) +quality/EquipmentForm -> menuStore (useMenuStore) +construction/estimates -> authStore (useAuthStore) +``` +**Impact**: All stores are in Common ERP. Direction is allowed. No breakage on separation. +**Risk**: If separate packages, stores become shared singletons requiring careful provider setup. + +--- + +### M3. Shared Hook Dependencies + +Target modules import from `@/hooks/`: +``` +production: usePermission +quality: useStatsLoader +construction: useStatsLoader, useListHandlers, useDateRange, useDaumPostcode, useCurrentTime +``` +**Impact**: All hooks are Common ERP. Allowed direction. + +--- + +### M4. `@/lib/` Utility Dependencies + +All modules depend on standard utilities: +``` +@/lib/utils (cn) +@/lib/utils/amount (formatNumber, formatAmount) +@/lib/utils/date (formatDate) +@/lib/utils/excel-download +@/lib/utils/redirect-error (isNextRedirectError) +@/lib/utils/status-config (getPresetStyle, getPriorityStyle) +@/lib/api/* (executeServerAction, executePaginatedAction, buildApiUrl, apiClient, serverFetch) +@/lib/formatters +@/lib/constants/filter-presets +``` +**Impact**: All in Common ERP. Direction allowed. + +--- + +### M5. document-system `ConstructionApprovalTable` Name Confusion +**File**: `src/components/document-system/components/ConstructionApprovalTable.tsx` +**Impact**: Despite the name, this is a generic 4-column approval table component in the shared `document-system`. It is imported by both production and construction modules. The name is misleading but the component is tenant-agnostic. +**Fix**: Consider renaming to `FourColumnApprovalTable` or similar during separation to avoid confusion. + +--- + +### M6. Production -> Bending Image API References +**File**: `src/components/production/WorkOrders/documents/bending/utils.ts` +``` +return `${API_BASE}/images/bending/guiderail/...` +return `${API_BASE}/images/bending/bottombar/...` +return `${API_BASE}/images/bending/box/...` +``` +**Impact**: Production references backend image API paths specific to the shutter MES domain. These endpoints exist on the backend and are module-specific. +**Fix**: These stay with the production module. No cross-dependency issue. + +--- + +### M7. Two Vehicle Modules +There are TWO vehicle-related component directories: +- `src/components/vehicle/` (10 files) -- older, simpler +- `src/components/vehicle-management/` (13 files) -- newer, IntegratedDetailTemplate-based + +Both have separate app routes: +- `src/app/[locale]/(protected)/vehicle/` (old) +- `src/app/[locale]/(protected)/vehicle-management/` (new) + +**Impact**: No cross-references found between them or from other modules. Both are fully self-contained. +**Fix**: Clean separation. May want to consolidate before extracting. + +--- + +## LOW RISK (Informational / No action needed) + +### L1. Module-Internal Dynamic Imports +Quality and production use `next/dynamic` for their own internal components (modals, heavy components). All dynamic imports are within their own module boundaries. No cross-module dynamic imports found. + +### L2. No Shared CSS/Style Modules +All modules use Tailwind utility classes. No module-specific CSS modules or shared stylesheets exist. No breakage risk. + +### L3. No React Context Providers in Modules +No module-specific React Context providers were found in production/quality/construction. All context comes from the shared `(protected)/layout.tsx` (RootProvider, ApiErrorProvider, FCMProvider, DevFillProvider, PermissionGate). + +### L4. No Module-Specific Layout Files +No nested `layout.tsx` files exist under production/quality/construction app routes. All pages use the shared `(protected)/layout.tsx`. + +### L5. No Test Files +No test files (`*.test.*`, `*.spec.*`) exist in the codebase. No test dependency issues. + +### L6. No Cross-Module API Endpoint Calls +Each module calls only its own backend API endpoints: +- Production: `/api/v1/work-orders/*`, `/api/v1/work-results/*`, `/api/v1/production-orders/*` +- Quality: `/api/v1/quality/*`, `/api/v1/equipment/*` +- Construction: `/construction/*` (via apiClient) + +No module calls another module's backend API directly. Clean backend separation. + +### L7. CustomEvent System +The `dashboard:invalidate` CustomEvent in `@/lib/dashboard-invalidation.ts` is the only cross-module event system. Production and construction dispatch events; the CEO Dashboard listens. This is a pub/sub pattern and tolerant of missing publishers. + +### L8. Tenant-Aware Cache Already Exists +`src/lib/cache/TenantAwareCache.ts` already implements tenant-id-based cache key isolation. This utility supports the separation strategy. + +### L9. Middleware Has No Module-Specific Logic +`src/middleware.ts` handles i18n and bot detection. No module-specific routing or tenant-based path filtering exists in middleware. Clean. + +--- + +## Dependency Flow Diagram (ASCII) + +``` + +-----------------+ + | Common ERP | + | (dashboard, | + | accounting, | + | sales, HR, | + | approval, | + | settings, | + | master-data, | + | outbound, | + | templates, | + | document-sys) | + +--------+--------+ + | + +--------------+---------------+ + | | | + +--------v------+ +----v-------+ +-----v----------+ + | Kyungdong | | Juil | | Vehicle | + | (production | | (construc | | (vehicle-mgmt) | + | + quality) | | tion) | | | + +------+--------+ +-----+------+ +----------------+ + | | | + v v | + depends on depends on | + Common ERP Common ERP v + self-contained + +FORBIDDEN ARROWS (must be broken): + Common ERP --X--> Production (approval, sales, dashboard) + Common ERP --X--> Construction (dashboard) +``` + +--- + +## Action Items Summary + +### Must Fix Before Separation (6 items) + +| # | Severity | Issue | Effort | +|---|----------|-------|--------| +| C1 | CRITICAL | ApprovalBox imports InspectionReportModal from production | Medium | +| C2 | CRITICAL | Sales production-orders pages import from production | High | +| C3 | CRITICAL | Sales page hard-navigates to /production/work-orders | Low | +| C4 | CRITICAL | QMS page imports production document modals | Medium | +| C5 | CRITICAL | CEO Dashboard hardcodes production/construction sections | High | +| C6 | CRITICAL | Dashboard invalidation hardcodes production/construction domains | Medium | + +### Recommended Actions (6 items) + +| # | Severity | Issue | Effort | +|---|----------|-------|--------| +| H1 | HIGH | Construction SiteBriefing imports HR actions | Low | +| H2 | HIGH | Production WorkerScreen imports process-management | Low | +| H4 | HIGH | Dev generator imports production type | Low | +| H5 | HIGH | Bidirectional dashboard invalidation coupling | Medium | +| H6 | HIGH | CEO Calendar hardcoded module routes | Low | +| H7 | HIGH | Menu transform production icon mapping | Low | + +### Total Estimated Effort + +- **CRITICAL fixes**: ~3-5 days of focused refactoring +- **HIGH fixes**: ~1-2 days +- **MEDIUM/LOW**: informational, no code changes needed + +--- + +## Recommended Separation Strategy + +### Phase 1: Shared Interface Layer (1-2 days) +1. Create `@/interfaces/production.ts` with shared types (ProcessOption, WorkOrder summary types) +2. Create `@/interfaces/quality.ts` with shared types (InspectionReport view props) +3. Move InspectionReportModal and WorkLogModal to `@/components/document-system/modals/` + +### Phase 2: Feature Flags (1-2 days) +1. Add tenant feature flags: `hasProduction`, `hasQuality`, `hasConstruction`, `hasVehicle` +2. Conditionally render CEO Dashboard sections based on flags +3. Conditionally render sales production-order sub-pages based on flags +4. Make dashboard invalidation domain registry dynamic + +### Phase 3: Route Guards (0.5 day) +1. Replace hardcoded route strings with a tenant-aware route resolver +2. Add fallback/redirect for unavailable module routes + +### Phase 4: Clean Separation (1 day) +1. Move production + quality components to a Kyungdong-specific directory or package +2. Move construction components to a Juil-specific directory or package +3. Verify all builds pass with each module removed independently diff --git a/claudedocs/architecture/[PLAN-2026-03-17] tenant-module-separation-plan.md b/claudedocs/architecture/[PLAN-2026-03-17] tenant-module-separation-plan.md new file mode 100644 index 00000000..b9e35fe8 --- /dev/null +++ b/claudedocs/architecture/[PLAN-2026-03-17] tenant-module-separation-plan.md @@ -0,0 +1,1811 @@ +# Tenant-Based Module Separation: Implementation Plan + +**Date**: 2026-03-17 +**Status**: APPROVED PLAN +**Prerequisite**: `[ANALYSIS-2026-03-17] tenant-module-separation-dependency-audit.md` +**Estimated Total Effort**: 12-16 working days across 4 phases +**Zero Downtime Requirement**: All changes are additive; no page removal until Phase 3 + +--- + +## Table of Contents + +1. [Architecture Overview](#1-architecture-overview) +2. [Phase 0: Prerequisite Fixes (4-5 days)](#2-phase-0-prerequisite-fixes) +3. [Phase 1: Module Registry & Route Guard (3-4 days)](#3-phase-1-module-registry--route-guard) +4. [Phase 2: Dashboard Decoupling (2-3 days)](#4-phase-2-dashboard-decoupling) +5. [Phase 3: Physical Separation (2-3 days)](#5-phase-3-physical-separation) +6. [Phase 4: Manifest-Based Module Loading (Future)](#6-phase-4-manifest-based-module-loading) +7. [Testing Strategy](#7-testing-strategy) +8. [Risk Register](#8-risk-register) +9. [Folder Structure Before/After](#9-folder-structure-beforeafter) +10. [Migration Order & Parallelism](#10-migration-order--parallelism) + +--- + +## 1. Architecture Overview + +### Current State + +``` +src/ + app/[locale]/(protected)/ + production/ (12 pages) -- Kyungdong tenant + quality/ (14 pages) -- Kyungdong tenant + construction/ (57 pages) -- Juil tenant + vehicle-management/ (12 pages) -- Optional module + accounting/ (32 pages) -- Common ERP + sales/ (22 pages) -- Common ERP (has 3 pages that import from production) + approval/ (6 pages) -- Common ERP (1 component imports from production) + ...other common modules + components/ + production/ (56 files) + quality/ (35+ files) + business/construction/ (161 files) + vehicle-management/ (13 files) + ...common components +``` + +### Target State (End of Phase 3) + +``` +src/ + modules/ # NEW: module registry + manifest + index.ts # module registry + types.ts # TenantModule, ModuleManifest types + tenant-config.ts # tenant -> module mapping + route-guard.ts # route access check utility + interfaces/ # NEW: shared type contracts + production-orders.ts # types+actions shared between sales & production + inspection-documents.ts # InspectionReportModal props interface + dashboard-sections.ts # dynamic section registration types + app/[locale]/(protected)/ + production/ # unchanged location, guarded by middleware + quality/ # unchanged location, guarded by middleware + construction/ # unchanged location, guarded by middleware + vehicle-management/ # unchanged location, guarded by middleware + components/ + document-system/modals/ # NEW: extracted shared modals + InspectionReportModal.tsx # moved from production + WorkLogModal.tsx # moved from production + production/ # unchanged, but no longer imported by common + quality/ # unchanged + business/construction/ # unchanged +``` + +### Dependency Direction Rules + +``` +ALLOWED: Tenant Module -----> Common ERP + (production -> @/lib/, @/components/ui/, @/hooks/, etc.) + +FORBIDDEN: Common ERP ----X---> Tenant Module + (approval -> production components) + (sales -> production actions/types) + (dashboard -> production/construction data) + +EXCEPTION: Common ERP ~~~?~~~> Tenant Module via: + 1. Dynamic import with fallback (lazy + error boundary) + 2. Shared interface in @/interfaces/ (types only) + 3. Module registry callback (runtime registration) +``` + +--- + +## 2. Phase 0: Prerequisite Fixes + +> **Goal**: Break all forbidden dependency arrows (Common -> Tenant) without changing runtime behavior. +> **Duration**: 4-5 days +> **Risk**: LOW (pure refactoring, no user-facing changes) +> **Rollback**: `git revert` each commit independently + +### Task 0.1: Extract InspectionReportModal to Shared Location + +**Problem**: `ApprovalBox/index.tsx` (common) imports `InspectionReportModal` from `production/WorkOrders/documents/`. Also `quality/qms/page.tsx` imports it. + +**Strategy**: Create a shared document modal system under `@/components/document-system/modals/`. + +**Files to create**: +- `src/components/document-system/modals/InspectionReportModal.tsx` +- `src/components/document-system/modals/WorkLogModal.tsx` +- `src/components/document-system/modals/index.ts` + +**Files to modify**: +- `src/components/approval/ApprovalBox/index.tsx` (change import path) +- `src/app/[locale]/(protected)/quality/qms/page.tsx` (change import path) +- `src/components/production/WorkOrders/documents/InspectionReportModal.tsx` (re-export from shared) +- `src/components/production/WorkOrders/documents/WorkLogModal.tsx` (re-export from shared) +- `src/components/production/WorkOrders/documents/index.ts` (update exports) + +**Code pattern**: +```typescript +// NEW: src/components/document-system/modals/InspectionReportModal.tsx +// Copy the full 570-line component here (from production/WorkOrders/documents/) +// Keep all existing props and behavior identical + +// MODIFY: src/components/production/WorkOrders/documents/InspectionReportModal.tsx +// Replace with re-export to avoid breaking internal production imports: +export { InspectionReportModal } from '@/components/document-system/modals/InspectionReportModal'; + +// MODIFY: src/components/approval/ApprovalBox/index.tsx line 76 +// FROM: +import { InspectionReportModal } from '@/components/production/WorkOrders/documents/InspectionReportModal'; +// TO: +import { InspectionReportModal } from '@/components/document-system/modals/InspectionReportModal'; + +// MODIFY: src/app/[locale]/(protected)/quality/qms/page.tsx lines 11-12 +// FROM: +import { InspectionReportModal } from '@/components/production/WorkOrders/documents'; +import { WorkLogModal } from '@/components/production/WorkOrders/documents'; +// TO: +import { InspectionReportModal } from '@/components/document-system/modals'; +import { WorkLogModal } from '@/components/document-system/modals'; +``` + +**Dependency analysis for InspectionReportModal.tsx (570 lines)**: +The modal imports from: +- `@/types/process.ts` (shared types, already in Common ERP) +- `@/lib/api/` utilities (Common ERP) +- `@/components/ui/` (Common ERP) +- `./inspection-shared` and `./Slat*Content`, `./Screen*Content`, `./Bending*Content` (production-internal) + +The production-internal content components (SlatInspectionContent, ScreenInspectionContent, etc.) are **rendered inside** InspectionReportModal via a switch statement. These are Kyungdong-specific inspection forms. + +**Revised strategy**: Instead of moving the entire modal with its production-specific content components, create a **wrapper pattern**: + +```typescript +// NEW: src/components/document-system/modals/InspectionReportModal.tsx +// This is a THIN wrapper that dynamic-imports the actual modal +'use client'; + +import dynamic from 'next/dynamic'; +import type { ComponentProps } from 'react'; + +// Dynamic import with loading fallback +const InspectionReportModalImpl = dynamic( + () => import('@/components/production/WorkOrders/documents/InspectionReportModal') + .then(mod => ({ default: mod.InspectionReportModal })), + { + loading: () => null, + ssr: false, + } +); + +// Re-export props type from a shared interface (no production dependency) +export interface InspectionReportModalProps { + isOpen: boolean; + onClose: () => void; + workOrderId: number | null; + type?: 'inspection' | 'worklog'; +} + +export function InspectionReportModal(props: InspectionReportModalProps) { + if (!props.isOpen) return null; + return ; +} +``` + +This pattern: +- Breaks the static import chain (Common no longer statically depends on production) +- Uses `next/dynamic` so the production code is only loaded at runtime when needed +- If production module is absent at build time, the dynamic import fails gracefully (modal just doesn't render) +- Zero behavior change for existing users + +**Estimated time**: 0.5 day +**Risk**: LOW +**Dependencies**: None +**Rollback**: Revert import paths + +--- + +### Task 0.2: Extract Production Orders Shared Interface + +**Problem**: 3 files under `sales/order-management-sales/production-orders/` import from `@/components/production/ProductionOrders/actions.ts` and `types.ts`. + +**Strategy**: Create a shared interface file with the types and action signatures that sales needs. The sales pages will use this shared interface. The actual implementation stays in production. + +**Files to create**: +- `src/interfaces/production-orders.ts` + +**Files to modify**: +- `src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx` +- `src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx` +- `src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx` + +**Code pattern**: +```typescript +// NEW: src/interfaces/production-orders.ts +// Extract ONLY the types and action signatures that sales needs + +export interface ProductionOrder { + id: number; + order_number: string; + status: ProductionStatus; + // ... (copy from production/ProductionOrders/types.ts) +} + +export type ProductionStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled'; + +export interface ProductionOrderDetail extends ProductionOrder { + work_orders: ProductionWorkOrder[]; + // ... +} + +export interface ProductionWorkOrder { + id: number; + // ... +} + +export interface ProductionOrderStats { + total: number; + pending: number; + in_progress: number; + completed: number; +} + +// Server actions -- these call the SAME backend API endpoint regardless of module presence +// The backend API /api/v1/production-orders/* exists independently of the frontend module +'use server'; + +import { executeServerAction, executePaginatedAction } from '@/lib/api/server-actions'; +import { buildApiUrl } from '@/lib/api/query-params'; + +export async function getProductionOrders(params: { page?: number; search?: string; status?: string }) { + return executePaginatedAction({ + url: buildApiUrl('/api/v1/production-orders', params), + }); +} + +export async function getProductionOrderDetail(id: number) { + return executeServerAction({ + url: buildApiUrl(`/api/v1/production-orders/${id}`), + }); +} + +export async function getProductionOrderStats() { + return executeServerAction({ + url: buildApiUrl('/api/v1/production-orders/stats'), + }); +} +``` + +**Important**: The actions in this shared interface call the **same backend API endpoints** as the production module's actions. The backend API exists independently. This is safe because the sales production-orders view is read-only (viewing production order status from sales perspective). + +**For AssigneeSelectModal** (imported by sales/[id]/production-order/page.tsx): +```typescript +// Same dynamic import pattern as Task 0.1 +// NEW: src/interfaces/components/AssigneeSelectModal.tsx +import dynamic from 'next/dynamic'; + +const AssigneeSelectModalImpl = dynamic( + () => import('@/components/production/WorkOrders/AssigneeSelectModal') + .then(mod => ({ default: mod.AssigneeSelectModal })), + { loading: () => null, ssr: false } +); + +export interface AssigneeSelectModalProps { + isOpen: boolean; + onClose: () => void; + onSelect: (assignee: { id: number; name: string }) => void; +} + +export function AssigneeSelectModal(props: AssigneeSelectModalProps) { + if (!props.isOpen) return null; + return ; +} +``` + +**Estimated time**: 1 day +**Risk**: MEDIUM (sales production-orders functionality must be verified) +**Dependencies**: None +**Rollback**: Revert 3 page files to original imports + +--- + +### Task 0.3: Fix Hardcoded Route Navigation + +**Problem**: `sales/production-orders/[id]/page.tsx` line 247 has `router.push("/production/work-orders")`. + +**Strategy**: Wrap in a module-aware navigation helper. + +**File to create**: +- `src/modules/route-resolver.ts` + +**File to modify**: +- `src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx` + +**Code pattern**: +```typescript +// NEW: src/modules/route-resolver.ts +/** + * Resolve routes that may point to tenant-specific modules. + * Falls back to a safe alternative if the target module is not available. + */ +export function resolveTenantRoute( + path: string, + fallback: string = '/dashboard' +): string { + // Phase 1: Simple passthrough (all modules present in monolith) + // Phase 2+: Check module registry for route availability + return path; +} + +// Common route mappings for cross-module navigation +export const CROSS_MODULE_ROUTES = { + workOrders: '/production/work-orders', + constructionContract: '/construction/project/contract', +} as const; + +// MODIFY: sales/production-orders/[id]/page.tsx line 247 +// FROM: +router.push("/production/work-orders"); +// TO: +import { resolveTenantRoute } from '@/modules/route-resolver'; +// ... +router.push(resolveTenantRoute("/production/work-orders", "/sales/order-management-sales/production-orders")); +``` + +**Estimated time**: 0.25 day +**Risk**: LOW +**Dependencies**: None +**Rollback**: Revert to hardcoded string + +--- + +### Task 0.4: Fix QMS Production Type Imports + +**Problem**: `quality/qms/mockData.ts` imports `WorkOrder` type from `production/ProductionDashboard/types` and `ShipmentDetail` from `outbound/ShipmentManagement/types`. + +**Strategy**: The QMS page and quality module are in the SAME tenant package (Kyungdong) as production, so production->quality dependencies are acceptable. However, the `outbound` import is cross-tenant (quality -> Common ERP direction), which is actually the ALLOWED direction. No change needed for outbound imports. + +For the production type import in mockData: since this is mock data, we can inline the type or import from the shared interface. + +**Files to modify**: +- `src/app/[locale]/(protected)/quality/qms/mockData.ts` (replace production type import) + +**Code pattern**: +```typescript +// MODIFY: quality/qms/mockData.ts +// FROM: +import type { WorkOrder } from '@/components/production/ProductionDashboard/types'; +// TO: +// Inline the minimal type needed for mock data +interface WorkOrderMock { + id: number; + work_order_number: string; + status: string; + // ... only fields used in mockData +} +``` + +**Estimated time**: 0.25 day +**Risk**: LOW +**Dependencies**: None + +--- + +### Task 0.5: Fix Dev Generator Production Import + +**Problem**: `src/components/dev/generators/workOrderData.ts` imports `ProcessOption` from production. + +**Strategy**: Move `ProcessOption` type to shared interfaces. + +**Files to modify**: +- `src/components/dev/generators/workOrderData.ts` +- `src/interfaces/production-orders.ts` (add ProcessOption type) + +**Estimated time**: 0.25 day +**Risk**: LOW (dev-only code) +**Dependencies**: Task 0.2 + +--- + +### Task 0.6: Make Dashboard Invalidation Dynamic + +**Problem**: `src/lib/dashboard-invalidation.ts` hardcodes `'production'` and `'construction'` as `DomainKey` values. + +**Strategy**: Change from hardcoded union type to a registry-based system. + +**File to modify**: +- `src/lib/dashboard-invalidation.ts` + +**Code pattern**: +```typescript +// MODIFY: src/lib/dashboard-invalidation.ts + +// BEFORE: hardcoded type union +type DomainKey = 'deposit' | 'withdrawal' | ... | 'production' | 'construction'; +const DOMAIN_SECTION_MAP: Record = { ... }; + +// AFTER: registry-based +type CoreDomainKey = 'deposit' | 'withdrawal' | 'sales' | 'purchase' | 'badDebt' + | 'expectedExpense' | 'bill' | 'giftCertificate' | 'journalEntry' + | 'order' | 'stock' | 'schedule' | 'client' | 'leave' + | 'approval' | 'attendance'; + +// Extendable domain key (modules can register additional domains) +type DomainKey = CoreDomainKey | string; + +// Core mappings (always present) +const CORE_DOMAIN_SECTION_MAP: Record = { + deposit: ['dailyReport', 'receivable'], + withdrawal: ['dailyReport', 'monthlyExpense'], + // ... all core mappings +}; + +// Extended mappings (registered by modules) +const extendedDomainMap = new Map(); + +/** Register module-specific dashboard domain mappings */ +export function registerDashboardDomain(domain: string, sections: DashboardSectionKey[]): void { + extendedDomainMap.set(domain, sections); +} + +// Updated function +export function invalidateDashboard(domain: DomainKey): void { + const sections = (CORE_DOMAIN_SECTION_MAP as Record)[domain] + ?? extendedDomainMap.get(domain) + ?? []; + if (sections.length === 0) return; + // ... rest unchanged +} +``` + +Then in production/construction modules, register on load: +```typescript +// src/components/production/WorkOrders/WorkOrderCreate.tsx (at module level) +import { registerDashboardDomain } from '@/lib/dashboard-invalidation'; +registerDashboardDomain('production', ['statusBoard', 'dailyProduction']); +registerDashboardDomain('shipment', ['statusBoard', 'unshipped']); + +// src/components/business/construction/.../ConstructionDetailClient.tsx +registerDashboardDomain('construction', ['statusBoard', 'construction']); +``` + +**Estimated time**: 0.5 day +**Risk**: LOW (backward compatible, registration is additive) +**Dependencies**: None +**Rollback**: Revert to hardcoded map + +--- + +### Phase 0 Summary + +| Task | Effort | Risk | Parallel? | +|------|--------|------|-----------| +| 0.1 Extract InspectionReportModal | 0.5d | LOW | Yes | +| 0.2 Extract Production Orders interface | 1d | MEDIUM | Yes | +| 0.3 Fix hardcoded route navigation | 0.25d | LOW | Yes | +| 0.4 Fix QMS production type imports | 0.25d | LOW | Yes | +| 0.5 Fix dev generator import | 0.25d | LOW | After 0.2 | +| 0.6 Make dashboard invalidation dynamic | 0.5d | LOW | Yes | +| **Total** | **2.75d** | | | +| **Buffer** | **+1.25d** | | | +| **Phase 0 Total** | **4d** | | | + +**Phase 0 Exit Criteria**: +- Zero imports from `@/components/production/` in any file under `src/components/approval/` +- Zero imports from `@/components/production/` in any file under `src/app/[locale]/(protected)/sales/` +- Zero hardcoded production/construction route strings outside their own modules +- `dashboard-invalidation.ts` has no hardcoded `'production'` or `'construction'` in its type definitions +- All existing functionality works identically (regression test) + +--- + +## 3. Phase 1: Module Registry & Route Guard + +> **Goal**: Create a tenant-aware module system and enforce route-level access control. +> **Duration**: 3-4 days +> **Prerequisite**: Phase 0 complete +> **Risk**: MEDIUM (middleware change affects all routes) + +### Task 1.1: Define Module Registry Types + +**File to create**: `src/modules/types.ts` + +```typescript +/** + * Module definition for tenant-based separation. + * Each module represents a group of pages and components + * that belong to a specific tenant or are optional add-ons. + */ + +export type ModuleId = + | 'common' // Always available + | 'production' // Kyungdong: Shutter MES + | 'quality' // Kyungdong: Quality management + | 'construction' // Juil: Construction management + | 'vehicle-management'; // Optional add-on + +export interface ModuleManifest { + id: ModuleId; + name: string; + description: string; + /** Route prefixes owned by this module (e.g., ['/production', '/quality']) */ + routePrefixes: string[]; + /** Dashboard section keys this module contributes */ + dashboardSections?: string[]; + /** Dashboard domain keys for invalidation */ + dashboardDomains?: Record; +} + +export interface TenantModuleConfig { + tenantId: number; + /** Which industry type this tenant belongs to */ + industry?: string; + /** Explicitly enabled modules (overrides industry defaults) */ + enabledModules: ModuleId[]; +} + +/** Runtime module availability check result */ +export interface ModuleAccess { + allowed: boolean; + reason?: 'not_licensed' | 'not_configured' | 'route_not_found'; + redirectTo?: string; +} +``` + +**Estimated time**: 0.25 day + +--- + +### Task 1.2: Create Module Registry + +**File to create**: `src/modules/index.ts` + +```typescript +import type { ModuleManifest, ModuleId } from './types'; + +/** + * Static module manifest registry. + * + * Phase 1: All modules registered here. + * Phase 2: Loaded from backend JSON. + */ +const MODULE_REGISTRY: Record = { + common: { + id: 'common', + name: 'Common ERP', + description: 'Core ERP modules available to all tenants', + routePrefixes: [ + '/dashboard', '/accounting', '/sales', '/hr', '/approval', + '/board', '/boards', '/customer-center', '/settings', + '/master-data', '/material', '/outbound', '/reports', + '/company-info', '/subscription', '/payment-history', + ], + }, + production: { + id: 'production', + name: 'Production Management', + description: 'Shutter MES production and work order management', + routePrefixes: ['/production'], + dashboardSections: ['production', 'shipment'], + dashboardDomains: { + production: ['statusBoard', 'dailyProduction'], + shipment: ['statusBoard', 'unshipped'], + }, + }, + quality: { + id: 'quality', + name: 'Quality Management', + description: 'Equipment and inspection management', + routePrefixes: ['/quality'], + dashboardSections: [], + }, + construction: { + id: 'construction', + name: 'Construction Management', + description: 'Juil construction project management', + routePrefixes: ['/construction'], + dashboardSections: ['construction'], + dashboardDomains: { + construction: ['statusBoard', 'construction'], + }, + }, + 'vehicle-management': { + id: 'vehicle-management', + name: 'Vehicle Management', + description: 'Vehicle and forklift management', + routePrefixes: ['/vehicle-management', '/vehicle'], + dashboardSections: [], + }, +}; + +/** Get module manifest by ID */ +export function getModuleManifest(moduleId: ModuleId): ModuleManifest | undefined { + return MODULE_REGISTRY[moduleId]; +} + +/** Get all registered module manifests */ +export function getAllModules(): ModuleManifest[] { + return Object.values(MODULE_REGISTRY); +} + +/** Find which module owns a given route prefix */ +export function getModuleForRoute(pathname: string): ModuleId { + for (const [moduleId, manifest] of Object.entries(MODULE_REGISTRY)) { + if (moduleId === 'common') continue; // Check specific modules first + for (const prefix of manifest.routePrefixes) { + if (pathname === prefix || pathname.startsWith(prefix + '/')) { + return moduleId as ModuleId; + } + } + } + return 'common'; // Default: common ERP +} + +/** Get all route prefixes for a set of enabled modules */ +export function getAllowedRoutePrefixes(enabledModules: ModuleId[]): string[] { + const prefixes: string[] = []; + for (const moduleId of ['common' as ModuleId, ...enabledModules]) { + const manifest = MODULE_REGISTRY[moduleId]; + if (manifest) { + prefixes.push(...manifest.routePrefixes); + } + } + return prefixes; +} + +/** Get dashboard section keys for enabled modules */ +export function getEnabledDashboardSections(enabledModules: ModuleId[]): string[] { + const sections: string[] = []; + for (const moduleId of enabledModules) { + const manifest = MODULE_REGISTRY[moduleId]; + if (manifest?.dashboardSections) { + sections.push(...manifest.dashboardSections); + } + } + return sections; +} +``` + +**Estimated time**: 0.5 day + +--- + +### Task 1.3: Create Tenant Configuration + +**File to create**: `src/modules/tenant-config.ts` + +```typescript +import type { ModuleId, TenantModuleConfig } from './types'; + +/** + * Phase 1: Hardcoded tenant-module mappings. + * Phase 2: Loaded from backend API response (/api/auth/user). + * + * Industry-based default modules: + * - 'shutter_mes' (Kyungdong): production + quality + * - 'construction' (Juil): construction + * - undefined / other: common only + */ + +const INDUSTRY_MODULE_MAP: Record = { + shutter_mes: ['production', 'quality'], + construction: ['construction'], +}; + +/** + * Resolve enabled modules for a tenant. + * + * Priority: + * 1. Explicit tenant configuration from backend (Phase 2) + * 2. Industry-based defaults + * 3. Empty (common ERP only) + */ +export function resolveEnabledModules(options: { + industry?: string; + explicitModules?: ModuleId[]; + optionalModules?: ModuleId[]; +}): ModuleId[] { + const { industry, explicitModules, optionalModules = [] } = options; + + // Phase 2: Backend provides explicit module list + if (explicitModules && explicitModules.length > 0) { + return [...explicitModules, ...optionalModules]; + } + + // Phase 1: Industry-based defaults + const industryModules = industry ? (INDUSTRY_MODULE_MAP[industry] ?? []) : []; + return [...industryModules, ...optionalModules]; +} + +/** + * Check if a specific module is enabled for the current tenant. + * This is the primary API for components to check module availability. + */ +export function isModuleEnabled( + moduleId: ModuleId, + enabledModules: ModuleId[] +): boolean { + if (moduleId === 'common') return true; + return enabledModules.includes(moduleId); +} +``` + +**Estimated time**: 0.25 day + +--- + +### Task 1.4: Create useModules Hook + +**File to create**: `src/hooks/useModules.ts` + +```typescript +'use client'; + +import { useMemo } from 'react'; +import { useAuthStore } from '@/stores/authStore'; +import type { ModuleId } from '@/modules/types'; +import { resolveEnabledModules, isModuleEnabled } from '@/modules/tenant-config'; +import { getModuleForRoute, getEnabledDashboardSections } from '@/modules'; + +/** + * Hook to access tenant module configuration. + * Returns enabled modules and helper functions. + */ +export function useModules() { + const tenant = useAuthStore((state) => state.currentUser?.tenant); + + const enabledModules = useMemo(() => { + if (!tenant) return []; + return resolveEnabledModules({ + industry: tenant.options?.industry, + // Phase 2: read from tenant.options?.modules + }); + }, [tenant]); + + const isEnabled = useMemo(() => { + return (moduleId: ModuleId) => isModuleEnabled(moduleId, enabledModules); + }, [enabledModules]); + + const isRouteAllowed = useMemo(() => { + return (pathname: string) => { + const owningModule = getModuleForRoute(pathname); + if (owningModule === 'common') return true; + return enabledModules.includes(owningModule); + }; + }, [enabledModules]); + + const dashboardSections = useMemo(() => { + return getEnabledDashboardSections(enabledModules); + }, [enabledModules]); + + return { + enabledModules, + isEnabled, + isRouteAllowed, + dashboardSections, + tenantIndustry: tenant?.options?.industry, + }; +} +``` + +**Estimated time**: 0.25 day + +--- + +### Task 1.5: Add Route Guard to Middleware + +**Problem**: Currently, any authenticated user can access any route via URL. Tenant users should only access their licensed modules. + +**Strategy**: Add a module check step to the existing middleware at `src/middleware.ts`, between the authentication check (step 7) and the i18n middleware (step 8). + +**File to modify**: `src/middleware.ts` + +**Design considerations**: +- Middleware runs on Edge Runtime -- cannot access Zustand store +- Must read tenant info from cookies or a lightweight API call +- First iteration: read `tenant_modules` cookie set at login +- The cookie is set by the login flow (or the API proxy refresh flow) + +**Code pattern**: +```typescript +// ADD to src/middleware.ts after step 7 (authentication check) + +// 7.5: Module-based route guard +// Read tenant's enabled modules from cookie (set at login) +const tenantModulesCookie = request.cookies.get('tenant_modules')?.value; +if (tenantModulesCookie) { + const enabledModules: string[] = JSON.parse(tenantModulesCookie); + + // Import route check logic (keep it simple for Edge Runtime) + const moduleRouteMap: Record = { + production: ['/production'], + quality: ['/quality'], + construction: ['/construction'], + 'vehicle-management': ['/vehicle-management', '/vehicle'], + }; + + // Check if the requested path belongs to a module the tenant doesn't have + for (const [moduleId, prefixes] of Object.entries(moduleRouteMap)) { + for (const prefix of prefixes) { + if ( + (pathnameWithoutLocale === prefix || pathnameWithoutLocale.startsWith(prefix + '/')) && + !enabledModules.includes(moduleId) && + !enabledModules.includes('all') // Superadmin override + ) { + // Redirect to dashboard with a message parameter + return NextResponse.redirect( + new URL('/dashboard?module_denied=true', request.url) + ); + } + } + } +} +``` + +**Backend requirement**: The login API response or `/api/auth/user` response needs to include `enabled_modules` or `tenant.options.modules`. This cookie must be set during login flow. + +**File to modify for cookie setting**: The login server action or API proxy that handles authentication. Specifically: +- `src/lib/api/auth/` -- wherever the login response is processed and cookies are set + +**Alternative (simpler, Phase 1 only)**: Skip middleware route guard initially. Instead, add a client-side `` component to the `(protected)/layout.tsx` that checks the route against enabled modules and shows an "access denied" page. + +```typescript +// NEW: src/components/auth/ModuleGuard.tsx +'use client'; + +import { usePathname } from 'next/navigation'; +import { useModules } from '@/hooks/useModules'; + +export function ModuleGuard({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const { isRouteAllowed } = useModules(); + + // Remove locale prefix for checking + const cleanPath = pathname.replace(/^\/[a-z]{2}\//, '/'); + + if (!isRouteAllowed(cleanPath)) { + return ( +
+

접근 권한 없음

+

+ 현재 계약에 포함되지 않은 모듈입니다. +

+ + 대시보드로 돌아가기 + +
+ ); + } + + return <>{children}; +} +``` + +**Modify**: `src/app/[locale]/(protected)/layout.tsx` +```typescript +// ADD import +import { ModuleGuard } from '@/components/auth/ModuleGuard'; + +// WRAP children + + + {children} + + +``` + +**Recommended approach**: Start with client-side `ModuleGuard` (simpler, no backend cookie change needed). Add middleware guard in Phase 2 when backend provides `enabled_modules`. + +**Estimated time**: 1 day +**Risk**: MEDIUM (affects all page loads, needs thorough testing) +**Dependencies**: Task 1.2, 1.3, 1.4 + +--- + +### Task 1.6: Add Module-Denied Toast to Dashboard + +**File to modify**: `src/app/[locale]/(protected)/dashboard/page.tsx` + +```typescript +'use client'; + +import { useEffect } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { toast } from 'sonner'; +import { Dashboard } from '@/components/business/Dashboard'; + +export default function DashboardPage() { + const searchParams = useSearchParams(); + + useEffect(() => { + if (searchParams.get('module_denied') === 'true') { + toast.error('접근 권한이 없는 모듈입니다. 관리자에게 문의하세요.'); + // Clean up URL + window.history.replaceState(null, '', '/dashboard'); + } + }, [searchParams]); + + return ; +} +``` + +**Estimated time**: 0.25 day +**Risk**: LOW + +--- + +### Task 1.7: Backend Coordination -- Tenant Module Data + +**Requirement**: The backend `/api/auth/user` response (or the login response) should include the tenant's enabled modules. + +**Proposed backend response extension**: +```json +{ + "user": { ... }, + "tenant": { + "id": 282, + "company_name": "(주)경동", + "business_num": "123-45-67890", + "tenant_st_code": "active", + "options": { + "industry": "shutter_mes", + "modules": ["production", "quality", "vehicle-management"] + } + }, + "menus": [ ... ] +} +``` + +**Backend API request document**: +```markdown +## Backend API Modification Request + +### Endpoint: GET /api/v1/auth/user (or login response) +### Change: Add `modules` to `tenant.options` + +**Current response** (tenant.options): +```json +{ + "company_scale": "중소기업", + "industry": "shutter_mes" +} +``` + +**Requested response** (tenant.options): +```json +{ + "company_scale": "중소기업", + "industry": "shutter_mes", + "modules": ["production", "quality"] +} +``` + +**Module values**: "production", "quality", "construction", "vehicle-management" +**Default behavior**: If `modules` is absent, use industry-based defaults +**Backend table**: `tenant_options` or `tenant_modules` (new table) +``` + +**Until backend provides this**: The frontend falls back to `industry` field for module resolution (Task 1.3 already handles this). + +**Estimated time**: 0 days (frontend) -- backend team separate +**Risk**: None for frontend (graceful fallback exists) + +--- + +### Phase 1 Summary + +| Task | Effort | Risk | Parallel? | +|------|--------|------|-----------| +| 1.1 Module types | 0.25d | LOW | Yes | +| 1.2 Module registry | 0.5d | LOW | After 1.1 | +| 1.3 Tenant config | 0.25d | LOW | After 1.1 | +| 1.4 useModules hook | 0.25d | LOW | After 1.2, 1.3 | +| 1.5 Route guard (client-side) | 1d | MEDIUM | After 1.4 | +| 1.6 Module-denied toast | 0.25d | LOW | After 1.5 | +| 1.7 Backend coordination | 0d | - | Parallel | +| **Total** | **2.5d** | | | +| **Buffer** | **+1d** | | | +| **Phase 1 Total** | **3.5d** | | | + +**Phase 1 Exit Criteria**: +- Module registry exists with correct route prefix mappings +- `useModules()` hook returns correct enabled modules based on tenant industry +- Navigating to `/production/*` as a construction-only tenant shows "access denied" +- Dashboard page shows toast when redirected from denied module +- All existing tenants with correct industry setting see no change in behavior + +--- + +## 4. Phase 2: Dashboard Decoupling + +> **Goal**: Make the CEO Dashboard dynamically render only sections for the tenant's enabled modules. +> **Duration**: 2-3 days +> **Prerequisite**: Phase 1 complete +> **Risk**: MEDIUM (dashboard is highly visible, any regression is immediately noticed) + +### Task 2.1: Add Module Awareness to Dashboard Settings Type + +**File to modify**: `src/components/business/CEODashboard/types.ts` + +```typescript +// ADD to types.ts + +/** Sections that require specific modules */ +export const MODULE_DEPENDENT_SECTIONS: Record = { + production: ['production', 'shipment'], + construction: ['construction'], + // 'unshipped' stays in common -- it's about outbound/logistics +}; + +/** Check if a section key requires a specific module */ +export function sectionRequiresModule(sectionKey: SectionKey): string | null { + for (const [moduleId, sections] of Object.entries(MODULE_DEPENDENT_SECTIONS)) { + if (sections.includes(sectionKey)) return moduleId; + } + return null; +} +``` + +**Estimated time**: 0.25 day + +--- + +### Task 2.2: Make CEODashboard Module-Aware + +**File to modify**: `src/components/business/CEODashboard/CEODashboard.tsx` + +**Changes**: +1. Import `useModules` hook +2. Filter out disabled module sections from `sectionOrder` +3. Skip API calls for disabled module data +4. Filter settings dialog to hide unavailable sections + +```typescript +// ADD import +import { useModules } from '@/hooks/useModules'; +import { sectionRequiresModule } from './types'; + +// ADD inside CEODashboard(): +const { enabledModules, isEnabled } = useModules(); + +// MODIFY useCEODashboard call: +const apiData = useCEODashboard({ + salesStatus: true, + purchaseStatus: true, + dailyProduction: isEnabled('production'), // conditional + unshipped: true, // common (outbound) + construction: isEnabled('construction'), // conditional + dailyAttendance: true, +}); + +// MODIFY sectionOrder filtering: +const sectionOrder = useMemo(() => { + const rawOrder = dashboardSettings.sectionOrder ?? DEFAULT_SECTION_ORDER; + // Filter out sections whose required module is not enabled + return rawOrder.filter((key) => { + const requiredModule = sectionRequiresModule(key); + if (!requiredModule) return true; // Common section, always show + return isEnabled(requiredModule as any); + }); +}, [dashboardSettings.sectionOrder, isEnabled]); + +// MODIFY renderDashboardSection for 'production' and 'construction' cases: +case 'production': + if (!isEnabled('production')) return null; // NEW CHECK + if (!(dashboardSettings.production ?? true) || !data.dailyProduction) return null; + // ... rest unchanged + +case 'construction': + if (!isEnabled('construction')) return null; // NEW CHECK + if (!(dashboardSettings.construction ?? true) || !data.constructionData) return null; + // ... rest unchanged +``` + +**Estimated time**: 0.5 day +**Risk**: MEDIUM (must verify all 18+ sections still render correctly) + +--- + +### Task 2.3: Make Dashboard Settings Dialog Module-Aware + +**File to modify**: `src/components/business/CEODashboard/dialogs/DashboardSettingsDialog.tsx` + +**Change**: Filter the settings toggles to only show sections for enabled modules. + +```typescript +// ADD import +import { useModules } from '@/hooks/useModules'; +import { sectionRequiresModule } from '../types'; + +// Inside component: +const { isEnabled } = useModules(); + +// Filter section list in the settings dialog +const availableSections = allSections.filter((section) => { + const requiredModule = sectionRequiresModule(section.key); + if (!requiredModule) return true; + return isEnabled(requiredModule as any); +}); +``` + +**Estimated time**: 0.25 day +**Risk**: LOW + +--- + +### Task 2.4: Make useCEODashboard Hook Skip Disabled APIs + +**File to modify**: `src/hooks/useCEODashboard.ts` + +**Change**: Accept boolean flags for which APIs to call. When `dailyProduction: false`, skip that API call entirely (return empty data, no network request). + +```typescript +// The hook already accepts flags like { dailyProduction: true } +// Just ensure that when false is passed, useDashboardFetch returns +// a stable empty result without making a network request. + +// Verify the existing useDashboardFetch implementation handles this: +const dailyProduction = useDashboardFetch( + enabled.dailyProduction ? 'dashboard/production/summary' : null, // null = skip + // ... +); +``` + +**Estimated time**: 0.5 day (need to verify/modify useDashboardFetch) +**Risk**: LOW + +--- + +### Task 2.5: Fix CalendarSection Module Route References + +**File to modify**: `src/components/business/CEODashboard/sections/CalendarSection.tsx` + +**Change**: Replace hardcoded route strings with conditional navigation. + +```typescript +// BEFORE: +order: '/production/work-orders', +construction: '/construction/project/contract', + +// AFTER: +import { useModules } from '@/hooks/useModules'; +// Inside component: +const { isEnabled } = useModules(); + +// In click handler: +if (type === 'order' && isEnabled('production')) { + router.push('/production/work-orders'); +} else if (type === 'construction' && isEnabled('construction')) { + router.push('/construction/project/contract'); +} else { + // No navigation if module not available + toast.info('해당 모듈이 활성화되어 있지 않습니다.'); +} +``` + +**Estimated time**: 0.25 day +**Risk**: LOW + +--- + +### Task 2.6: Make Summary Nav Bar Module-Aware + +**File to modify**: `src/components/business/CEODashboard/useSectionSummary.ts` + +**Change**: Exclude module-dependent sections from summary calculation when module is disabled. + +**Estimated time**: 0.25 day +**Risk**: LOW + +--- + +### Phase 2 Summary + +| Task | Effort | Risk | Parallel? | +|------|--------|------|-----------| +| 2.1 Module-aware types | 0.25d | LOW | Yes | +| 2.2 CEODashboard module-aware | 0.5d | MEDIUM | After 2.1 | +| 2.3 Settings dialog | 0.25d | LOW | After 2.1 | +| 2.4 useCEODashboard skip | 0.5d | LOW | After 2.1 | +| 2.5 CalendarSection routes | 0.25d | LOW | After 2.1 | +| 2.6 Summary nav bar | 0.25d | LOW | After 2.1 | +| **Total** | **2d** | | | +| **Buffer** | **+0.5d** | | | +| **Phase 2 Total** | **2.5d** | | | + +**Phase 2 Exit Criteria**: +- Kyungdong tenant dashboard shows production/shipment sections, no construction +- Juil tenant dashboard shows construction section, no production/shipment +- Common-only tenant dashboard shows neither production nor construction sections +- Settings dialog only shows available sections +- No console errors, no failed API calls for disabled sections +- Calendar navigation gracefully handles missing modules + +--- + +## 5. Phase 3: Physical Separation + +> **Goal**: Organize the codebase so tenant-specific code is clearly demarcated and can be optionally excluded from builds. +> **Duration**: 2-3 days +> **Prerequisite**: Phase 2 complete +> **Risk**: LOW (reorganization, no behavior change) + +### Task 3.1: Create Module Boundary Markers + +Rather than physically moving files (which would create massive diffs and break git history), we establish **boundary markers** using barrel exports and documentation. + +**Files to create**: +- `src/components/production/MODULE.md` (module metadata) +- `src/components/quality/MODULE.md` +- `src/components/business/construction/MODULE.md` +- `src/components/vehicle-management/MODULE.md` + +```markdown +# MODULE.md -- Production Module + +**Module ID**: production +**Tenant**: Kyungdong (Shutter MES) +**Route Prefixes**: /production +**Component Count**: 56 files +**Dependencies on Common ERP**: + - @/lib/api/* (server actions, API client) + - @/components/ui/* (UI primitives) + - @/components/templates/* (list/detail templates) + - @/components/organisms/* (page layout) + - @/hooks/* (usePermission, etc.) + - @/types/process.ts (shared process types) + - @/stores/authStore (tenant info) + - @/stores/menuStore (sidebar state) +**Exports to Common ERP**: NONE (all cross-references resolved in Phase 0) +**Shared via @/interfaces/**: production-orders.ts +**Shared via @/components/document-system/**: InspectionReportModal, WorkLogModal +``` + +**Estimated time**: 0.25 day + +--- + +### Task 3.2: Verify Build With Module Stubbing + +Create a script that verifies the build can succeed when tenant modules are replaced with stubs. + +**File to create**: `scripts/verify-module-separation.sh` + +```bash +#!/bin/bash +# Verify that common ERP builds cleanly when tenant modules are stubbed. +# This does NOT actually build -- it checks for import violations. + +echo "Checking for forbidden imports (Common -> Tenant)..." + +# Define tenant-specific paths +TENANT_PATHS=( + "@/components/production/" + "@/components/quality/" + "@/components/business/construction/" + "@/components/vehicle-management/" +) + +# Define common ERP source directories (excluding tenant pages) +COMMON_DIRS=( + "src/components/approval" + "src/components/accounting" + "src/components/auth" + "src/components/atoms" + "src/components/board" + "src/components/business/CEODashboard" + "src/components/business/Dashboard.tsx" + "src/components/clients" + "src/components/common" + "src/components/customer-center" + "src/components/document-system" + "src/components/hr" + "src/components/items" + "src/components/layout" + "src/components/material" + "src/components/molecules" + "src/components/organisms" + "src/components/orders" + "src/components/outbound" + "src/components/pricing" + "src/components/providers" + "src/components/reports" + "src/components/settings" + "src/components/stocks" + "src/components/templates" + "src/components/ui" + "src/lib" + "src/hooks" + "src/stores" + "src/contexts" +) + +VIOLATIONS=0 + +for dir in "${COMMON_DIRS[@]}"; do + for tenant_path in "${TENANT_PATHS[@]}"; do + # Search for static imports from tenant paths + found=$(grep -rn "from ['\"]${tenant_path}" "$dir" --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v "// MODULE_SEPARATION_OK" | grep -v "dynamic(") + if [ -n "$found" ]; then + echo "VIOLATION: $dir imports from $tenant_path" + echo "$found" + VIOLATIONS=$((VIOLATIONS + 1)) + fi + done +done + +if [ $VIOLATIONS -eq 0 ]; then + echo "All clear. No forbidden imports found." + exit 0 +else + echo "Found $VIOLATIONS forbidden import(s). Fix before proceeding." + exit 1 +fi +``` + +**Estimated time**: 0.5 day +**Risk**: LOW (read-only verification) + +--- + +### Task 3.3: Sales Production-Orders Conditional Loading + +The `sales/order-management-sales/production-orders/` pages should only be accessible when the production module is enabled. + +**Strategy**: Add a `useModules` check at the top of these page components. + +**Files to modify**: +- `src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx` +- `src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx` +- `src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx` + +```typescript +// ADD to top of each page component: +import { useModules } from '@/hooks/useModules'; + +export default function ProductionOrdersPage() { + const { isEnabled } = useModules(); + + if (!isEnabled('production')) { + return ( + +
+

생산관리 모듈이 활성화되어 있지 않습니다.

+
+
+ ); + } + + // ... existing page content +} +``` + +**Estimated time**: 0.5 day +**Risk**: LOW + +--- + +### Task 3.4: Update tsconfig Path Aliases (Optional) + +For future package extraction, add path aliases that make module boundaries explicit. + +**File to modify**: `tsconfig.json` + +```json +{ + "compilerOptions": { + "paths": { + "@/*": ["./src/*"], + "@modules/*": ["./src/modules/*"], + "@interfaces/*": ["./src/interfaces/*"] + } + } +} +``` + +**Estimated time**: 0.25 day +**Risk**: LOW + +--- + +### Task 3.5: Document Module Boundaries in CLAUDE.md + +**File to modify**: Project `CLAUDE.md` + +Add a section documenting the module separation architecture: + +```markdown +## Module Separation Architecture +**Priority**: RED + +### Module Ownership +| Module ID | Route Prefixes | Component Path | Tenant | +|-----------|----------------|----------------|--------| +| common | /dashboard, /accounting, /sales, ... | src/components/{accounting,approval,...} | All | +| production | /production | src/components/production/ | Kyungdong | +| quality | /quality | src/components/quality/ | Kyungdong | +| construction | /construction | src/components/business/construction/ | Juil | +| vehicle-management | /vehicle-management | src/components/vehicle-management/ | Optional | + +### Dependency Rules +- ALLOWED: tenant module -> Common ERP (e.g., production -> @/lib/*) +- FORBIDDEN: Common ERP -> tenant module (e.g., approval -> production) +- SHARED: Use @/interfaces/ for types, @/components/document-system/ for shared modals +- DYNAMIC: Use next/dynamic for optional cross-module component loading + +### Verification +Run `scripts/verify-module-separation.sh` to check for forbidden imports. +``` + +**Estimated time**: 0.25 day +**Risk**: LOW + +--- + +### Phase 3 Summary + +| Task | Effort | Risk | Parallel? | +|------|--------|------|-----------| +| 3.1 Module boundary markers | 0.25d | LOW | Yes | +| 3.2 Verification script | 0.5d | LOW | Yes | +| 3.3 Sales conditional loading | 0.5d | LOW | Yes | +| 3.4 tsconfig paths | 0.25d | LOW | Yes | +| 3.5 Document in CLAUDE.md | 0.25d | LOW | Yes | +| **Total** | **1.75d** | | | +| **Buffer** | **+0.5d** | | | +| **Phase 3 Total** | **2.25d** | | | + +**Phase 3 Exit Criteria**: +- `verify-module-separation.sh` passes with 0 violations +- Each module has MODULE.md with dependency documentation +- Sales production-orders pages check module availability +- tsconfig has @modules/ and @interfaces/ aliases +- CLAUDE.md documents module separation rules + +--- + +## 6. Phase 4: Manifest-Based Module Loading (Future) + +> **Goal**: Backend-driven module configuration. No frontend code changes needed for new tenants. +> **Duration**: 5-8 days (separate project) +> **Prerequisite**: Phase 3 complete + backend API changes + +This phase is a separate project. Documenting the design here for reference. + +### 4.1: Backend Module Configuration API + +**New endpoint**: `GET /api/v1/tenant/modules` + +```json +{ + "tenant_id": 282, + "modules": [ + { + "id": "production", + "enabled": true, + "config": { + "features": ["work-orders", "worker-screen", "production-dashboard"], + "hidden_features": ["screen-production"] + } + }, + { + "id": "quality", + "enabled": true, + "config": { + "features": ["equipment", "inspections", "qms"] + } + } + ], + "dashboard_sections": ["production", "shipment", "unshipped"], + "menu_overrides": {} +} +``` + +### 4.2: Frontend Manifest Loader + +```typescript +// src/modules/manifest-loader.ts +export async function loadModuleManifest(tenantId: number): Promise { + const response = await fetch(`/api/proxy/tenant/modules`); + const data = await response.json(); + return data.modules; +} +``` + +### 4.3: Dynamic Route Generation + +Using Next.js catch-all routes with module manifest: + +```typescript +// src/app/[locale]/(protected)/[...slug]/page.tsx +// This catch-all route handles module pages that are dynamically enabled. +// Phase 4 only -- requires significant backend work. +``` + +### 4.4: Module Feature Flags + +Fine-grained control within modules: + +```typescript +// e.g., production module has features: work-orders, worker-screen, etc. +// A tenant might have production enabled but worker-screen disabled +function isFeatureEnabled(moduleId: string, featureId: string): boolean; +``` + +--- + +## 7. Testing Strategy + +### Phase 0 Testing + +| Test | Method | Who | +|------|--------|-----| +| ApprovalBox renders correctly | Manual: navigate to /approval/inbox, open a work_order linked document | User | +| Sales production-orders list works | Manual: /sales/order-management-sales/production-orders | User | +| Sales production-order detail works | Manual: click an item in the list above | User | +| QMS page renders | Manual: /quality/qms | User | +| Dashboard invalidation still works | Manual: create a work order, navigate to dashboard, verify production section refreshes | User | +| Build passes | User runs `npm run build` | User | + +### Phase 1 Testing + +| Test | Method | Who | +|------|--------|-----| +| Module guard blocks unauthorized routes | Set tenant industry to 'construction', navigate to /production | Dev | +| Module guard allows authorized routes | Set tenant industry to 'shutter_mes', navigate to /production | Dev | +| Common routes always accessible | Navigate to /accounting, /sales, /hr with any tenant | Dev | +| Module-denied toast appears | Navigate to denied route, verify redirect + toast | Dev | +| Build passes | User runs `npm run build` | User | + +### Phase 2 Testing + +| Test | Method | Who | +|------|--------|-----| +| Kyungdong dashboard shows production | Log in as Kyungdong tenant, check dashboard sections | Dev | +| Kyungdong dashboard hides construction | Same login, verify no construction section | Dev | +| Juil dashboard shows construction | Log in as Juil tenant, check dashboard | Dev | +| Juil dashboard hides production | Same login, verify no production/shipment sections | Dev | +| Common tenant shows neither | Log in as common-only tenant, check dashboard | Dev | +| Settings dialog matches | Open settings, verify only available sections shown | Dev | +| Calendar navigation handles missing modules | Click calendar item that would go to disabled module | Dev | +| No console errors | Check browser console during all above tests | Dev | +| Build passes | User runs `npm run build` | User | + +### Phase 3 Testing + +| Test | Method | Who | +|------|--------|-----| +| Verification script passes | Run `scripts/verify-module-separation.sh` | Dev | +| Sales production-orders disabled for non-production tenant | Navigate as construction tenant | Dev | +| Full regression test | Navigate all major pages as each tenant type | Dev + User | +| Build passes | User runs `npm run build` | User | + +--- + +## 8. Risk Register + +| # | Risk | Probability | Impact | Mitigation | Phase | +|---|------|------------|--------|------------|-------| +| R1 | InspectionReportModal dynamic import fails silently | LOW | HIGH | Error boundary wrapper, fallback UI, monitoring | 0 | +| R2 | Sales production-orders breaks after interface extraction | MEDIUM | HIGH | Keep original actions.ts as fallback, test all 3 pages | 0 | +| R3 | Middleware route guard blocks legitimate access | MEDIUM | CRITICAL | Start with client-side guard (softer failure), add middleware later | 1 | +| R4 | Tenant industry field not set for existing tenants | HIGH | MEDIUM | Default to 'all modules enabled' when industry is undefined | 1 | +| R5 | Dashboard section removal changes layout/spacing | LOW | LOW | Test each section combination, verify CSS grid/flex behavior | 2 | +| R6 | Dashboard API call failure on disabled module endpoint | LOW | MEDIUM | Graceful null handling already exists (data ?? fallback) | 2 | +| R7 | Build size increases due to dynamic imports | LOW | LOW | Measure bundle size before/after, dynamic imports reduce initial bundle | 3 | +| R8 | Developer accidentally adds forbidden import | MEDIUM | LOW | Verification script in CI, CLAUDE.md rules, MODULE.md docs | 3 | + +### Rollback Strategy Per Phase + +| Phase | Rollback Method | Time to Rollback | +|-------|----------------|------------------| +| Phase 0 | `git revert` each task commit | < 5 minutes | +| Phase 1 | Remove ModuleGuard from layout.tsx, revert middleware | < 10 minutes | +| Phase 2 | Revert CEODashboard changes (remove isEnabled checks) | < 10 minutes | +| Phase 3 | No runtime changes to revert (documentation + scripts only) | N/A | + +--- + +## 9. Folder Structure Before/After + +### Before (Current) + +``` +src/ + app/[locale]/(protected)/ + accounting/ # Common ERP + approval/ # Common ERP (imports from production -- VIOLATION) + board/ # Common ERP + construction/ # Juil tenant + customer-center/ # Common ERP + dashboard/ # Common ERP (renders production/construction -- VIOLATION) + hr/ # Common ERP + master-data/ # Common ERP + material/ # Common ERP + outbound/ # Common ERP + production/ # Kyungdong tenant + quality/ # Kyungdong tenant + reports/ # Common ERP + sales/ # Common ERP (imports from production -- VIOLATION) + settings/ # Common ERP + vehicle-management/ # Optional + components/ + approval/ # imports InspectionReportModal from production + business/ + CEODashboard/ # hardcodes production/construction sections + construction/ # Juil tenant components + production/ # Kyungdong tenant components + quality/ # Kyungdong tenant components + vehicle-management/ # Optional components + lib/ + dashboard-invalidation.ts # hardcodes production/construction +``` + +### After (End of Phase 3) + +``` +src/ + modules/ # NEW + index.ts # module registry + types.ts # ModuleId, ModuleManifest, TenantModuleConfig + tenant-config.ts # industry -> module mapping + route-resolver.ts # tenant-aware route resolution + interfaces/ # NEW + production-orders.ts # shared types + actions for sales <-> production + components/ + AssigneeSelectModal.tsx # dynamic import wrapper + app/[locale]/(protected)/ + accounting/ # Common ERP + approval/ # Common ERP (no more production imports) + board/ # Common ERP + construction/ # Juil tenant (guarded by ModuleGuard) + customer-center/ # Common ERP + dashboard/ # Common ERP (conditionally renders sections) + hr/ # Common ERP + master-data/ # Common ERP + material/ # Common ERP + outbound/ # Common ERP + production/ # Kyungdong tenant (guarded by ModuleGuard) + quality/ # Kyungdong tenant (guarded by ModuleGuard) + reports/ # Common ERP + sales/ # Common ERP (production-orders guarded) + settings/ # Common ERP + vehicle-management/ # Optional (guarded by ModuleGuard) + components/ + auth/ + ModuleGuard.tsx # NEW: route-level module access check + approval/ # clean (no production imports) + business/ + CEODashboard/ # module-aware section rendering + construction/ # Juil tenant (with MODULE.md) + document-system/ + modals/ # NEW: shared modals + InspectionReportModal.tsx # dynamic import wrapper + WorkLogModal.tsx # dynamic import wrapper + index.ts + production/ # Kyungdong tenant (with MODULE.md) + WorkOrders/documents/ + InspectionReportModal.tsx # re-exports from document-system + WorkLogModal.tsx # re-exports from document-system + quality/ # Kyungdong tenant (with MODULE.md) + vehicle-management/ # Optional (with MODULE.md) + hooks/ + useModules.ts # NEW: tenant module access hook + lib/ + dashboard-invalidation.ts # registry-based (no hardcoded modules) + scripts/ + verify-module-separation.sh # NEW: import violation checker +``` + +--- + +## 10. Migration Order & Parallelism + +### Execution Timeline + +``` +Week 1 (Phase 0): + Day 1-2: Tasks 0.1, 0.2, 0.3, 0.4 (all parallel) + Day 3: Task 0.5, 0.6 + Day 4: Phase 0 testing + buffer + +Week 2 (Phase 1 + Phase 2): + Day 1: Tasks 1.1, 1.2, 1.3 (parallel) + Day 2: Tasks 1.4, 1.5 (sequential) + Day 3: Task 1.6 + Phase 1 testing + Day 4: Tasks 2.1, 2.2, 2.3 (2.1 first, then parallel) + Day 5: Tasks 2.4, 2.5, 2.6 + Phase 2 testing + +Week 3 (Phase 3 + Buffer): + Day 1: Tasks 3.1, 3.2, 3.3, 3.4, 3.5 (all parallel) + Day 2: Phase 3 testing + full regression + Day 3: Buffer / bug fixes +``` + +### Parallelism Map + +``` +Phase 0: + [0.1 InspReportModal] [0.2 ProdOrders Interface] [0.3 Route Nav] [0.4 QMS Types] + | | | | + v v | | + [0.5 Dev Gen] ----------------+ | + | | + [0.6 Dashboard Invalidation] --+----------------------------------------+ + | + v + Phase 0 Testing + +Phase 1: + [1.1 Types] ----+----> [1.2 Registry] ----+ + | | + +----> [1.3 Tenant Config]-+----> [1.4 useModules] ----> [1.5 Route Guard] ----> [1.6 Toast] + | + v + Phase 1 Testing + +Phase 2: + [2.1 Module-aware types] ----+----> [2.2 Dashboard] + |----> [2.3 Settings Dialog] + |----> [2.4 Hook Skip] + |----> [2.5 Calendar Routes] + +----> [2.6 Summary Nav] + | + v + Phase 2 Testing + +Phase 3: + [3.1 MODULE.md] [3.2 Verify Script] [3.3 Sales Guard] [3.4 tsconfig] [3.5 CLAUDE.md] + | | | | | + v v v v v + Phase 3 Testing + Full Regression +``` + +### What Can Be Done Today (Before Backend Changes) + +Everything in Phases 0-3 can proceed without backend changes. The `useModules` hook falls back to industry-based module resolution using the existing `tenant.options.industry` field in the auth store. The only backend requirement is that this field is populated correctly for existing tenants. + +If `industry` is not set for some tenants, the system defaults to showing all modules (current behavior), so there is zero risk of breaking existing functionality. + +--- + +## Appendix A: File Count Summary + +| Category | Files | Pages | +|----------|-------|-------| +| Common ERP components | ~400+ | ~165 | +| Production (Kyungdong) | 56 component files | 12 pages | +| Quality (Kyungdong) | 35+ component files | 14 pages | +| Construction (Juil) | 161 component files | 57 pages | +| Vehicle Management (Optional) | 13 component files | 12 pages | +| **Total** | ~665+ | 275 | + +## Appendix B: New Files Created (All Phases) + +| File | Phase | Purpose | +|------|-------|---------| +| `src/modules/types.ts` | 1 | Module type definitions | +| `src/modules/index.ts` | 1 | Module registry | +| `src/modules/tenant-config.ts` | 1 | Tenant-to-module mapping | +| `src/modules/route-resolver.ts` | 0 | Tenant-aware route resolution | +| `src/interfaces/production-orders.ts` | 0 | Shared production order types/actions | +| `src/interfaces/components/AssigneeSelectModal.tsx` | 0 | Dynamic import wrapper | +| `src/components/document-system/modals/InspectionReportModal.tsx` | 0 | Dynamic import wrapper | +| `src/components/document-system/modals/WorkLogModal.tsx` | 0 | Dynamic import wrapper | +| `src/components/document-system/modals/index.ts` | 0 | Barrel export | +| `src/components/auth/ModuleGuard.tsx` | 1 | Route-level module check | +| `src/hooks/useModules.ts` | 1 | Module access hook | +| `scripts/verify-module-separation.sh` | 3 | Import violation checker | +| `src/components/production/MODULE.md` | 3 | Module boundary doc | +| `src/components/quality/MODULE.md` | 3 | Module boundary doc | +| `src/components/business/construction/MODULE.md` | 3 | Module boundary doc | +| `src/components/vehicle-management/MODULE.md` | 3 | Module boundary doc | + +**Total new files**: 16 +**Total modified files**: ~20 + +## Appendix C: Backend API Request Summary + +| # | Type | Endpoint | Description | Required Phase | +|---|------|----------|-------------|----------------| +| B1 | MODIFY | `GET /api/auth/user` | Add `tenant.options.modules` array | Phase 1 (optional, has fallback) | +| B2 | NEW | `GET /api/v1/tenant/modules` | Full module configuration | Phase 4 | + +--- + +**Document Version**: 1.0 +**Author**: Claude (System Architect analysis) +**Review Required By**: Backend team (B1, B2), Frontend lead (all phases) diff --git a/src/components/material/ReceivingManagement/ReceivingDetail.tsx b/src/components/material/ReceivingManagement/ReceivingDetail.tsx index 711354fb..b70810fa 100644 --- a/src/components/material/ReceivingManagement/ReceivingDetail.tsx +++ b/src/components/material/ReceivingManagement/ReceivingDetail.tsx @@ -91,16 +91,6 @@ const INITIAL_FORM_DATA: Partial = { inventoryAdjustments: [], }; -// 로트번호 생성 (YYMMDD-NN) -function generateLotNo(): string { - const now = new Date(); - const yy = String(now.getFullYear()).slice(-2); - const mm = String(now.getMonth() + 1).padStart(2, '0'); - const dd = String(now.getDate()).padStart(2, '0'); - const seq = String(Math.floor(Math.random() * 100)).padStart(2, '0'); - return `${yy}${mm}${dd}-${seq}`; -} - // localStorage에서 로그인 사용자 정보 가져오기 function getLoggedInUser(): { name: string; department: string } { if (typeof window === 'undefined') return { name: '', department: '' }; @@ -169,7 +159,6 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) { const data = generateReceivingData(); setFormData((prev) => ({ ...prev, - lotNo: generateLotNo(), itemCode: data.itemCode, itemName: data.itemName, specification: data.specification, @@ -574,15 +563,12 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) { /> - {/* 원자재로트 - 수정 가능 */} + {/* 원자재로트 - 자동채번 (readOnly) */}
- setFormData((prev) => ({ ...prev, lotNo: e.target.value }))} - placeholder="원자재로트를 입력하세요" - /> +
+ {formData.lotNo || (isNewMode ? '저장 시 자동 생성' : '-')} +
{/* 품목코드 - 검색 모달 선택 */} diff --git a/src/components/material/ReceivingManagement/ReceivingProcessDialog.tsx b/src/components/material/ReceivingManagement/ReceivingProcessDialog.tsx index a52621e7..266f9f02 100644 --- a/src/components/material/ReceivingManagement/ReceivingProcessDialog.tsx +++ b/src/components/material/ReceivingManagement/ReceivingProcessDialog.tsx @@ -23,16 +23,6 @@ import { import { Alert, AlertDescription } from '@/components/ui/alert'; import type { ReceivingDetail, ReceivingProcessFormData } from './types'; -// LOT 번호 생성 함수 (YYMMDD-NN 형식) -function generateLotNo(): string { - const now = new Date(); - const yy = String(now.getFullYear()).slice(-2); - const mm = String(now.getMonth() + 1).padStart(2, '0'); - const dd = String(now.getDate()).padStart(2, '0'); - const random = String(Math.floor(Math.random() * 100)).padStart(2, '0'); - return `${yy}${mm}${dd}-${random}`; -} - interface Props { open: boolean; onOpenChange: (open: boolean) => void; @@ -42,7 +32,7 @@ interface Props { export function ReceivingProcessDialog({ open, onOpenChange, detail, onComplete }: Props) { // 폼 데이터 - const [receivingLot, setReceivingLot] = useState(() => generateLotNo()); + const [receivingLot, setReceivingLot] = useState(''); const [supplierLot, setSupplierLot] = useState(''); const [receivingQty, setReceivingQty] = useState((detail.orderQty ?? 0).toString()); const [receivingLocation, setReceivingLocation] = useState(''); @@ -56,9 +46,7 @@ export function ReceivingProcessDialog({ open, onOpenChange, detail, onComplete const validateForm = useCallback((): boolean => { const errors: string[] = []; - if (!receivingLot.trim()) { - errors.push('입고LOT는 필수 입력 항목입니다.'); - } + // 입고LOT는 API가 자동 채번하므로 필수 검증 제거 if (!receivingQty.trim() || isNaN(Number(receivingQty)) || Number(receivingQty) <= 0) { errors.push('입고수량은 필수 입력 항목입니다. 유효한 숫자를 입력해주세요.'); @@ -68,7 +56,7 @@ export function ReceivingProcessDialog({ open, onOpenChange, detail, onComplete setValidationErrors(errors); return errors.length === 0; - }, [receivingLot, receivingQty]); + }, [receivingQty]); // 입고 처리 const handleSubmit = useCallback(async () => { @@ -161,16 +149,12 @@ export function ReceivingProcessDialog({ open, onOpenChange, detail, onComplete {/* 입력 필드 */}
- + { - setReceivingLot(e.target.value); - setValidationErrors([]); - }} - placeholder="예: 251223-41" + readOnly + className="bg-gray-50" + placeholder="저장 시 자동 생성" />
diff --git a/src/components/material/ReceivingManagement/actions.ts b/src/components/material/ReceivingManagement/actions.ts index db78735d..9e9fb219 100644 --- a/src/components/material/ReceivingManagement/actions.ts +++ b/src/components/material/ReceivingManagement/actions.ts @@ -497,7 +497,7 @@ function transformFrontendToApi( if (data.remark !== undefined) result.remark = data.remark; if (data.receivingQty !== undefined) result.receiving_qty = data.receivingQty; if (data.receivingDate !== undefined) result.receiving_date = data.receivingDate; - if (data.lotNo !== undefined) result.lot_no = data.lotNo; + // lot_no는 API가 자동 채번하므로 프론트에서 전송하지 않음 if (data.supplierMaterialNo !== undefined) result.material_no = data.supplierMaterialNo; if (data.manufacturer !== undefined) result.manufacturer = data.manufacturer; if (data.certificateFileId !== undefined) result.certificate_file_id = data.certificateFileId; @@ -511,7 +511,7 @@ function transformProcessDataToApi( ): Record { return { receiving_qty: data.receivingQty, - lot_no: data.receivingLot, + // lot_no는 API가 자동 채번하므로 프론트에서 전송하지 않음 supplier_lot: data.supplierLot, receiving_location: data.receivingLocation, remark: data.remark, diff --git a/src/components/pricing/PricingListClient.tsx b/src/components/pricing/PricingListClient.tsx index ba867b9e..4aff63be 100644 --- a/src/components/pricing/PricingListClient.tsx +++ b/src/components/pricing/PricingListClient.tsx @@ -13,7 +13,6 @@ import { Package, AlertCircle, CheckCircle2, - RefreshCw, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -326,18 +325,19 @@ export function PricingListClient({ }; // 헤더 액션 (함수로 정의) - const headerActions = () => ( - - ); + // NOTE: 품목 마스터 동기화 버튼 - 실제 로직 미구현 상태로 주석처리 + // const headerActions = () => ( + // + // ); // UniversalListPage 설정 const pricingConfig: UniversalListConfig = { @@ -356,7 +356,7 @@ export function PricingListClient({ }, columns: tableColumns, - headerActions, + // headerActions, // 품목 마스터 동기화 버튼 미구현으로 주석처리 stats, tabs,