From d90a3a9aff1a36c24ff5f23d79f4a168ee43e3d0 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 8 Apr 2026 10:41:32 +0900 Subject: [PATCH 1/3] feat: Update smart factory log scheduling and enhance item info page - Modified the upsertSchedule function to regenerate today's plan immediately after schedule changes, ensuring that already sent users are automatically excluded. - Increased the data size limit for item info page requests from 500 to 99999 for better data handling across multiple companies. - Added new pages for design request management, task management, and equipment info, providing comprehensive tools for managing design and equipment processes. These updates aim to improve the efficiency and usability of the smart factory log and item management systems, enhancing user experience and operational effectiveness. --- .../controllers/smartFactoryLogController.ts | 6 +- .../COMPANY_10/master-data/item-info/page.tsx | 2 +- .../COMPANY_16/master-data/item-info/page.tsx | 2 +- .../COMPANY_29/master-data/item-info/page.tsx | 2 +- .../design/change-management/page.tsx | 1524 +++++++++ .../COMPANY_30/design/design-request/page.tsx | 781 +++++ .../(main)/COMPANY_30/design/my-work/page.tsx | 1958 ++++++++++++ .../(main)/COMPANY_30/design/project/page.tsx | 1501 +++++++++ .../design/task-management/page.tsx | 1288 ++++++++ .../(main)/COMPANY_30/equipment/info/page.tsx | 945 ++++++ .../equipment/plc-settings/page.tsx | 501 +++ .../(main)/COMPANY_30/logistics/info/page.tsx | 905 ++++++ .../COMPANY_30/logistics/inventory/page.tsx | 738 +++++ .../logistics/material-status/page.tsx | 608 ++++ .../COMPANY_30/logistics/outbound/page.tsx | 1789 +++++++++++ .../COMPANY_30/logistics/packaging/page.tsx | 1031 ++++++ .../COMPANY_30/logistics/receiving/page.tsx | 1757 +++++++++++ .../COMPANY_30/logistics/warehouse/page.tsx | 1608 ++++++++++ .../COMPANY_30/master-data/company/page.tsx | 848 +++++ .../master-data/department/page.tsx | 767 +++++ .../COMPANY_30/master-data/item-info/page.tsx | 535 ++++ .../COMPANY_30/master-data/options/page.tsx | 136 + .../app/(main)/COMPANY_30/mold/info/page.tsx | 1450 +++++++++ .../COMPANY_30/monitoring/equipment/page.tsx | 575 ++++ .../COMPANY_30/monitoring/production/page.tsx | 504 +++ .../COMPANY_30/monitoring/quality/page.tsx | 512 +++ .../outsourcing/subcontractor-item/page.tsx | 577 ++++ .../outsourcing/subcontractor/page.tsx | 1410 +++++++++ .../(main)/COMPANY_30/production/bom/page.tsx | 2521 +++++++++++++++ .../production/plan-management/page.tsx | 1825 +++++++++++ .../process-info/ItemRoutingTab.tsx | 994 ++++++ .../process-info/ProcessMasterTab.tsx | 664 ++++ .../process-info/ProcessWorkStandardTab.tsx | 17 + .../production/process-info/page.tsx | 223 ++ .../WorkStandardEditModal.tsx | 1007 ++++++ .../production/work-instruction/page.tsx | 843 +++++ .../(main)/COMPANY_30/purchase/order/page.tsx | 1267 ++++++++ .../purchase/purchase-item/page.tsx | 1240 ++++++++ .../COMPANY_30/purchase/supplier/page.tsx | 2762 +++++++++++++++++ .../COMPANY_30/quality/inspection/page.tsx | 1703 ++++++++++ .../quality/item-inspection/page.tsx | 721 +++++ .../(main)/COMPANY_30/sales/claim/page.tsx | 932 ++++++ .../(main)/COMPANY_30/sales/customer/page.tsx | 2760 ++++++++++++++++ .../(main)/COMPANY_30/sales/order/page.tsx | 1143 +++++++ .../(main)/COMPANY_30/sales/quote/page.tsx | 995 ++++++ .../COMPANY_30/sales/sales-item/page.tsx | 1242 ++++++++ .../COMPANY_30/sales/shipping-order/page.tsx | 928 ++++++ .../COMPANY_30/sales/shipping-plan/page.tsx | 454 +++ .../COMPANY_7/master-data/item-info/page.tsx | 2 +- .../COMPANY_8/master-data/item-info/page.tsx | 2 +- .../design/change-management/page.tsx | 1524 +++++++++ .../COMPANY_9/design/design-request/page.tsx | 781 +++++ .../(main)/COMPANY_9/design/my-work/page.tsx | 1958 ++++++++++++ .../(main)/COMPANY_9/design/project/page.tsx | 1501 +++++++++ .../COMPANY_9/design/task-management/page.tsx | 1288 ++++++++ .../(main)/COMPANY_9/equipment/info/page.tsx | 945 ++++++ .../COMPANY_9/equipment/plc-settings/page.tsx | 501 +++ .../(main)/COMPANY_9/logistics/info/page.tsx | 905 ++++++ .../COMPANY_9/logistics/inventory/page.tsx | 738 +++++ .../logistics/material-status/page.tsx | 608 ++++ .../COMPANY_9/logistics/outbound/page.tsx | 1789 +++++++++++ .../COMPANY_9/logistics/packaging/page.tsx | 1031 ++++++ .../COMPANY_9/logistics/receiving/page.tsx | 1757 +++++++++++ .../COMPANY_9/logistics/warehouse/page.tsx | 1608 ++++++++++ .../COMPANY_9/master-data/company/page.tsx | 848 +++++ .../COMPANY_9/master-data/department/page.tsx | 767 +++++ .../COMPANY_9/master-data/item-info/page.tsx | 535 ++++ .../COMPANY_9/master-data/options/page.tsx | 136 + .../app/(main)/COMPANY_9/mold/info/page.tsx | 1450 +++++++++ .../COMPANY_9/monitoring/equipment/page.tsx | 575 ++++ .../COMPANY_9/monitoring/production/page.tsx | 504 +++ .../COMPANY_9/monitoring/quality/page.tsx | 512 +++ .../outsourcing/subcontractor-item/page.tsx | 577 ++++ .../outsourcing/subcontractor/page.tsx | 1410 +++++++++ .../(main)/COMPANY_9/production/bom/page.tsx | 2521 +++++++++++++++ .../production/plan-management/page.tsx | 1825 +++++++++++ .../process-info/ItemRoutingTab.tsx | 994 ++++++ .../process-info/ProcessMasterTab.tsx | 664 ++++ .../process-info/ProcessWorkStandardTab.tsx | 17 + .../production/process-info/page.tsx | 223 ++ .../WorkStandardEditModal.tsx | 1007 ++++++ .../production/work-instruction/page.tsx | 843 +++++ .../(main)/COMPANY_9/purchase/order/page.tsx | 1267 ++++++++ .../COMPANY_9/purchase/purchase-item/page.tsx | 1240 ++++++++ .../COMPANY_9/purchase/supplier/page.tsx | 2762 +++++++++++++++++ .../COMPANY_9/quality/inspection/page.tsx | 1703 ++++++++++ .../quality/item-inspection/page.tsx | 721 +++++ .../app/(main)/COMPANY_9/sales/claim/page.tsx | 932 ++++++ .../(main)/COMPANY_9/sales/customer/page.tsx | 2760 ++++++++++++++++ .../app/(main)/COMPANY_9/sales/quote/page.tsx | 995 ++++++ .../COMPANY_9/sales/sales-item/page.tsx | 1242 ++++++++ .../COMPANY_9/sales/shipping-order/page.tsx | 928 ++++++ .../COMPANY_9/sales/shipping-plan/page.tsx | 454 +++ .../components/layout/AdminPageRenderer.tsx | 161 + 94 files changed, 96005 insertions(+), 7 deletions(-) create mode 100644 frontend/app/(main)/COMPANY_30/design/change-management/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/design/design-request/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/design/my-work/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/design/project/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/design/task-management/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/equipment/info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/equipment/plc-settings/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/logistics/info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/logistics/inventory/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/logistics/material-status/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/logistics/outbound/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/logistics/packaging/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/logistics/receiving/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/logistics/warehouse/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/master-data/company/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/master-data/department/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/master-data/item-info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/master-data/options/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/mold/info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/monitoring/equipment/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/monitoring/production/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/monitoring/quality/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/outsourcing/subcontractor-item/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/outsourcing/subcontractor/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/production/bom/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/production/plan-management/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/production/process-info/ItemRoutingTab.tsx create mode 100644 frontend/app/(main)/COMPANY_30/production/process-info/ProcessMasterTab.tsx create mode 100644 frontend/app/(main)/COMPANY_30/production/process-info/ProcessWorkStandardTab.tsx create mode 100644 frontend/app/(main)/COMPANY_30/production/process-info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/production/work-instruction/WorkStandardEditModal.tsx create mode 100644 frontend/app/(main)/COMPANY_30/production/work-instruction/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/purchase/order/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/purchase/purchase-item/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/purchase/supplier/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/quality/inspection/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/quality/item-inspection/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/sales/claim/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/sales/customer/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/sales/order/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/sales/quote/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/sales/sales-item/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/sales/shipping-order/page.tsx create mode 100644 frontend/app/(main)/COMPANY_30/sales/shipping-plan/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/design/change-management/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/design/design-request/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/design/my-work/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/design/project/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/design/task-management/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/equipment/info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/equipment/plc-settings/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/logistics/info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/logistics/inventory/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/logistics/material-status/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/logistics/outbound/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/logistics/packaging/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/logistics/receiving/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/logistics/warehouse/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/master-data/company/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/master-data/department/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/master-data/item-info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/master-data/options/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/mold/info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/monitoring/equipment/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/monitoring/production/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/monitoring/quality/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/outsourcing/subcontractor-item/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/outsourcing/subcontractor/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/production/bom/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/production/process-info/ItemRoutingTab.tsx create mode 100644 frontend/app/(main)/COMPANY_9/production/process-info/ProcessMasterTab.tsx create mode 100644 frontend/app/(main)/COMPANY_9/production/process-info/ProcessWorkStandardTab.tsx create mode 100644 frontend/app/(main)/COMPANY_9/production/process-info/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/production/work-instruction/WorkStandardEditModal.tsx create mode 100644 frontend/app/(main)/COMPANY_9/production/work-instruction/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/purchase/order/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/purchase/purchase-item/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/purchase/supplier/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/quality/inspection/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/sales/claim/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/sales/customer/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/sales/quote/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/sales/sales-item/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/sales/shipping-order/page.tsx create mode 100644 frontend/app/(main)/COMPANY_9/sales/shipping-plan/page.tsx diff --git a/backend-node/src/controllers/smartFactoryLogController.ts b/backend-node/src/controllers/smartFactoryLogController.ts index 6f98d37c..baff84e4 100644 --- a/backend-node/src/controllers/smartFactoryLogController.ts +++ b/backend-node/src/controllers/smartFactoryLogController.ts @@ -9,6 +9,7 @@ import { encryptionService } from "../services/encryptionService"; import { sendSmartFactoryLog, getTodayPlanStatus, + planDailySends, } from "../utils/smartFactoryLog"; /** @@ -277,8 +278,9 @@ export const upsertSchedule = async ( ] ); - // 계획은 매일 00:05에만 생성 (즉시 재생성하면 지난 시각 소급 전송 위험) - res.json({ success: true, message: "스케줄이 저장되었습니다. 내일 00:05부터 적용됩니다." }); + // 스케줄 변경 후 오늘 계획 즉시 재생성 (이미 전송된 사용자는 자동 제외됨) + await planDailySends(); + res.json({ success: true, message: "스케줄이 저장되었습니다." }); } catch (error) { logger.error("스케줄 저장 실패:", error); res.status(500).json({ success: false, message: "스케줄 저장 실패" }); diff --git a/frontend/app/(main)/COMPANY_10/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_10/master-data/item-info/page.tsx index fec0f371..3f037275 100644 --- a/frontend/app/(main)/COMPANY_10/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_10/master-data/item-info/page.tsx @@ -197,7 +197,7 @@ export default function ItemInfoPage() { const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); const res = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, { page: 1, - size: 500, + size: 99999, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); diff --git a/frontend/app/(main)/COMPANY_16/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_16/master-data/item-info/page.tsx index fec0f371..3f037275 100644 --- a/frontend/app/(main)/COMPANY_16/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/master-data/item-info/page.tsx @@ -197,7 +197,7 @@ export default function ItemInfoPage() { const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); const res = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, { page: 1, - size: 500, + size: 99999, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); diff --git a/frontend/app/(main)/COMPANY_29/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_29/master-data/item-info/page.tsx index fec0f371..3f037275 100644 --- a/frontend/app/(main)/COMPANY_29/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_29/master-data/item-info/page.tsx @@ -197,7 +197,7 @@ export default function ItemInfoPage() { const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); const res = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, { page: 1, - size: 500, + size: 99999, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); diff --git a/frontend/app/(main)/COMPANY_30/design/change-management/page.tsx b/frontend/app/(main)/COMPANY_30/design/change-management/page.tsx new file mode 100644 index 00000000..39f06769 --- /dev/null +++ b/frontend/app/(main)/COMPANY_30/design/change-management/page.tsx @@ -0,0 +1,1524 @@ +"use client"; + +import React, { useState, useMemo, useEffect, useCallback } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { + Plus, + Save, + Inbox, + Pencil, + FileText, + XCircle, + ArrowRight, + Paperclip, + Upload, + Loader2, + Settings2, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { toast } from "sonner"; +import { + getDesignRequestList, + createDesignRequest, + updateDesignRequest, + addRequestHistory, + getEcnList, + createEcn, + updateEcn, +} from "@/lib/api/design"; +import { useTableSettings } from "@/hooks/useTableSettings"; +import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; + +// --- Types --- +type ChangeType = "설계오류" | "원가절감" | "고객요청" | "공정개선" | "법규대응"; +type EcrStatus = "요청접수" | "영향도분석" | "ECN발행" | "기각"; +type EcnStatus = "ECN발행" | "도면변경" | "통보완료" | "적용완료"; +type TabType = "ecr" | "ecn"; + +interface EcrHistory { + status: string; + date: string; + user: string; + desc: string; +} + +interface EcrItem { + id: string; + _id?: string; + date: string; + changeType: ChangeType; + urgency: "보통" | "긴급"; + status: EcrStatus; + target: string; + drawingNo: string; + reqDept: string; + requester: string; + reason: string; + content: string; + impact: string[]; + applyTiming: string; + ecnNo: string; + history: EcrHistory[]; +} + +interface EcnItem { + id: string; + _id?: string; + ecrNo: string; + ecrId?: string; + date: string; + applyDate: string; + status: EcnStatus; + target: string; + drawingBefore: string; + drawingAfter: string; + designer: string; + before: string; + after: string; + reason: string; + notifyDepts: string[]; + remark: string; + history: EcrHistory[]; +} + +// --- Style Helpers --- +const getChangeTypeStyle = (type: ChangeType) => { + switch (type) { + case "설계오류": + return "bg-destructive/10 text-destructive border-destructive/20"; + case "원가절감": + return "bg-success/10 text-success border-success/20"; + case "고객요청": + return "bg-info/10 text-info border-info/20"; + case "공정개선": + return "bg-warning/10 text-warning border-warning/20"; + case "법규대응": + return "bg-primary/10 text-primary border-primary/20"; + default: + return "bg-muted text-muted-foreground border-border"; + } +}; + +const getEcrStatusStyle = (status: EcrStatus) => { + switch (status) { + case "요청접수": + return "bg-info/10 text-info border-info/20"; + case "영향도분석": + return "bg-warning/10 text-warning border-warning/20"; + case "ECN발행": + return "bg-success/10 text-success border-success/20"; + case "기각": + return "bg-muted text-muted-foreground border-border"; + default: + return "bg-muted text-muted-foreground border-border"; + } +}; + +const getEcnStatusStyle = (status: EcnStatus) => { + switch (status) { + case "ECN발행": + return "bg-info/10 text-info border-info/20"; + case "도면변경": + return "bg-primary/10 text-primary border-primary/20"; + case "통보완료": + return "bg-warning/10 text-warning border-warning/20"; + case "적용완료": + return "bg-success/10 text-success border-success/20"; + default: + return "bg-muted text-muted-foreground border-border"; + } +}; + +const getImpactBadgeStyle = (impact: string) => { + switch (impact) { + case "BOM": + return "bg-info/10 text-info border-info/20"; + case "공정": + return "bg-warning/10 text-warning border-warning/20"; + case "금형": + return "bg-destructive/10 text-destructive border-destructive/20"; + case "검사기준": + return "bg-primary/10 text-primary border-primary/20"; + case "구매": + case "원가": + return "bg-success/10 text-success border-success/20"; + default: + return "bg-muted text-muted-foreground border-border"; + } +}; + +const getTimelineStatusStyle = (status: string) => { + switch (status) { + case "기각": + return "bg-muted text-muted-foreground border-border"; + case "적용완료": + case "ECN발행": + return "bg-success/10 text-success border-success/20"; + case "영향도분석": + return "bg-warning/10 text-warning border-warning/20"; + case "도면변경": + return "bg-primary/10 text-primary border-primary/20"; + case "통보완료": + return "bg-info/10 text-info border-info/20"; + default: + return "bg-info/10 text-info border-info/20"; + } +}; + +// --- Constants --- +const CHANGE_TYPES: ChangeType[] = ["설계오류", "원가절감", "고객요청", "공정개선", "법규대응"]; +const ECR_STATUSES: EcrStatus[] = ["요청접수", "영향도분석", "ECN발행", "기각"]; +const ECN_STATUSES: EcnStatus[] = ["ECN발행", "도면변경", "통보완료", "적용완료"]; +const DEPARTMENTS = ["품질팀", "생산팀", "영업팀", "구매팀", "설계팀"]; +const DESIGNERS = ["이설계", "박도면", "최기구", "김전장"]; +const IMPACT_OPTIONS = [ + { key: "BOM", label: "BOM 변경" }, + { key: "공정", label: "공정 변경" }, + { key: "금형", label: "금형 변경" }, + { key: "검사기준", label: "검사기준 변경" }, + { key: "구매", label: "구매 변경" }, + { key: "원가", label: "원가 영향" }, +]; +const NOTIFY_DEPTS = [ + { key: "생산팀", label: "생산팀" }, + { key: "품질팀", label: "품질팀" }, + { key: "구매팀", label: "구매팀" }, + { key: "영업팀", label: "영업팀" }, + { key: "물류팀", label: "물류팀" }, + { key: "금형팀", label: "금형팀" }, +]; + +// --- API Response Mapping --- +function mapEcrFromApi(raw: any): EcrItem { + const history = (raw.history || []).map((h: any) => ({ + status: h.step || h.status || "", + date: h.history_date || "", + user: h.user_name || "", + desc: h.description || "", + })); + return { + id: raw.request_no || raw.id || "", + _id: raw.id, + date: raw.request_date || "", + changeType: (raw.change_type as ChangeType) || "설계오류", + urgency: (raw.urgency as "보통" | "긴급") || "보통", + status: (raw.status as EcrStatus) || "요청접수", + target: raw.target_name || "", + drawingNo: raw.drawing_no || "", + reqDept: raw.req_dept || "", + requester: raw.requester || "", + reason: raw.reason || "", + content: raw.content || "", + impact: Array.isArray(raw.impact) ? raw.impact : [], + applyTiming: raw.apply_timing || "", + ecnNo: raw.ecn_no || "", + history, + }; +} + +function mapEcnFromApi(raw: any, ecrData: EcrItem[]): EcnItem { + const history = (raw.history || []).map((h: any) => ({ + status: h.status || "", + date: h.history_date || "", + user: h.user_name || "", + desc: h.description || "", + })); + const ecrNo = raw.ecr_id + ? ecrData.find((e) => e._id === raw.ecr_id)?.id ?? raw.ecr_id + : ""; + return { + id: raw.ecn_no || raw.id || "", + _id: raw.id, + ecrNo, + ecrId: raw.ecr_id, + date: raw.ecn_date || "", + applyDate: raw.apply_date || "", + status: (raw.status as EcnStatus) || "ECN발행", + target: raw.target || "", + drawingBefore: raw.drawing_before || "", + drawingAfter: raw.drawing_after || "", + designer: raw.designer || "", + before: raw.before_content || "", + after: raw.after_content || "", + reason: raw.reason || "", + notifyDepts: Array.isArray(raw.notify_depts) ? raw.notify_depts : [], + remark: raw.remark || "", + history, + }; +} + +// --- Timeline Component --- +function Timeline({ history }: { history: EcrHistory[] }) { + return ( +
+ {history.map((h, idx) => { + const isLast = idx === history.length - 1; + const isRejected = h.status === "기각"; + const isCompleted = h.status === "적용완료"; + return ( +
+
+
+ {!isLast && ( +
+ )} +
+
+
+ + {h.status} + +
+

{h.desc}

+

+ {h.date} · {h.user} +

+
+
+ ); + })} +
+ ); +} + +// --- Grid Columns --- +const ECR_GRID_COLUMNS = [ + { key: "request_no", label: "ECR번호" }, + { key: "change_type", label: "변경유형" }, + { key: "status", label: "상태" }, + { key: "urgency", label: "긴급" }, + { key: "target_name", label: "대상 품목/설비" }, + { key: "drawing_no", label: "도면번호" }, + { key: "req_dept", label: "요청부서" }, + { key: "requester", label: "요청자" }, + { key: "request_date", label: "요청일자" }, + { key: "ecn_no", label: "관련 ECN" }, +]; + +const ECN_GRID_COLUMNS = [ + { key: "ecn_no", label: "ECN번호" }, + { key: "status", label: "상태" }, + { key: "target", label: "대상 품목/설비" }, + { key: "drawing_after", label: "도면 (변경 후)" }, + { key: "designer", label: "설계담당" }, + { key: "ecn_date", label: "발행일자" }, + { key: "apply_date", label: "적용일자" }, + { key: "notify_depts", label: "통보 부서" }, + { key: "ecr_id", label: "관련 ECR" }, +]; + +// --- Main Component --- +export default function DesignChangeManagementPage() { + const tsEcr = useTableSettings("c16-change-management-ecr", "dsn_design_request", ECR_GRID_COLUMNS); + const tsEcn = useTableSettings("c16-change-management-ecn", "dsn_ecn", ECN_GRID_COLUMNS); + const [currentTab, setCurrentTab] = useState("ecr"); + const [ecrData, setEcrData] = useState([]); + const [ecnData, setEcnData] = useState([]); + const [loading, setLoading] = useState(true); + const [selectedId, setSelectedId] = useState(null); + const [detailOpen, setDetailOpen] = useState(false); + + // 검색 필터 (DynamicSearchFilter) + const [searchFilters, setSearchFilters] = useState([]); + + // ECR 모달 + const [isEcrModalOpen, setIsEcrModalOpen] = useState(false); + const [isEcrEditMode, setIsEcrEditMode] = useState(false); + const [ecrForm, setEcrForm] = useState>({}); + const [ecrImpactChecks, setEcrImpactChecks] = useState>({}); + + // ECN 모달 + const [isEcnModalOpen, setIsEcnModalOpen] = useState(false); + const [ecnForm, setEcnForm] = useState>({}); + const [ecnNotifyChecks, setEcnNotifyChecks] = useState>({}); + + // 기각 모달 + const [isRejectModalOpen, setIsRejectModalOpen] = useState(false); + const [rejectReason, setRejectReason] = useState(""); + const [rejectTargetId, setRejectTargetId] = useState(""); + + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const [ecrRes, ecnRes] = await Promise.all([ + getDesignRequestList({ source_type: "ecr" }), + getEcnList(), + ]); + if (ecrRes.success && ecrRes.data) { + setEcrData((ecrRes.data as any[]).map(mapEcrFromApi)); + } + if (ecnRes.success && ecnRes.data) { + const ecrList = ecrRes.success && ecrRes.data ? (ecrRes.data as any[]).map(mapEcrFromApi) : []; + setEcnData((ecnRes.data as any[]).map((r) => mapEcnFromApi(r, ecrList))); + } + } catch { + toast.error("데이터를 불러오는데 실패했어요."); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + // snake_case → camelCase 매핑 (ECR) + const ecrFieldMap: Record = { + request_no: "id", + request_date: "date", + change_type: "changeType", + target_name: "target", + drawing_no: "drawingNo", + req_dept: "reqDept", + ecn_no: "ecnNo", + apply_timing: "applyTiming", + }; + // snake_case → camelCase 매핑 (ECN) + const ecnFieldMap: Record = { + ecn_no: "id", + ecn_date: "date", + apply_date: "applyDate", + drawing_before: "drawingBefore", + drawing_after: "drawingAfter", + ecr_id: "ecrNo", + notify_depts: "notifyDepts", + }; + const getFieldValue = (obj: any, colName: string, map: Record): string => { + const key = map[colName] || colName; + const val = obj[key]; + if (Array.isArray(val)) return val.join(","); + return val !== undefined && val !== null ? String(val) : ""; + }; + + const applyFilters = (items: any[], map: Record) => { + if (searchFilters.length === 0) return items; + return items.filter((item) => { + for (const f of searchFilters) { + const val = getFieldValue(item, f.columnName, map); + if (f.operator === "contains") { + if (!val.toLowerCase().includes(f.value.toLowerCase())) return false; + } else if (f.operator === "equals") { + if (val !== f.value) return false; + } else if (f.operator === "in") { + const allowed = f.value.split("|"); + if (!allowed.includes(val)) return false; + } else if (f.operator === "between") { + const [from, to] = f.value.split("|"); + if (from && val < from) return false; + if (to && val > to) return false; + } + } + return true; + }); + }; + + // --- Filtered Data --- + const filteredEcr = useMemo(() => { + return applyFilters(ecrData, ecrFieldMap) + .sort((a: EcrItem, b: EcrItem) => b.date.localeCompare(a.date)); + }, [ecrData, searchFilters]); + + const filteredEcn = useMemo(() => { + return applyFilters(ecnData, ecnFieldMap) + .sort((a: EcnItem, b: EcnItem) => b.date.localeCompare(a.date)); + }, [ecnData, searchFilters]); + + // --- Status Counts --- + const ecrStatusCounts = useMemo(() => { + const counts: Record = {}; + ECR_STATUSES.forEach((s) => (counts[s] = ecrData.filter((r) => r.status === s).length)); + return counts; + }, [ecrData]); + + const ecnStatusCounts = useMemo(() => { + const counts: Record = {}; + ECN_STATUSES.forEach((s) => (counts[s] = ecnData.filter((r) => r.status === s).length)); + return counts; + }, [ecnData]); + + // --- Selected Items --- + const selectedEcr = useMemo( + () => (currentTab === "ecr" ? ecrData.find((r) => r.id === selectedId) : null), + [ecrData, selectedId, currentTab] + ); + const selectedEcn = useMemo( + () => (currentTab === "ecn" ? ecnData.find((r) => r.id === selectedId) : null), + [ecnData, selectedId, currentTab] + ); + + // --- Tab Switch --- + const handleTabSwitch = (tab: TabType) => { + setCurrentTab(tab); + setSelectedId(null); + }; + + const handleFilterByStatus = (_status: string) => { + // Status filter now handled by DynamicSearchFilter + }; + + // --- ECR/ECN Navigation --- + const navigateToLink = (targetId: string) => { + setDetailOpen(false); + if (targetId.startsWith("ECN")) { + setCurrentTab("ecn"); + setSelectedId(targetId); + } else if (targetId.startsWith("ECR")) { + setCurrentTab("ecr"); + setSelectedId(targetId); + } + }; + + // --- ECR Number Generator --- + const generateEcrNo = useCallback(() => { + const year = new Date().getFullYear(); + const prefix = `ECR-${year}-`; + const existing = ecrData.filter((r) => r.id.startsWith(prefix)); + const maxNum = existing.reduce((max, r) => { + const num = parseInt(r.id.split("-")[2]); + return num > max ? num : max; + }, 0); + return `${prefix}${String(maxNum + 1).padStart(4, "0")}`; + }, [ecrData]); + + const generateEcnNo = useCallback(() => { + const year = new Date().getFullYear(); + const prefix = `ECN-${year}-`; + const existing = ecnData.filter((r) => r.id.startsWith(prefix)); + const maxNum = existing.reduce((max, r) => { + const num = parseInt(r.id.split("-")[2]); + return num > max ? num : max; + }, 0); + return `${prefix}${String(maxNum + 1).padStart(4, "0")}`; + }, [ecnData]); + + // --- ECR Modal --- + const openEcrRegisterModal = () => { + setIsEcrEditMode(false); + setEcrForm({ + id: generateEcrNo(), + date: new Date().toISOString().split("T")[0], + changeType: undefined, + urgency: "보통", + target: "", + drawingNo: "", + reqDept: "", + requester: "", + reason: "", + content: "", + applyTiming: "즉시", + }); + setEcrImpactChecks({}); + setIsEcrModalOpen(true); + }; + + const openEcrEditModal = (id: string) => { + const item = ecrData.find((r) => r.id === id); + if (!item) return; + setIsEcrEditMode(true); + setEcrForm({ ...item }); + const checks: Record = {}; + IMPACT_OPTIONS.forEach((opt) => { + checks[opt.key] = item.impact.includes(opt.key); + }); + setEcrImpactChecks(checks); + setIsEcrModalOpen(true); + }; + + const handleSaveEcr = async () => { + if (!ecrForm.changeType) { + toast.error("변경 유형을 선택해 주세요."); + return; + } + if (!ecrForm.target?.trim()) { + toast.error("대상 품목/설비를 입력해 주세요."); + return; + } + if (!ecrForm.reason?.trim()) { + toast.error("변경 사유를 입력해 주세요."); + return; + } + if (!ecrForm.content?.trim()) { + toast.error("변경 요구 내용을 입력해 주세요."); + return; + } + + const impact = IMPACT_OPTIONS.filter((opt) => ecrImpactChecks[opt.key]).map((opt) => opt.key); + const reqDate = ecrForm.date || new Date().toISOString().split("T")[0]; + const historyEntry = { + step: "요청접수", + history_date: reqDate, + user_name: ecrForm.requester || "시스템", + description: `${ecrForm.reqDept || ""}에서 ECR 등록`, + }; + + if (isEcrEditMode && ecrForm._id) { + const res = await updateDesignRequest(ecrForm._id, { + request_no: ecrForm.id, + request_date: reqDate, + change_type: ecrForm.changeType, + urgency: ecrForm.urgency || "보통", + target_name: ecrForm.target, + drawing_no: ecrForm.drawingNo || "", + req_dept: ecrForm.reqDept || "", + requester: ecrForm.requester || "", + reason: ecrForm.reason, + content: ecrForm.content, + impact, + apply_timing: ecrForm.applyTiming || "즉시", + }); + if (res.success) { + toast.success("ECR이 수정되었어요."); + setIsEcrModalOpen(false); + fetchData(); + } else { + toast.error(res.message || "ECR 수정에 실패했어요."); + } + } else { + const res = await createDesignRequest({ + request_no: ecrForm.id || generateEcrNo(), + source_type: "ecr", + request_date: reqDate, + change_type: ecrForm.changeType, + urgency: ecrForm.urgency || "보통", + status: "요청접수", + target_name: ecrForm.target, + drawing_no: ecrForm.drawingNo || "", + req_dept: ecrForm.reqDept || "", + requester: ecrForm.requester || "", + reason: ecrForm.reason, + content: ecrForm.content, + impact, + apply_timing: ecrForm.applyTiming || "즉시", + history: [historyEntry], + }); + if (res.success) { + toast.success("ECR이 등록되었어요."); + setIsEcrModalOpen(false); + fetchData(); + } else { + toast.error(res.message || "ECR 등록에 실패했어요."); + } + } + }; + + // --- ECN Modal --- + const openEcnIssueModal = (ecrId: string) => { + const ecr = ecrData.find((r) => r.id === ecrId); + if (!ecr) return; + + setEcnForm({ + id: generateEcnNo(), + ecrNo: ecrId, + ecrId: ecr._id, + date: new Date().toISOString().split("T")[0], + target: ecr.target, + reason: ecr.reason, + drawingBefore: ecr.drawingNo, + drawingAfter: "", + designer: "", + before: "", + after: "", + applyDate: "", + remark: "", + }); + setEcnNotifyChecks({}); + setIsEcnModalOpen(true); + }; + + const handleSaveEcn = async () => { + if (!ecnForm.after?.trim()) { + toast.error("변경 후(TO-BE) 내용을 입력해 주세요."); + return; + } + if (!ecnForm.applyDate) { + toast.error("적용일자를 입력해 주세요."); + return; + } + if (!ecnForm.ecrId) { + toast.error("관련 ECR 정보가 없어요."); + return; + } + + const notifyDepts = NOTIFY_DEPTS.filter((d) => ecnNotifyChecks[d.key]).map((d) => d.key); + const ecnDate = ecnForm.date || new Date().toISOString().split("T")[0]; + const historyEntry = { + status: "ECN발행", + history_date: ecnDate, + user_name: ecnForm.designer || "시스템", + description: "ECN 발행", + }; + + const ecnNo = ecnForm.id || generateEcnNo(); + const res = await createEcn({ + ecn_no: ecnNo, + ecr_id: ecnForm.ecrId, + ecn_date: ecnDate, + apply_date: ecnForm.applyDate, + status: "ECN발행", + target: ecnForm.target || "", + drawing_before: ecnForm.drawingBefore || "", + drawing_after: ecnForm.drawingAfter || "(미정)", + designer: ecnForm.designer || "", + before_content: ecnForm.before || "", + after_content: ecnForm.after || "", + reason: ecnForm.reason || "", + remark: ecnForm.remark || "", + notify_depts: notifyDepts, + history: [historyEntry], + }); + + if (res.success) { + await updateDesignRequest(ecnForm.ecrId!, { + status: "ECN발행", + ecn_no: ecnNo, + }); + await addRequestHistory(ecnForm.ecrId!, { + step: "ECN발행", + history_date: ecnDate, + user_name: ecnForm.designer || "시스템", + description: `${ecnNo} 발행`, + }); + toast.success("ECN이 발행되었어요."); + setIsEcnModalOpen(false); + fetchData(); + } else { + toast.error(res.message || "ECN 발행에 실패했어요."); + } + }; + + // --- ECR Reject --- + const openRejectModal = (id: string) => { + setRejectTargetId(id); + setRejectReason(""); + setIsRejectModalOpen(true); + }; + + const handleRejectSubmit = async () => { + if (!rejectReason.trim()) { + toast.error("기각 사유를 입력해 주세요."); + return; + } + + const ecr = ecrData.find((r) => r.id === rejectTargetId); + if (!ecr?._id) { + toast.error("ECR 정보를 찾을 수 없어요."); + return; + } + + const updateRes = await updateDesignRequest(ecr._id, { status: "기각", review_memo: rejectReason }); + if (!updateRes.success) { + toast.error(updateRes.message || "ECR 기각에 실패했어요."); + return; + } + await addRequestHistory(ecr._id, { + step: "기각", + history_date: new Date().toISOString().split("T")[0], + user_name: "설계팀", + description: rejectReason, + }); + toast.success("ECR이 기각되었어요."); + setIsRejectModalOpen(false); + fetchData(); + }; + + // --- Stat Cards --- + const ecrStatCards = [ + { label: "요청접수", value: ecrStatusCounts["요청접수"] || 0, color: "text-info" }, + { label: "영향도분석", value: ecrStatusCounts["영향도분석"] || 0, color: "text-warning" }, + { label: "ECN발행", value: ecrStatusCounts["ECN발행"] || 0, color: "text-success" }, + ]; + + const ecnStatCards = [ + { label: "도면변경", value: ecnStatusCounts["도면변경"] || 0, color: "text-primary" }, + { label: "통보완료", value: ecnStatusCounts["통보완료"] || 0, color: "text-info" }, + { label: "적용완료", value: ecnStatusCounts["적용완료"] || 0, color: "text-success" }, + ]; + + const currentStatCards = currentTab === "ecr" ? ecrStatCards : ecnStatCards; + const currentList = currentTab === "ecr" ? filteredEcr : filteredEcn; + + const handleRowClick = (id: string) => { + setSelectedId(id); + setDetailOpen(true); + }; + + return ( +
+ {loading && ( +
+ +
+ )} + + {/* 탭 선택 + 검색 필터 */} +
+
+ +
+ {currentTab === "ecr" ? ( + + ) : ( + + )} +
+ + {/* 현황 카드 */} +
+ {currentStatCards.map((card) => ( + + ))} +
+ + {/* 액션 바 */} +
+
+

+ {currentTab === "ecr" ? "설계변경요청(ECR) 목록" : "설계변경통지(ECN) 목록"} +

+ {currentList.length}건 +
+
+ {currentTab === "ecr" && ( + + )} + +
+
+ + {/* 테이블 영역 */} +
+
+ {currentTab === "ecr" ? ( + {val} }, + { key: "changeType", label: "변경유형", width: "w-[90px]", align: "center" as const, render: (val: any) => {val} }, + { key: "status", label: "상태", width: "w-[90px]", align: "center" as const, render: (val: any) => {val} }, + { key: "urgency", label: "긴급", width: "w-[60px]", align: "center" as const, render: (val: any) => val === "긴급" ? 긴급 : - }, + { key: "target", label: "대상 품목/설비", width: "w-[200px]" }, + { key: "drawingNo", label: "도면번호", width: "w-[150px]" }, + { key: "reqDept", label: "요청부서", width: "w-[80px]" }, + { key: "requester", label: "요청자", width: "w-[70px]" }, + { key: "date", label: "요청일자", width: "w-[100px]" }, + { key: "ecnNo", label: "관련 ECN", width: "w-[130px]", render: (val: any) => val ? : - }, + ] as EDataTableColumn[]} + data={tsEcr.groupData(filteredEcr)} + rowKey={(row) => row.id} + selectedId={selectedId} + onSelect={(id) => { if (id) handleRowClick(id); }} + onRowClick={(row) => handleRowClick(row.id)} + emptyMessage="조건에 맞는 ECR이 없어요" + showRowNumber + showPagination={false} + draggableColumns={false} + /> + ) : ( + {val} }, + { key: "status", label: "상태", width: "w-[90px]", align: "center" as const, render: (val: any) => {val} }, + { key: "target", label: "대상 품목/설비", width: "w-[200px]" }, + { key: "drawingAfter", label: "도면 (변경 후)", width: "w-[160px]", render: (val: any) => {val} }, + { key: "designer", label: "설계담당", width: "w-[80px]" }, + { key: "date", label: "발행일자", width: "w-[100px]" }, + { key: "applyDate", label: "적용일자", width: "w-[100px]" }, + { key: "notifyDepts", label: "통보 부서", width: "w-[140px]", render: (val: any) => {Array.isArray(val) ? val.join(", ") : val} }, + { key: "ecrNo", label: "관련 ECR", width: "w-[130px]", render: (val: any) => }, + ] as EDataTableColumn[]} + data={tsEcn.groupData(filteredEcn)} + rowKey={(row) => row.id} + selectedId={selectedId} + onSelect={(id) => { if (id) handleRowClick(id); }} + onRowClick={(row) => handleRowClick(row.id)} + emptyMessage="조건에 맞는 ECN이 없어요" + showRowNumber + showPagination={false} + draggableColumns={false} + /> + )} +
+
+ + {/* 상세 정보 다이얼로그 */} + + + + {currentTab === "ecr" ? `ECR 상세 — ${selectedEcr?.id || ""}` : `ECN 상세 — ${selectedEcn?.id || ""}`} + {currentTab === "ecr" ? "설계변경요청의 상세 정보를 확인해요." : "설계변경통지의 상세 정보를 확인해요."} + +
+
+ {/* ECR 상세 */} + {selectedEcr ? ( + <> +
+

+ 기본 정보 +

+
+
+ ECR번호 + {selectedEcr.id} +
+
+ 상태 + + {selectedEcr.status} + +
+
+ 변경 유형 + + {selectedEcr.changeType} + +
+
+ 긴급도 + + {selectedEcr.urgency === "긴급" ? ( + 긴급 + ) : ( + "보통" + )} + +
+
+ 대상 품목/설비 + {selectedEcr.target} +
+
+ 도면번호 + {selectedEcr.drawingNo} +
+
+ 요청부서 / 요청자 + {selectedEcr.reqDept} / {selectedEcr.requester} +
+
+ 요청일자 + {selectedEcr.date} +
+
+ 희망 적용시점 + {selectedEcr.applyTiming} +
+
+ 관련 ECN + {selectedEcr.ecnNo ? ( + + ) : ( + 미발행 + )} +
+
+
+ +
+

변경 사유

+
+ {selectedEcr.reason} +
+
+ +
+

변경 요구 내용

+
+ {selectedEcr.content} +
+
+ +
+

영향 범위

+
+ {selectedEcr.impact.map((imp) => ( + + {imp} + + ))} +
+
+ +
+

처리 이력

+ +
+ + ) : selectedEcn ? ( + <> +
+

+ ECN 기본 정보 +

+
+
+ ECN번호 + {selectedEcn.id} +
+
+ 상태 + + {selectedEcn.status} + +
+
+ 대상 품목/설비 + {selectedEcn.target} +
+
+ 설계담당 + {selectedEcn.designer} +
+
+ 발행일자 + {selectedEcn.date} +
+
+ 적용일자 + {selectedEcn.applyDate} +
+
+ 관련 ECR + +
+
+ 통보 부서 + {selectedEcn.notifyDepts.join(", ")} +
+
+
+ +
+

변경 전/후 비교

+
+
+
+ 변경 전 ({selectedEcn.drawingBefore}) +
+
{selectedEcn.before}
+
+
+
+ 변경 후 ({selectedEcn.drawingAfter}) +
+
{selectedEcn.after}
+
+
+
+ +
+

변경 사유

+
+ {selectedEcn.reason} +
+ {selectedEcn.remark && ( +

비고: {selectedEcn.remark}

+ )} +
+ +
+

처리 이력

+ +
+ + ) : null} +
+
+ + {selectedEcr && ( + <> + + {selectedEcr.status === "영향도분석" && ( + <> + + + + )} + + )} + +
+
+ + {/* ECR 등록/수정 모달 */} + + + + {isEcrEditMode ? "설계변경요청(ECR) 수정" : "설계변경요청(ECR) 등록"} + {isEcrEditMode ? "ECR 정보를 수정해요." : "새로운 설계변경요청을 등록해요."} + +
+
+ {/* 좌측: 요청 정보 */} +
+

변경 요청 정보

+ +
+ + +
+ +
+
+ + setEcrForm((p) => ({ ...p, date: e.target.value }))} + className="h-8 text-xs sm:h-10 sm:text-sm" + /> +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + setEcrForm((p) => ({ ...p, target: e.target.value }))} + placeholder="품목코드 / 설비명" + className="h-8 text-xs sm:h-10 sm:text-sm" + /> +
+ +
+ + setEcrForm((p) => ({ ...p, drawingNo: e.target.value }))} + placeholder="DWG-XXX-XXX" + className="h-8 text-xs sm:h-10 sm:text-sm" + /> +
+ +
+
+ + +
+
+ + setEcrForm((p) => ({ ...p, requester: e.target.value }))} + placeholder="요청자명" + className="h-8 text-xs sm:h-10 sm:text-sm" + /> +
+
+
+ + {/* 우측: 변경 내용 */} +
+
+

변경 내용

+ +
+ +