From b158b0aa775e515e66d22e4322f014c10542670f Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 16 Apr 2026 18:23:20 +0900 Subject: [PATCH 1/3] feat: Enhance mold serial summary retrieval and improve category handling - Updated the `getMoldSerialSummary` function to dynamically retrieve category values for mold statuses and operations, allowing for more flexible data aggregation. - Implemented a mapping mechanism to categorize status codes based on their labels, improving the clarity of the summary results. - Adjusted SQL queries to utilize the new category mappings for more accurate counts of mold statuses. - Refactored the packaging and loading unit deletion logic to handle company code checks more efficiently, ensuring proper data access control. --- .../src/controllers/moldController.ts | 31 +++++++- .../src/controllers/packagingController.ts | 18 +++-- .../(main)/COMPANY_10/equipment/info/page.tsx | 16 ++-- .../COMPANY_10/master-data/company/page.tsx | 4 - .../app/(main)/COMPANY_10/mold/info/page.tsx | 73 +++++++++++-------- .../(main)/COMPANY_10/production/bom/page.tsx | 47 ------------ .../process-info/ItemRoutingTab.tsx | 32 ++++++-- .../process-info/ProcessMasterTab.tsx | 28 ++++--- .../(main)/COMPANY_16/equipment/info/page.tsx | 16 ++-- .../COMPANY_16/master-data/company/page.tsx | 4 - .../app/(main)/COMPANY_16/mold/info/page.tsx | 73 +++++++++++-------- .../(main)/COMPANY_16/production/bom/page.tsx | 47 ------------ .../process-info/ItemRoutingTab.tsx | 32 ++++++-- .../process-info/ProcessMasterTab.tsx | 28 ++++--- .../(main)/COMPANY_29/equipment/info/page.tsx | 16 ++-- .../COMPANY_29/master-data/company/page.tsx | 4 - .../app/(main)/COMPANY_29/mold/info/page.tsx | 73 +++++++++++-------- .../(main)/COMPANY_29/production/bom/page.tsx | 47 ------------ .../process-info/ItemRoutingTab.tsx | 32 ++++++-- .../process-info/ProcessMasterTab.tsx | 28 ++++--- .../(main)/COMPANY_30/equipment/info/page.tsx | 16 ++-- .../COMPANY_30/master-data/company/page.tsx | 4 - .../app/(main)/COMPANY_30/mold/info/page.tsx | 73 +++++++++++-------- .../(main)/COMPANY_30/production/bom/page.tsx | 47 ------------ .../process-info/ItemRoutingTab.tsx | 32 ++++++-- .../process-info/ProcessMasterTab.tsx | 28 ++++--- .../(main)/COMPANY_7/equipment/info/page.tsx | 16 ++-- .../COMPANY_7/master-data/company/page.tsx | 4 - .../app/(main)/COMPANY_7/mold/info/page.tsx | 73 +++++++++++-------- .../(main)/COMPANY_7/production/bom/page.tsx | 47 ------------ .../process-info/ItemRoutingTab.tsx | 32 ++++++-- .../process-info/ProcessMasterTab.tsx | 28 ++++--- .../(main)/COMPANY_8/equipment/info/page.tsx | 16 ++-- .../COMPANY_8/master-data/company/page.tsx | 4 - .../COMPANY_8/master-data/item-info/page.tsx | 53 ++++++++++---- .../app/(main)/COMPANY_8/mold/info/page.tsx | 73 +++++++++++-------- .../outsourcing/subcontractor/page.tsx | 27 +++++++ .../(main)/COMPANY_8/production/bom/page.tsx | 47 ------------ .../process-info/ItemRoutingTab.tsx | 32 ++++++-- .../process-info/ProcessMasterTab.tsx | 29 +++++--- .../COMPANY_8/purchase/supplier/page.tsx | 27 +++++++ .../(main)/COMPANY_8/sales/customer/page.tsx | 37 ++++++++++ .../(main)/COMPANY_9/equipment/info/page.tsx | 16 ++-- .../COMPANY_9/master-data/company/page.tsx | 4 - .../app/(main)/COMPANY_9/mold/info/page.tsx | 73 +++++++++++-------- .../(main)/COMPANY_9/production/bom/page.tsx | 47 ------------ .../process-info/ItemRoutingTab.tsx | 32 ++++++-- .../process-info/ProcessMasterTab.tsx | 28 ++++--- frontend/components/common/EDataTable.tsx | 7 +- 49 files changed, 824 insertions(+), 777 deletions(-) diff --git a/backend-node/src/controllers/moldController.ts b/backend-node/src/controllers/moldController.ts index 25e49186..19ef1eb2 100644 --- a/backend-node/src/controllers/moldController.ts +++ b/backend-node/src/controllers/moldController.ts @@ -512,13 +512,36 @@ export async function getMoldSerialSummary(req: AuthenticatedRequest, res: Respo const companyCode = req.user!.companyCode; const { moldCode } = req.params; + // 카테고리 코드/영문코드/한글라벨 모두 대응 + // 먼저 카테고리 값 조회하여 매핑 + // mold_serial.status + mold_mng.operation_status 양쪽 카테고리 모두 조회 + const catSql = `SELECT value_code, value_label FROM category_values + WHERE ((table_name='mold_serial' AND column_name='status') OR (table_name='mold_mng' AND column_name='operation_status')) + AND company_code=$1`; + const catRows = await query(catSql, [companyCode]); + + // 카테고리 라벨 기준으로 그룹핑할 코드 목록 생성 + const codesByLabel: Record = { "사용중": ["IN_USE"], "수리중": ["REPAIR"], "보관중": ["STORED"], "폐기": ["DISPOSED"] }; + for (const cat of catRows) { + const label = cat.value_label || ""; + if (label.includes("사용")) (codesByLabel["사용중"] = codesByLabel["사용중"] || []).push(cat.value_code); + else if (label.includes("수리")) (codesByLabel["수리중"] = codesByLabel["수리중"] || []).push(cat.value_code); + else if (label.includes("보관") || label.includes("미사용")) (codesByLabel["보관중"] = codesByLabel["보관중"] || []).push(cat.value_code); + else if (label.includes("폐기")) (codesByLabel["폐기"] = codesByLabel["폐기"] || []).push(cat.value_code); + } + + const inUseCodes = codesByLabel["사용중"].map(c => `'${c}'`).join(","); + const repairCodes = codesByLabel["수리중"].map(c => `'${c}'`).join(","); + const storedCodes = codesByLabel["보관중"].map(c => `'${c}'`).join(","); + const disposedCodes = codesByLabel["폐기"].map(c => `'${c}'`).join(","); + const sql = ` SELECT COUNT(*) as total, - COUNT(*) FILTER (WHERE status = 'IN_USE') as in_use, - COUNT(*) FILTER (WHERE status = 'REPAIR') as repair, - COUNT(*) FILTER (WHERE status = 'STORED') as stored, - COUNT(*) FILTER (WHERE status = 'DISPOSED') as disposed + COUNT(*) FILTER (WHERE status IN (${inUseCodes})) as in_use, + COUNT(*) FILTER (WHERE status IN (${repairCodes})) as repair, + COUNT(*) FILTER (WHERE status IN (${storedCodes})) as stored, + COUNT(*) FILTER (WHERE status IN (${disposedCodes})) as disposed FROM mold_serial WHERE mold_code = $1 AND company_code = $2 `; diff --git a/backend-node/src/controllers/packagingController.ts b/backend-node/src/controllers/packagingController.ts index d87c0c7e..037ab46a 100644 --- a/backend-node/src/controllers/packagingController.ts +++ b/backend-node/src/controllers/packagingController.ts @@ -228,10 +228,11 @@ export async function deletePkgUnitItem( const { id } = req.params; const pool = getPool(); - const result = await pool.query( - `DELETE FROM pkg_unit_item WHERE id=$1 AND company_code=$2 RETURNING id`, - [id, companyCode] - ); + const query = companyCode === "*" + ? `DELETE FROM pkg_unit_item WHERE id=$1 RETURNING id` + : `DELETE FROM pkg_unit_item WHERE id=$1 AND company_code=$2 RETURNING id`; + const params = companyCode === "*" ? [id] : [id, companyCode]; + const result = await pool.query(query, params); if (result.rowCount === 0) { res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." }); @@ -471,10 +472,11 @@ export async function deleteLoadingUnitPkg( const { id } = req.params; const pool = getPool(); - const result = await pool.query( - `DELETE FROM loading_unit_pkg WHERE id=$1 AND company_code=$2 RETURNING id`, - [id, companyCode] - ); + const query = companyCode === "*" + ? `DELETE FROM loading_unit_pkg WHERE id=$1 RETURNING id` + : `DELETE FROM loading_unit_pkg WHERE id=$1 AND company_code=$2 RETURNING id`; + const params = companyCode === "*" ? [id] : [id, companyCode]; + const result = await pool.query(query, params); if (result.rowCount === 0) { res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." }); diff --git a/frontend/app/(main)/COMPANY_10/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_10/equipment/info/page.tsx index c2f3c0ca..17d81598 100644 --- a/frontend/app/(main)/COMPANY_10/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_10/equipment/info/page.tsx @@ -147,17 +147,17 @@ export default function EquipmentInfoPage() { const colProps: Record> = { equipment_code: { width: "w-[110px]" }, equipment_name: { minWidth: "min-w-[130px]", truncate: true, render: (v) => v || "-" }, - equipment_type: { width: "w-[90px]", render: (v) => v || "-" }, + equipment_type: { width: "w-[90px]", render: (v) => resolve("equipment_type", v) || v || "-" }, manufacturer: { width: "w-[100px]", render: (v) => v || "-" }, installation_location: { width: "w-[100px]", render: (v) => v || "-" }, - operation_status: { width: "w-[80px]", render: (v) => v || "-" }, + operation_status: { width: "w-[80px]", render: (v) => resolve("operation_status", v) || v || "-" }, }; return ts.visibleColumns.map((col) => ({ key: col.key, label: col.label, ...colProps[col.key], })); - }, [ts.visibleColumns]); + }, [ts.visibleColumns, catOptions]); // 설비 조회 const fetchEquipments = useCallback(async () => { @@ -170,11 +170,7 @@ export default function EquipmentInfoPage() { autoFilter: true, }); const raw = res.data?.data?.data || res.data?.data?.rows || []; - setEquipments(raw.map((r: any) => ({ - ...r, - equipment_type: resolve("equipment_type", r.equipment_type), - operation_status: resolve("operation_status", r.operation_status), - }))); + setEquipments(raw); setEquipCount(res.data?.data?.total || raw.length); } catch { toast.error("설비 목록 조회 실패"); } finally { setEquipLoading(false); } }, [searchFilters, catOptions]); @@ -437,9 +433,9 @@ export default function EquipmentInfoPage() { const handleExcelDownload = async () => { if (equipments.length === 0) return; await exportToExcel(equipments.map((e) => ({ - 설비코드: e.equipment_code, 설비명: e.equipment_name, 설비유형: e.equipment_type, + 설비코드: e.equipment_code, 설비명: e.equipment_name, 설비유형: resolve("equipment_type", e.equipment_type), 제조사: e.manufacturer, 모델명: e.model_name, 설치장소: e.installation_location, - 도입일자: e.introduction_date, 가동상태: e.operation_status, + 도입일자: e.introduction_date, 가동상태: resolve("operation_status", e.operation_status), })), "설비정보.xlsx", "설비"); toast.success("다운로드 완료"); }; diff --git a/frontend/app/(main)/COMPANY_10/master-data/company/page.tsx b/frontend/app/(main)/COMPANY_10/master-data/company/page.tsx index a607b7ea..4cf60273 100644 --- a/frontend/app/(main)/COMPANY_10/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_10/master-data/company/page.tsx @@ -563,10 +563,6 @@ export default function CompanyPage() { {/* 기본 정보 그리드 (2열) */}
-
- - -
@@ -470,7 +500,7 @@ export default function MoldInfoPage() {

{mold.mold_code}

{mold.mold_name}

{mold.mold_type && ( - {mold.mold_type} + {resolveMoldType(mold.mold_type)} )}
@@ -531,10 +561,7 @@ export default function MoldInfoPage() { 전체 - 사출금형 - 프레스금형 - 다이캐스팅 - 단조금형 + {moldTypeCatOptions.map((o) => {o.label})} @@ -546,10 +573,7 @@ export default function MoldInfoPage() { 전체 - 사용중 - 점검중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} @@ -670,13 +694,13 @@ export default function MoldInfoPage() {

{selectedMold.mold_name}

{selectedMold.mold_type && ( - {selectedMold.mold_type} + {resolveMoldType(selectedMold.mold_type)} )} {selectedMold.category && ( {selectedMold.category} )} - - {STATUS_MAP[selectedMold.operation_status]?.label || selectedMold.operation_status || "-"} + + {resolveOpStatus(selectedMold.operation_status) || "-"}
@@ -811,15 +835,15 @@ export default function MoldInfoPage() { {serials.map((s: any) => { - const ss = SERIAL_STATUS_MAP[s.status] || { label: s.status || "-", variant: "secondary" as const }; - const maxShot = detail?.shot_count || 0; + const ssLabel = resolveOpStatus(s.status); + const maxShot = selectedMold?.shot_count || 0; const curShot = s.current_shot_count || 0; const pct = maxShot > 0 ? Math.min(Math.round((curShot / maxShot) * 100), 100) : 0; return ( {s.serial_number} - {ss.label} + {ssLabel} {maxShot > 0 ? ( @@ -1043,10 +1067,7 @@ export default function MoldInfoPage() { - 사출금형 - 프레스금형 - 다이캐스팅 - 단조금형 + {moldTypeCatOptions.map((o) => {o.label})} @@ -1117,10 +1138,7 @@ export default function MoldInfoPage() { - 사용중 - 점검중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} @@ -1175,10 +1193,7 @@ export default function MoldInfoPage() { - 사용중 - 보관중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} diff --git a/frontend/app/(main)/COMPANY_10/production/bom/page.tsx b/frontend/app/(main)/COMPANY_10/production/bom/page.tsx index ef441cfc..8c7b582b 100644 --- a/frontend/app/(main)/COMPANY_10/production/bom/page.tsx +++ b/frontend/app/(main)/COMPANY_10/production/bom/page.tsx @@ -1530,53 +1530,6 @@ export default function BomManagementPage() { ) : (
- {/* 상세 카드 */} -
-
-

BOM 상세정보

- -
- {detailLoading ? ( -
- -
- ) : bomHeader ? ( -
-
- 품목코드 - {bomHeader.item_code || bomHeader.item_number || "-"} -
-
- 품명 - {bomHeader.item_name || "-"} -
-
- BOM 유형 - {BOM_TYPE_OPTIONS.find((o) => o.code === bomHeader.bom_type)?.label || bomHeader.bom_type || "-"} -
-
- 버전 - {bomHeader.version || "-"} -
-
- 기준수량 - {bomHeader.base_qty || "1"} {bomHeader.unit || ""} -
-
- 상태 - {renderStatusBadge(bomHeader.status)} -
-
- 메모 - {bomHeader.remark || "-"} -
-
- ) : null} -
- {/* 하단 탭: 트리뷰 / 버전 / 이력 */}
{ diff --git a/frontend/app/(main)/COMPANY_10/production/process-info/ItemRoutingTab.tsx b/frontend/app/(main)/COMPANY_10/production/process-info/ItemRoutingTab.tsx index e50e27d5..4eefd66c 100644 --- a/frontend/app/(main)/COMPANY_10/production/process-info/ItemRoutingTab.tsx +++ b/frontend/app/(main)/COMPANY_10/production/process-info/ItemRoutingTab.tsx @@ -92,6 +92,7 @@ export function ItemRoutingTab() { const [formWorkType, setFormWorkType] = useState("내부"); const [formStandardTime, setFormStandardTime] = useState(""); const [formOutsource, setFormOutsource] = useState(""); + const [subcontractorOptions, setSubcontractorOptions] = useState<{ code: string; name: string }[]>([]); const [detailSubmitting, setDetailSubmitting] = useState(false); const [registerDialogOpen, setRegisterDialogOpen] = useState(false); @@ -107,6 +108,19 @@ export function ItemRoutingTab() { return () => window.clearTimeout(t); }, [searchInput]); + // 외주사 목록 로드 + useEffect(() => { + (async () => { + try { + const res = await apiClient.post("/table-management/tables/subcontractor_mng/data", { + page: 1, size: 500, autoFilter: true, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setSubcontractorOptions(rows.map((r: any) => ({ code: r.subcontractor_code || r.id, name: r.subcontractor_name || "" }))); + } catch { /* skip */ } + })(); + }, []); + useEffect(() => { const t = window.setTimeout(() => setRegisterSearchDebounced(registerSearch.trim()), 300); return () => window.clearTimeout(t); @@ -469,9 +483,9 @@ export function ItemRoutingTab() { details.map((d) => ({ ...d, process_display: d.process_name || d.process_code, - outsource_display: d.outsource_supplier || "—", + outsource_display: subcontractorOptions.find((s) => s.code === d.outsource_supplier)?.name || d.outsource_supplier || "—", })), - [details], + [details, subcontractorOptions], ); return ( @@ -896,12 +910,14 @@ export function ItemRoutingTab() { {showOutsourceField && (
- setFormOutsource(e.target.value)} - placeholder="외주 업체명" - className="h-9" - /> +
)}
diff --git a/frontend/app/(main)/COMPANY_10/production/process-info/ProcessMasterTab.tsx b/frontend/app/(main)/COMPANY_10/production/process-info/ProcessMasterTab.tsx index cfbee962..207fa3ad 100644 --- a/frontend/app/(main)/COMPANY_10/production/process-info/ProcessMasterTab.tsx +++ b/frontend/app/(main)/COMPANY_10/production/process-info/ProcessMasterTab.tsx @@ -221,18 +221,28 @@ export function ProcessMasterTab() { }; const openEdit = () => { - if (!selectedProcess) { - toast.message("수정할 공정을 좌측 목록에서 선택해주세요"); + if (selectedIds.size === 0) { + toast.message("수정할 공정을 체크박스로 선택해주세요"); + return; + } + if (selectedIds.size > 1) { + toast.message("수정은 1건만 선택해주세요"); + return; + } + const targetId = Array.from(selectedIds)[0]; + const target = processes.find((p) => p.id === targetId); + if (!target) { + toast.error("선택한 공정을 찾을 수 없습니다"); return; } setFormMode("edit"); - setEditingId(selectedProcess.id); - setFormProcessCode(selectedProcess.process_code); - setFormProcessName(selectedProcess.process_name); - setFormProcessType(selectedProcess.process_type); - setFormStandardTime(selectedProcess.standard_time ?? ""); - setFormWorkerCount(selectedProcess.worker_count ?? ""); - setFormUseYn(selectedProcess.use_yn); + setEditingId(target.id); + setFormProcessCode(target.process_code); + setFormProcessName(target.process_name); + setFormProcessType(target.process_type); + setFormStandardTime(target.standard_time ?? ""); + setFormWorkerCount(target.worker_count ?? ""); + setFormUseYn(target.use_yn); setFormOpen(true); }; diff --git a/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx index 4eb21404..0b297655 100644 --- a/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx @@ -150,17 +150,17 @@ export default function EquipmentInfoPage() { const colProps: Record> = { equipment_code: { width: "w-[110px]" }, equipment_name: { minWidth: "min-w-[130px]", truncate: true, render: (v) => v || "-" }, - equipment_type: { width: "w-[90px]", render: (v) => v || "-" }, + equipment_type: { width: "w-[90px]", render: (v) => resolve("equipment_type", v) || v || "-" }, manufacturer: { width: "w-[100px]", render: (v) => v || "-" }, installation_location: { width: "w-[100px]", render: (v) => v || "-" }, - operation_status: { width: "w-[80px]", render: (v) => v || "-" }, + operation_status: { width: "w-[80px]", render: (v) => resolve("operation_status", v) || v || "-" }, }; return ts.visibleColumns.map((col) => ({ key: col.key, label: col.label, ...colProps[col.key], })); - }, [ts.visibleColumns]); + }, [ts.visibleColumns, catOptions]); // 설비 조회 const fetchEquipments = useCallback(async () => { @@ -173,11 +173,7 @@ export default function EquipmentInfoPage() { autoFilter: true, }); const raw = res.data?.data?.data || res.data?.data?.rows || []; - setEquipments(raw.map((r: any) => ({ - ...r, - equipment_type: resolve("equipment_type", r.equipment_type), - operation_status: resolve("operation_status", r.operation_status), - }))); + setEquipments(raw); setEquipCount(res.data?.data?.total || raw.length); } catch { toast.error("설비 목록 조회 실패"); } finally { setEquipLoading(false); } }, [searchFilters, catOptions]); @@ -482,9 +478,9 @@ export default function EquipmentInfoPage() { const handleExcelDownload = async () => { if (equipments.length === 0) return; await exportToExcel(equipments.map((e) => ({ - 설비코드: e.equipment_code, 설비명: e.equipment_name, 설비유형: e.equipment_type, + 설비코드: e.equipment_code, 설비명: e.equipment_name, 설비유형: resolve("equipment_type", e.equipment_type), 제조사: e.manufacturer, 모델명: e.model_name, 설치장소: e.installation_location, - 도입일자: e.introduction_date, 가동상태: e.operation_status, + 도입일자: e.introduction_date, 가동상태: resolve("operation_status", e.operation_status), })), "설비정보.xlsx", "설비"); toast.success("다운로드 완료"); }; diff --git a/frontend/app/(main)/COMPANY_16/master-data/company/page.tsx b/frontend/app/(main)/COMPANY_16/master-data/company/page.tsx index a607b7ea..4cf60273 100644 --- a/frontend/app/(main)/COMPANY_16/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_16/master-data/company/page.tsx @@ -563,10 +563,6 @@ export default function CompanyPage() { {/* 기본 정보 그리드 (2열) */}
-
- - -
@@ -470,7 +500,7 @@ export default function MoldInfoPage() {

{mold.mold_code}

{mold.mold_name}

{mold.mold_type && ( - {mold.mold_type} + {resolveMoldType(mold.mold_type)} )}
@@ -531,10 +561,7 @@ export default function MoldInfoPage() { 전체 - 사출금형 - 프레스금형 - 다이캐스팅 - 단조금형 + {moldTypeCatOptions.map((o) => {o.label})}
@@ -546,10 +573,7 @@ export default function MoldInfoPage() { 전체 - 사용중 - 점검중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} @@ -670,13 +694,13 @@ export default function MoldInfoPage() {

{selectedMold.mold_name}

{selectedMold.mold_type && ( - {selectedMold.mold_type} + {resolveMoldType(selectedMold.mold_type)} )} {selectedMold.category && ( {selectedMold.category} )} - - {STATUS_MAP[selectedMold.operation_status]?.label || selectedMold.operation_status || "-"} + + {resolveOpStatus(selectedMold.operation_status) || "-"}
@@ -811,15 +835,15 @@ export default function MoldInfoPage() { {serials.map((s: any) => { - const ss = SERIAL_STATUS_MAP[s.status] || { label: s.status || "-", variant: "secondary" as const }; - const maxShot = detail?.shot_count || 0; + const ssLabel = resolveOpStatus(s.status); + const maxShot = selectedMold?.shot_count || 0; const curShot = s.current_shot_count || 0; const pct = maxShot > 0 ? Math.min(Math.round((curShot / maxShot) * 100), 100) : 0; return ( {s.serial_number} - {ss.label} + {ssLabel} {maxShot > 0 ? ( @@ -1043,10 +1067,7 @@ export default function MoldInfoPage() { - 사출금형 - 프레스금형 - 다이캐스팅 - 단조금형 + {moldTypeCatOptions.map((o) => {o.label})} @@ -1117,10 +1138,7 @@ export default function MoldInfoPage() { - 사용중 - 점검중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} @@ -1175,10 +1193,7 @@ export default function MoldInfoPage() { - 사용중 - 보관중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} diff --git a/frontend/app/(main)/COMPANY_16/production/bom/page.tsx b/frontend/app/(main)/COMPANY_16/production/bom/page.tsx index ef441cfc..8c7b582b 100644 --- a/frontend/app/(main)/COMPANY_16/production/bom/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/bom/page.tsx @@ -1530,53 +1530,6 @@ export default function BomManagementPage() { ) : (
- {/* 상세 카드 */} -
-
-

BOM 상세정보

- -
- {detailLoading ? ( -
- -
- ) : bomHeader ? ( -
-
- 품목코드 - {bomHeader.item_code || bomHeader.item_number || "-"} -
-
- 품명 - {bomHeader.item_name || "-"} -
-
- BOM 유형 - {BOM_TYPE_OPTIONS.find((o) => o.code === bomHeader.bom_type)?.label || bomHeader.bom_type || "-"} -
-
- 버전 - {bomHeader.version || "-"} -
-
- 기준수량 - {bomHeader.base_qty || "1"} {bomHeader.unit || ""} -
-
- 상태 - {renderStatusBadge(bomHeader.status)} -
-
- 메모 - {bomHeader.remark || "-"} -
-
- ) : null} -
- {/* 하단 탭: 트리뷰 / 버전 / 이력 */}
{ diff --git a/frontend/app/(main)/COMPANY_16/production/process-info/ItemRoutingTab.tsx b/frontend/app/(main)/COMPANY_16/production/process-info/ItemRoutingTab.tsx index e50e27d5..4eefd66c 100644 --- a/frontend/app/(main)/COMPANY_16/production/process-info/ItemRoutingTab.tsx +++ b/frontend/app/(main)/COMPANY_16/production/process-info/ItemRoutingTab.tsx @@ -92,6 +92,7 @@ export function ItemRoutingTab() { const [formWorkType, setFormWorkType] = useState("내부"); const [formStandardTime, setFormStandardTime] = useState(""); const [formOutsource, setFormOutsource] = useState(""); + const [subcontractorOptions, setSubcontractorOptions] = useState<{ code: string; name: string }[]>([]); const [detailSubmitting, setDetailSubmitting] = useState(false); const [registerDialogOpen, setRegisterDialogOpen] = useState(false); @@ -107,6 +108,19 @@ export function ItemRoutingTab() { return () => window.clearTimeout(t); }, [searchInput]); + // 외주사 목록 로드 + useEffect(() => { + (async () => { + try { + const res = await apiClient.post("/table-management/tables/subcontractor_mng/data", { + page: 1, size: 500, autoFilter: true, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setSubcontractorOptions(rows.map((r: any) => ({ code: r.subcontractor_code || r.id, name: r.subcontractor_name || "" }))); + } catch { /* skip */ } + })(); + }, []); + useEffect(() => { const t = window.setTimeout(() => setRegisterSearchDebounced(registerSearch.trim()), 300); return () => window.clearTimeout(t); @@ -469,9 +483,9 @@ export function ItemRoutingTab() { details.map((d) => ({ ...d, process_display: d.process_name || d.process_code, - outsource_display: d.outsource_supplier || "—", + outsource_display: subcontractorOptions.find((s) => s.code === d.outsource_supplier)?.name || d.outsource_supplier || "—", })), - [details], + [details, subcontractorOptions], ); return ( @@ -896,12 +910,14 @@ export function ItemRoutingTab() { {showOutsourceField && (
- setFormOutsource(e.target.value)} - placeholder="외주 업체명" - className="h-9" - /> +
)}
diff --git a/frontend/app/(main)/COMPANY_16/production/process-info/ProcessMasterTab.tsx b/frontend/app/(main)/COMPANY_16/production/process-info/ProcessMasterTab.tsx index cfbee962..207fa3ad 100644 --- a/frontend/app/(main)/COMPANY_16/production/process-info/ProcessMasterTab.tsx +++ b/frontend/app/(main)/COMPANY_16/production/process-info/ProcessMasterTab.tsx @@ -221,18 +221,28 @@ export function ProcessMasterTab() { }; const openEdit = () => { - if (!selectedProcess) { - toast.message("수정할 공정을 좌측 목록에서 선택해주세요"); + if (selectedIds.size === 0) { + toast.message("수정할 공정을 체크박스로 선택해주세요"); + return; + } + if (selectedIds.size > 1) { + toast.message("수정은 1건만 선택해주세요"); + return; + } + const targetId = Array.from(selectedIds)[0]; + const target = processes.find((p) => p.id === targetId); + if (!target) { + toast.error("선택한 공정을 찾을 수 없습니다"); return; } setFormMode("edit"); - setEditingId(selectedProcess.id); - setFormProcessCode(selectedProcess.process_code); - setFormProcessName(selectedProcess.process_name); - setFormProcessType(selectedProcess.process_type); - setFormStandardTime(selectedProcess.standard_time ?? ""); - setFormWorkerCount(selectedProcess.worker_count ?? ""); - setFormUseYn(selectedProcess.use_yn); + setEditingId(target.id); + setFormProcessCode(target.process_code); + setFormProcessName(target.process_name); + setFormProcessType(target.process_type); + setFormStandardTime(target.standard_time ?? ""); + setFormWorkerCount(target.worker_count ?? ""); + setFormUseYn(target.use_yn); setFormOpen(true); }; diff --git a/frontend/app/(main)/COMPANY_29/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_29/equipment/info/page.tsx index c2f3c0ca..17d81598 100644 --- a/frontend/app/(main)/COMPANY_29/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_29/equipment/info/page.tsx @@ -147,17 +147,17 @@ export default function EquipmentInfoPage() { const colProps: Record> = { equipment_code: { width: "w-[110px]" }, equipment_name: { minWidth: "min-w-[130px]", truncate: true, render: (v) => v || "-" }, - equipment_type: { width: "w-[90px]", render: (v) => v || "-" }, + equipment_type: { width: "w-[90px]", render: (v) => resolve("equipment_type", v) || v || "-" }, manufacturer: { width: "w-[100px]", render: (v) => v || "-" }, installation_location: { width: "w-[100px]", render: (v) => v || "-" }, - operation_status: { width: "w-[80px]", render: (v) => v || "-" }, + operation_status: { width: "w-[80px]", render: (v) => resolve("operation_status", v) || v || "-" }, }; return ts.visibleColumns.map((col) => ({ key: col.key, label: col.label, ...colProps[col.key], })); - }, [ts.visibleColumns]); + }, [ts.visibleColumns, catOptions]); // 설비 조회 const fetchEquipments = useCallback(async () => { @@ -170,11 +170,7 @@ export default function EquipmentInfoPage() { autoFilter: true, }); const raw = res.data?.data?.data || res.data?.data?.rows || []; - setEquipments(raw.map((r: any) => ({ - ...r, - equipment_type: resolve("equipment_type", r.equipment_type), - operation_status: resolve("operation_status", r.operation_status), - }))); + setEquipments(raw); setEquipCount(res.data?.data?.total || raw.length); } catch { toast.error("설비 목록 조회 실패"); } finally { setEquipLoading(false); } }, [searchFilters, catOptions]); @@ -437,9 +433,9 @@ export default function EquipmentInfoPage() { const handleExcelDownload = async () => { if (equipments.length === 0) return; await exportToExcel(equipments.map((e) => ({ - 설비코드: e.equipment_code, 설비명: e.equipment_name, 설비유형: e.equipment_type, + 설비코드: e.equipment_code, 설비명: e.equipment_name, 설비유형: resolve("equipment_type", e.equipment_type), 제조사: e.manufacturer, 모델명: e.model_name, 설치장소: e.installation_location, - 도입일자: e.introduction_date, 가동상태: e.operation_status, + 도입일자: e.introduction_date, 가동상태: resolve("operation_status", e.operation_status), })), "설비정보.xlsx", "설비"); toast.success("다운로드 완료"); }; diff --git a/frontend/app/(main)/COMPANY_29/master-data/company/page.tsx b/frontend/app/(main)/COMPANY_29/master-data/company/page.tsx index a607b7ea..4cf60273 100644 --- a/frontend/app/(main)/COMPANY_29/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_29/master-data/company/page.tsx @@ -563,10 +563,6 @@ export default function CompanyPage() { {/* 기본 정보 그리드 (2열) */}
-
- - -
@@ -470,7 +500,7 @@ export default function MoldInfoPage() {

{mold.mold_code}

{mold.mold_name}

{mold.mold_type && ( - {mold.mold_type} + {resolveMoldType(mold.mold_type)} )}
@@ -531,10 +561,7 @@ export default function MoldInfoPage() { 전체 - 사출금형 - 프레스금형 - 다이캐스팅 - 단조금형 + {moldTypeCatOptions.map((o) => {o.label})}
@@ -546,10 +573,7 @@ export default function MoldInfoPage() { 전체 - 사용중 - 점검중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} @@ -670,13 +694,13 @@ export default function MoldInfoPage() {

{selectedMold.mold_name}

{selectedMold.mold_type && ( - {selectedMold.mold_type} + {resolveMoldType(selectedMold.mold_type)} )} {selectedMold.category && ( {selectedMold.category} )} - - {STATUS_MAP[selectedMold.operation_status]?.label || selectedMold.operation_status || "-"} + + {resolveOpStatus(selectedMold.operation_status) || "-"}
@@ -811,15 +835,15 @@ export default function MoldInfoPage() { {serials.map((s: any) => { - const ss = SERIAL_STATUS_MAP[s.status] || { label: s.status || "-", variant: "secondary" as const }; - const maxShot = detail?.shot_count || 0; + const ssLabel = resolveOpStatus(s.status); + const maxShot = selectedMold?.shot_count || 0; const curShot = s.current_shot_count || 0; const pct = maxShot > 0 ? Math.min(Math.round((curShot / maxShot) * 100), 100) : 0; return ( {s.serial_number} - {ss.label} + {ssLabel} {maxShot > 0 ? ( @@ -1043,10 +1067,7 @@ export default function MoldInfoPage() { - 사출금형 - 프레스금형 - 다이캐스팅 - 단조금형 + {moldTypeCatOptions.map((o) => {o.label})} @@ -1117,10 +1138,7 @@ export default function MoldInfoPage() { - 사용중 - 점검중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} @@ -1175,10 +1193,7 @@ export default function MoldInfoPage() { - 사용중 - 보관중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} diff --git a/frontend/app/(main)/COMPANY_29/production/bom/page.tsx b/frontend/app/(main)/COMPANY_29/production/bom/page.tsx index ef441cfc..8c7b582b 100644 --- a/frontend/app/(main)/COMPANY_29/production/bom/page.tsx +++ b/frontend/app/(main)/COMPANY_29/production/bom/page.tsx @@ -1530,53 +1530,6 @@ export default function BomManagementPage() { ) : (
- {/* 상세 카드 */} -
-
-

BOM 상세정보

- -
- {detailLoading ? ( -
- -
- ) : bomHeader ? ( -
-
- 품목코드 - {bomHeader.item_code || bomHeader.item_number || "-"} -
-
- 품명 - {bomHeader.item_name || "-"} -
-
- BOM 유형 - {BOM_TYPE_OPTIONS.find((o) => o.code === bomHeader.bom_type)?.label || bomHeader.bom_type || "-"} -
-
- 버전 - {bomHeader.version || "-"} -
-
- 기준수량 - {bomHeader.base_qty || "1"} {bomHeader.unit || ""} -
-
- 상태 - {renderStatusBadge(bomHeader.status)} -
-
- 메모 - {bomHeader.remark || "-"} -
-
- ) : null} -
- {/* 하단 탭: 트리뷰 / 버전 / 이력 */}
{ diff --git a/frontend/app/(main)/COMPANY_29/production/process-info/ItemRoutingTab.tsx b/frontend/app/(main)/COMPANY_29/production/process-info/ItemRoutingTab.tsx index e50e27d5..4eefd66c 100644 --- a/frontend/app/(main)/COMPANY_29/production/process-info/ItemRoutingTab.tsx +++ b/frontend/app/(main)/COMPANY_29/production/process-info/ItemRoutingTab.tsx @@ -92,6 +92,7 @@ export function ItemRoutingTab() { const [formWorkType, setFormWorkType] = useState("내부"); const [formStandardTime, setFormStandardTime] = useState(""); const [formOutsource, setFormOutsource] = useState(""); + const [subcontractorOptions, setSubcontractorOptions] = useState<{ code: string; name: string }[]>([]); const [detailSubmitting, setDetailSubmitting] = useState(false); const [registerDialogOpen, setRegisterDialogOpen] = useState(false); @@ -107,6 +108,19 @@ export function ItemRoutingTab() { return () => window.clearTimeout(t); }, [searchInput]); + // 외주사 목록 로드 + useEffect(() => { + (async () => { + try { + const res = await apiClient.post("/table-management/tables/subcontractor_mng/data", { + page: 1, size: 500, autoFilter: true, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setSubcontractorOptions(rows.map((r: any) => ({ code: r.subcontractor_code || r.id, name: r.subcontractor_name || "" }))); + } catch { /* skip */ } + })(); + }, []); + useEffect(() => { const t = window.setTimeout(() => setRegisterSearchDebounced(registerSearch.trim()), 300); return () => window.clearTimeout(t); @@ -469,9 +483,9 @@ export function ItemRoutingTab() { details.map((d) => ({ ...d, process_display: d.process_name || d.process_code, - outsource_display: d.outsource_supplier || "—", + outsource_display: subcontractorOptions.find((s) => s.code === d.outsource_supplier)?.name || d.outsource_supplier || "—", })), - [details], + [details, subcontractorOptions], ); return ( @@ -896,12 +910,14 @@ export function ItemRoutingTab() { {showOutsourceField && (
- setFormOutsource(e.target.value)} - placeholder="외주 업체명" - className="h-9" - /> +
)}
diff --git a/frontend/app/(main)/COMPANY_29/production/process-info/ProcessMasterTab.tsx b/frontend/app/(main)/COMPANY_29/production/process-info/ProcessMasterTab.tsx index cfbee962..207fa3ad 100644 --- a/frontend/app/(main)/COMPANY_29/production/process-info/ProcessMasterTab.tsx +++ b/frontend/app/(main)/COMPANY_29/production/process-info/ProcessMasterTab.tsx @@ -221,18 +221,28 @@ export function ProcessMasterTab() { }; const openEdit = () => { - if (!selectedProcess) { - toast.message("수정할 공정을 좌측 목록에서 선택해주세요"); + if (selectedIds.size === 0) { + toast.message("수정할 공정을 체크박스로 선택해주세요"); + return; + } + if (selectedIds.size > 1) { + toast.message("수정은 1건만 선택해주세요"); + return; + } + const targetId = Array.from(selectedIds)[0]; + const target = processes.find((p) => p.id === targetId); + if (!target) { + toast.error("선택한 공정을 찾을 수 없습니다"); return; } setFormMode("edit"); - setEditingId(selectedProcess.id); - setFormProcessCode(selectedProcess.process_code); - setFormProcessName(selectedProcess.process_name); - setFormProcessType(selectedProcess.process_type); - setFormStandardTime(selectedProcess.standard_time ?? ""); - setFormWorkerCount(selectedProcess.worker_count ?? ""); - setFormUseYn(selectedProcess.use_yn); + setEditingId(target.id); + setFormProcessCode(target.process_code); + setFormProcessName(target.process_name); + setFormProcessType(target.process_type); + setFormStandardTime(target.standard_time ?? ""); + setFormWorkerCount(target.worker_count ?? ""); + setFormUseYn(target.use_yn); setFormOpen(true); }; diff --git a/frontend/app/(main)/COMPANY_30/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_30/equipment/info/page.tsx index f415be9c..5aeac1f7 100644 --- a/frontend/app/(main)/COMPANY_30/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_30/equipment/info/page.tsx @@ -145,17 +145,17 @@ export default function EquipmentInfoPage() { const colProps: Record> = { equipment_code: { width: "w-[110px]" }, equipment_name: { minWidth: "min-w-[130px]", truncate: true, render: (v) => v || "-" }, - equipment_type: { width: "w-[90px]", render: (v) => v || "-" }, + equipment_type: { width: "w-[90px]", render: (v) => resolve("equipment_type", v) || v || "-" }, manufacturer: { width: "w-[100px]", render: (v) => v || "-" }, installation_location: { width: "w-[100px]", render: (v) => v || "-" }, - operation_status: { width: "w-[80px]", render: (v) => v || "-" }, + operation_status: { width: "w-[80px]", render: (v) => resolve("operation_status", v) || v || "-" }, }; return ts.visibleColumns.map((col) => ({ key: col.key, label: col.label, ...colProps[col.key], })); - }, [ts.visibleColumns]); + }, [ts.visibleColumns, catOptions]); // 설비 조회 const fetchEquipments = useCallback(async () => { @@ -168,11 +168,7 @@ export default function EquipmentInfoPage() { autoFilter: true, }); const raw = res.data?.data?.data || res.data?.data?.rows || []; - setEquipments(raw.map((r: any) => ({ - ...r, - equipment_type: resolve("equipment_type", r.equipment_type), - operation_status: resolve("operation_status", r.operation_status), - }))); + setEquipments(raw); setEquipCount(res.data?.data?.total || raw.length); } catch { toast.error("설비 목록 조회 실패"); } finally { setEquipLoading(false); } }, [searchFilters, catOptions]); @@ -415,9 +411,9 @@ export default function EquipmentInfoPage() { const handleExcelDownload = async () => { if (equipments.length === 0) return; await exportToExcel(equipments.map((e) => ({ - 설비코드: e.equipment_code, 설비명: e.equipment_name, 설비유형: e.equipment_type, + 설비코드: e.equipment_code, 설비명: e.equipment_name, 설비유형: resolve("equipment_type", e.equipment_type), 제조사: e.manufacturer, 모델명: e.model_name, 설치장소: e.installation_location, - 도입일자: e.introduction_date, 가동상태: e.operation_status, + 도입일자: e.introduction_date, 가동상태: resolve("operation_status", e.operation_status), })), "설비정보.xlsx", "설비"); toast.success("다운로드 완료"); }; diff --git a/frontend/app/(main)/COMPANY_30/master-data/company/page.tsx b/frontend/app/(main)/COMPANY_30/master-data/company/page.tsx index 0e4de0cd..347ea2ea 100644 --- a/frontend/app/(main)/COMPANY_30/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_30/master-data/company/page.tsx @@ -563,10 +563,6 @@ export default function CompanyPage() { {/* 기본 정보 그리드 (2열) */}
-
- - -
@@ -470,7 +500,7 @@ export default function MoldInfoPage() {

{mold.mold_code}

{mold.mold_name}

{mold.mold_type && ( - {mold.mold_type} + {resolveMoldType(mold.mold_type)} )}
@@ -531,10 +561,7 @@ export default function MoldInfoPage() { 전체 - 사출금형 - 프레스금형 - 다이캐스팅 - 단조금형 + {moldTypeCatOptions.map((o) => {o.label})}
@@ -546,10 +573,7 @@ export default function MoldInfoPage() { 전체 - 사용중 - 점검중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} @@ -670,13 +694,13 @@ export default function MoldInfoPage() {

{selectedMold.mold_name}

{selectedMold.mold_type && ( - {selectedMold.mold_type} + {resolveMoldType(selectedMold.mold_type)} )} {selectedMold.category && ( {selectedMold.category} )} - - {STATUS_MAP[selectedMold.operation_status]?.label || selectedMold.operation_status || "-"} + + {resolveOpStatus(selectedMold.operation_status) || "-"}
@@ -811,15 +835,15 @@ export default function MoldInfoPage() { {serials.map((s: any) => { - const ss = SERIAL_STATUS_MAP[s.status] || { label: s.status || "-", variant: "secondary" as const }; - const maxShot = detail?.shot_count || 0; + const ssLabel = resolveOpStatus(s.status); + const maxShot = selectedMold?.shot_count || 0; const curShot = s.current_shot_count || 0; const pct = maxShot > 0 ? Math.min(Math.round((curShot / maxShot) * 100), 100) : 0; return ( {s.serial_number} - {ss.label} + {ssLabel} {maxShot > 0 ? ( @@ -1043,10 +1067,7 @@ export default function MoldInfoPage() { - 사출금형 - 프레스금형 - 다이캐스팅 - 단조금형 + {moldTypeCatOptions.map((o) => {o.label})} @@ -1117,10 +1138,7 @@ export default function MoldInfoPage() { - 사용중 - 점검중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} @@ -1175,10 +1193,7 @@ export default function MoldInfoPage() { - 사용중 - 보관중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} diff --git a/frontend/app/(main)/COMPANY_30/production/bom/page.tsx b/frontend/app/(main)/COMPANY_30/production/bom/page.tsx index 9b2e32c8..81356855 100644 --- a/frontend/app/(main)/COMPANY_30/production/bom/page.tsx +++ b/frontend/app/(main)/COMPANY_30/production/bom/page.tsx @@ -1530,53 +1530,6 @@ export default function BomManagementPage() { ) : (
- {/* 상세 카드 */} -
-
-

BOM 상세정보

- -
- {detailLoading ? ( -
- -
- ) : bomHeader ? ( -
-
- 품목코드 - {bomHeader.item_code || bomHeader.item_number || "-"} -
-
- 품명 - {bomHeader.item_name || "-"} -
-
- BOM 유형 - {BOM_TYPE_OPTIONS.find((o) => o.code === bomHeader.bom_type)?.label || bomHeader.bom_type || "-"} -
-
- 버전 - {bomHeader.version || "-"} -
-
- 기준수량 - {bomHeader.base_qty || "1"} {bomHeader.unit || ""} -
-
- 상태 - {renderStatusBadge(bomHeader.status)} -
-
- 메모 - {bomHeader.remark || "-"} -
-
- ) : null} -
- {/* 하단 탭: 트리뷰 / 버전 / 이력 */}
{ diff --git a/frontend/app/(main)/COMPANY_30/production/process-info/ItemRoutingTab.tsx b/frontend/app/(main)/COMPANY_30/production/process-info/ItemRoutingTab.tsx index e50e27d5..4eefd66c 100644 --- a/frontend/app/(main)/COMPANY_30/production/process-info/ItemRoutingTab.tsx +++ b/frontend/app/(main)/COMPANY_30/production/process-info/ItemRoutingTab.tsx @@ -92,6 +92,7 @@ export function ItemRoutingTab() { const [formWorkType, setFormWorkType] = useState("내부"); const [formStandardTime, setFormStandardTime] = useState(""); const [formOutsource, setFormOutsource] = useState(""); + const [subcontractorOptions, setSubcontractorOptions] = useState<{ code: string; name: string }[]>([]); const [detailSubmitting, setDetailSubmitting] = useState(false); const [registerDialogOpen, setRegisterDialogOpen] = useState(false); @@ -107,6 +108,19 @@ export function ItemRoutingTab() { return () => window.clearTimeout(t); }, [searchInput]); + // 외주사 목록 로드 + useEffect(() => { + (async () => { + try { + const res = await apiClient.post("/table-management/tables/subcontractor_mng/data", { + page: 1, size: 500, autoFilter: true, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setSubcontractorOptions(rows.map((r: any) => ({ code: r.subcontractor_code || r.id, name: r.subcontractor_name || "" }))); + } catch { /* skip */ } + })(); + }, []); + useEffect(() => { const t = window.setTimeout(() => setRegisterSearchDebounced(registerSearch.trim()), 300); return () => window.clearTimeout(t); @@ -469,9 +483,9 @@ export function ItemRoutingTab() { details.map((d) => ({ ...d, process_display: d.process_name || d.process_code, - outsource_display: d.outsource_supplier || "—", + outsource_display: subcontractorOptions.find((s) => s.code === d.outsource_supplier)?.name || d.outsource_supplier || "—", })), - [details], + [details, subcontractorOptions], ); return ( @@ -896,12 +910,14 @@ export function ItemRoutingTab() { {showOutsourceField && (
- setFormOutsource(e.target.value)} - placeholder="외주 업체명" - className="h-9" - /> +
)}
diff --git a/frontend/app/(main)/COMPANY_30/production/process-info/ProcessMasterTab.tsx b/frontend/app/(main)/COMPANY_30/production/process-info/ProcessMasterTab.tsx index cfbee962..207fa3ad 100644 --- a/frontend/app/(main)/COMPANY_30/production/process-info/ProcessMasterTab.tsx +++ b/frontend/app/(main)/COMPANY_30/production/process-info/ProcessMasterTab.tsx @@ -221,18 +221,28 @@ export function ProcessMasterTab() { }; const openEdit = () => { - if (!selectedProcess) { - toast.message("수정할 공정을 좌측 목록에서 선택해주세요"); + if (selectedIds.size === 0) { + toast.message("수정할 공정을 체크박스로 선택해주세요"); + return; + } + if (selectedIds.size > 1) { + toast.message("수정은 1건만 선택해주세요"); + return; + } + const targetId = Array.from(selectedIds)[0]; + const target = processes.find((p) => p.id === targetId); + if (!target) { + toast.error("선택한 공정을 찾을 수 없습니다"); return; } setFormMode("edit"); - setEditingId(selectedProcess.id); - setFormProcessCode(selectedProcess.process_code); - setFormProcessName(selectedProcess.process_name); - setFormProcessType(selectedProcess.process_type); - setFormStandardTime(selectedProcess.standard_time ?? ""); - setFormWorkerCount(selectedProcess.worker_count ?? ""); - setFormUseYn(selectedProcess.use_yn); + setEditingId(target.id); + setFormProcessCode(target.process_code); + setFormProcessName(target.process_name); + setFormProcessType(target.process_type); + setFormStandardTime(target.standard_time ?? ""); + setFormWorkerCount(target.worker_count ?? ""); + setFormUseYn(target.use_yn); setFormOpen(true); }; diff --git a/frontend/app/(main)/COMPANY_7/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_7/equipment/info/page.tsx index c2f3c0ca..17d81598 100644 --- a/frontend/app/(main)/COMPANY_7/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_7/equipment/info/page.tsx @@ -147,17 +147,17 @@ export default function EquipmentInfoPage() { const colProps: Record> = { equipment_code: { width: "w-[110px]" }, equipment_name: { minWidth: "min-w-[130px]", truncate: true, render: (v) => v || "-" }, - equipment_type: { width: "w-[90px]", render: (v) => v || "-" }, + equipment_type: { width: "w-[90px]", render: (v) => resolve("equipment_type", v) || v || "-" }, manufacturer: { width: "w-[100px]", render: (v) => v || "-" }, installation_location: { width: "w-[100px]", render: (v) => v || "-" }, - operation_status: { width: "w-[80px]", render: (v) => v || "-" }, + operation_status: { width: "w-[80px]", render: (v) => resolve("operation_status", v) || v || "-" }, }; return ts.visibleColumns.map((col) => ({ key: col.key, label: col.label, ...colProps[col.key], })); - }, [ts.visibleColumns]); + }, [ts.visibleColumns, catOptions]); // 설비 조회 const fetchEquipments = useCallback(async () => { @@ -170,11 +170,7 @@ export default function EquipmentInfoPage() { autoFilter: true, }); const raw = res.data?.data?.data || res.data?.data?.rows || []; - setEquipments(raw.map((r: any) => ({ - ...r, - equipment_type: resolve("equipment_type", r.equipment_type), - operation_status: resolve("operation_status", r.operation_status), - }))); + setEquipments(raw); setEquipCount(res.data?.data?.total || raw.length); } catch { toast.error("설비 목록 조회 실패"); } finally { setEquipLoading(false); } }, [searchFilters, catOptions]); @@ -437,9 +433,9 @@ export default function EquipmentInfoPage() { const handleExcelDownload = async () => { if (equipments.length === 0) return; await exportToExcel(equipments.map((e) => ({ - 설비코드: e.equipment_code, 설비명: e.equipment_name, 설비유형: e.equipment_type, + 설비코드: e.equipment_code, 설비명: e.equipment_name, 설비유형: resolve("equipment_type", e.equipment_type), 제조사: e.manufacturer, 모델명: e.model_name, 설치장소: e.installation_location, - 도입일자: e.introduction_date, 가동상태: e.operation_status, + 도입일자: e.introduction_date, 가동상태: resolve("operation_status", e.operation_status), })), "설비정보.xlsx", "설비"); toast.success("다운로드 완료"); }; diff --git a/frontend/app/(main)/COMPANY_7/master-data/company/page.tsx b/frontend/app/(main)/COMPANY_7/master-data/company/page.tsx index a607b7ea..4cf60273 100644 --- a/frontend/app/(main)/COMPANY_7/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_7/master-data/company/page.tsx @@ -563,10 +563,6 @@ export default function CompanyPage() { {/* 기본 정보 그리드 (2열) */}
-
- - -
@@ -470,7 +500,7 @@ export default function MoldInfoPage() {

{mold.mold_code}

{mold.mold_name}

{mold.mold_type && ( - {mold.mold_type} + {resolveMoldType(mold.mold_type)} )}
@@ -531,10 +561,7 @@ export default function MoldInfoPage() { 전체 - 사출금형 - 프레스금형 - 다이캐스팅 - 단조금형 + {moldTypeCatOptions.map((o) => {o.label})}
@@ -546,10 +573,7 @@ export default function MoldInfoPage() { 전체 - 사용중 - 점검중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} @@ -670,13 +694,13 @@ export default function MoldInfoPage() {

{selectedMold.mold_name}

{selectedMold.mold_type && ( - {selectedMold.mold_type} + {resolveMoldType(selectedMold.mold_type)} )} {selectedMold.category && ( {selectedMold.category} )} - - {STATUS_MAP[selectedMold.operation_status]?.label || selectedMold.operation_status || "-"} + + {resolveOpStatus(selectedMold.operation_status) || "-"}
@@ -811,15 +835,15 @@ export default function MoldInfoPage() { {serials.map((s: any) => { - const ss = SERIAL_STATUS_MAP[s.status] || { label: s.status || "-", variant: "secondary" as const }; - const maxShot = detail?.shot_count || 0; + const ssLabel = resolveOpStatus(s.status); + const maxShot = selectedMold?.shot_count || 0; const curShot = s.current_shot_count || 0; const pct = maxShot > 0 ? Math.min(Math.round((curShot / maxShot) * 100), 100) : 0; return ( {s.serial_number} - {ss.label} + {ssLabel} {maxShot > 0 ? ( @@ -1043,10 +1067,7 @@ export default function MoldInfoPage() { - 사출금형 - 프레스금형 - 다이캐스팅 - 단조금형 + {moldTypeCatOptions.map((o) => {o.label})} @@ -1117,10 +1138,7 @@ export default function MoldInfoPage() { - 사용중 - 점검중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} @@ -1175,10 +1193,7 @@ export default function MoldInfoPage() { - 사용중 - 보관중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} diff --git a/frontend/app/(main)/COMPANY_7/production/bom/page.tsx b/frontend/app/(main)/COMPANY_7/production/bom/page.tsx index ef441cfc..8c7b582b 100644 --- a/frontend/app/(main)/COMPANY_7/production/bom/page.tsx +++ b/frontend/app/(main)/COMPANY_7/production/bom/page.tsx @@ -1530,53 +1530,6 @@ export default function BomManagementPage() { ) : (
- {/* 상세 카드 */} -
-
-

BOM 상세정보

- -
- {detailLoading ? ( -
- -
- ) : bomHeader ? ( -
-
- 품목코드 - {bomHeader.item_code || bomHeader.item_number || "-"} -
-
- 품명 - {bomHeader.item_name || "-"} -
-
- BOM 유형 - {BOM_TYPE_OPTIONS.find((o) => o.code === bomHeader.bom_type)?.label || bomHeader.bom_type || "-"} -
-
- 버전 - {bomHeader.version || "-"} -
-
- 기준수량 - {bomHeader.base_qty || "1"} {bomHeader.unit || ""} -
-
- 상태 - {renderStatusBadge(bomHeader.status)} -
-
- 메모 - {bomHeader.remark || "-"} -
-
- ) : null} -
- {/* 하단 탭: 트리뷰 / 버전 / 이력 */}
{ diff --git a/frontend/app/(main)/COMPANY_7/production/process-info/ItemRoutingTab.tsx b/frontend/app/(main)/COMPANY_7/production/process-info/ItemRoutingTab.tsx index e50e27d5..4eefd66c 100644 --- a/frontend/app/(main)/COMPANY_7/production/process-info/ItemRoutingTab.tsx +++ b/frontend/app/(main)/COMPANY_7/production/process-info/ItemRoutingTab.tsx @@ -92,6 +92,7 @@ export function ItemRoutingTab() { const [formWorkType, setFormWorkType] = useState("내부"); const [formStandardTime, setFormStandardTime] = useState(""); const [formOutsource, setFormOutsource] = useState(""); + const [subcontractorOptions, setSubcontractorOptions] = useState<{ code: string; name: string }[]>([]); const [detailSubmitting, setDetailSubmitting] = useState(false); const [registerDialogOpen, setRegisterDialogOpen] = useState(false); @@ -107,6 +108,19 @@ export function ItemRoutingTab() { return () => window.clearTimeout(t); }, [searchInput]); + // 외주사 목록 로드 + useEffect(() => { + (async () => { + try { + const res = await apiClient.post("/table-management/tables/subcontractor_mng/data", { + page: 1, size: 500, autoFilter: true, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setSubcontractorOptions(rows.map((r: any) => ({ code: r.subcontractor_code || r.id, name: r.subcontractor_name || "" }))); + } catch { /* skip */ } + })(); + }, []); + useEffect(() => { const t = window.setTimeout(() => setRegisterSearchDebounced(registerSearch.trim()), 300); return () => window.clearTimeout(t); @@ -469,9 +483,9 @@ export function ItemRoutingTab() { details.map((d) => ({ ...d, process_display: d.process_name || d.process_code, - outsource_display: d.outsource_supplier || "—", + outsource_display: subcontractorOptions.find((s) => s.code === d.outsource_supplier)?.name || d.outsource_supplier || "—", })), - [details], + [details, subcontractorOptions], ); return ( @@ -896,12 +910,14 @@ export function ItemRoutingTab() { {showOutsourceField && (
- setFormOutsource(e.target.value)} - placeholder="외주 업체명" - className="h-9" - /> +
)}
diff --git a/frontend/app/(main)/COMPANY_7/production/process-info/ProcessMasterTab.tsx b/frontend/app/(main)/COMPANY_7/production/process-info/ProcessMasterTab.tsx index cfbee962..207fa3ad 100644 --- a/frontend/app/(main)/COMPANY_7/production/process-info/ProcessMasterTab.tsx +++ b/frontend/app/(main)/COMPANY_7/production/process-info/ProcessMasterTab.tsx @@ -221,18 +221,28 @@ export function ProcessMasterTab() { }; const openEdit = () => { - if (!selectedProcess) { - toast.message("수정할 공정을 좌측 목록에서 선택해주세요"); + if (selectedIds.size === 0) { + toast.message("수정할 공정을 체크박스로 선택해주세요"); + return; + } + if (selectedIds.size > 1) { + toast.message("수정은 1건만 선택해주세요"); + return; + } + const targetId = Array.from(selectedIds)[0]; + const target = processes.find((p) => p.id === targetId); + if (!target) { + toast.error("선택한 공정을 찾을 수 없습니다"); return; } setFormMode("edit"); - setEditingId(selectedProcess.id); - setFormProcessCode(selectedProcess.process_code); - setFormProcessName(selectedProcess.process_name); - setFormProcessType(selectedProcess.process_type); - setFormStandardTime(selectedProcess.standard_time ?? ""); - setFormWorkerCount(selectedProcess.worker_count ?? ""); - setFormUseYn(selectedProcess.use_yn); + setEditingId(target.id); + setFormProcessCode(target.process_code); + setFormProcessName(target.process_name); + setFormProcessType(target.process_type); + setFormStandardTime(target.standard_time ?? ""); + setFormWorkerCount(target.worker_count ?? ""); + setFormUseYn(target.use_yn); setFormOpen(true); }; diff --git a/frontend/app/(main)/COMPANY_8/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_8/equipment/info/page.tsx index c2f3c0ca..17d81598 100644 --- a/frontend/app/(main)/COMPANY_8/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_8/equipment/info/page.tsx @@ -147,17 +147,17 @@ export default function EquipmentInfoPage() { const colProps: Record> = { equipment_code: { width: "w-[110px]" }, equipment_name: { minWidth: "min-w-[130px]", truncate: true, render: (v) => v || "-" }, - equipment_type: { width: "w-[90px]", render: (v) => v || "-" }, + equipment_type: { width: "w-[90px]", render: (v) => resolve("equipment_type", v) || v || "-" }, manufacturer: { width: "w-[100px]", render: (v) => v || "-" }, installation_location: { width: "w-[100px]", render: (v) => v || "-" }, - operation_status: { width: "w-[80px]", render: (v) => v || "-" }, + operation_status: { width: "w-[80px]", render: (v) => resolve("operation_status", v) || v || "-" }, }; return ts.visibleColumns.map((col) => ({ key: col.key, label: col.label, ...colProps[col.key], })); - }, [ts.visibleColumns]); + }, [ts.visibleColumns, catOptions]); // 설비 조회 const fetchEquipments = useCallback(async () => { @@ -170,11 +170,7 @@ export default function EquipmentInfoPage() { autoFilter: true, }); const raw = res.data?.data?.data || res.data?.data?.rows || []; - setEquipments(raw.map((r: any) => ({ - ...r, - equipment_type: resolve("equipment_type", r.equipment_type), - operation_status: resolve("operation_status", r.operation_status), - }))); + setEquipments(raw); setEquipCount(res.data?.data?.total || raw.length); } catch { toast.error("설비 목록 조회 실패"); } finally { setEquipLoading(false); } }, [searchFilters, catOptions]); @@ -437,9 +433,9 @@ export default function EquipmentInfoPage() { const handleExcelDownload = async () => { if (equipments.length === 0) return; await exportToExcel(equipments.map((e) => ({ - 설비코드: e.equipment_code, 설비명: e.equipment_name, 설비유형: e.equipment_type, + 설비코드: e.equipment_code, 설비명: e.equipment_name, 설비유형: resolve("equipment_type", e.equipment_type), 제조사: e.manufacturer, 모델명: e.model_name, 설치장소: e.installation_location, - 도입일자: e.introduction_date, 가동상태: e.operation_status, + 도입일자: e.introduction_date, 가동상태: resolve("operation_status", e.operation_status), })), "설비정보.xlsx", "설비"); toast.success("다운로드 완료"); }; diff --git a/frontend/app/(main)/COMPANY_8/master-data/company/page.tsx b/frontend/app/(main)/COMPANY_8/master-data/company/page.tsx index a607b7ea..4cf60273 100644 --- a/frontend/app/(main)/COMPANY_8/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_8/master-data/company/page.tsx @@ -563,10 +563,6 @@ export default function CompanyPage() { {/* 기본 정보 그리드 (2열) */}
-
- - -
- ) : numberingParts.some(p => p.isManual) ? ( - // 파트별 세그먼트 렌더링 (수동 입력 파트 있음) + ) : ( + // 자동 채번값 표시 + 사용자 직접 수정 가능 + setFormData(prev => ({ ...prev, [field.key]: e.target.value }))} + onFocus={(e) => e.target.select()} + placeholder="자동 채번 (직접 입력 가능)" + className="h-9" + /> + ) + /* 기존 세그먼트 UI 비활성화 — 대진산업은 직접 입력 허용 + numberingParts.some(p => p.isManual) ? (
{numberingParts.map((part, idx) => { const isFirst = idx === 0; @@ -852,7 +878,6 @@ export default function ItemInfoPage() { })}
) : ( - // 전체 auto: 읽기전용 표시 ) + */ ) : ["selling_price", "standard_price"].includes(field.key) ? ( + {ConfirmDialogComponent}
); } diff --git a/frontend/app/(main)/COMPANY_8/mold/info/page.tsx b/frontend/app/(main)/COMPANY_8/mold/info/page.tsx index 126c803d..ac3b2f9d 100644 --- a/frontend/app/(main)/COMPANY_8/mold/info/page.tsx +++ b/frontend/app/(main)/COMPANY_8/mold/info/page.tsx @@ -72,6 +72,36 @@ export default function MoldInfoPage() { const [selectedMoldCode, setSelectedMoldCode] = useState(null); const [viewMode, setViewMode] = useState<"list" | "grid">("list"); + // ─── 카테고리 옵션 (금형유형, 운영상태) ─── + const [moldTypeCatOptions, setMoldTypeCatOptions] = useState<{ code: string; label: string }[]>([]); + const [operationStatusCatOptions, setOperationStatusCatOptions] = useState<{ code: string; label: string }[]>([]); + + useEffect(() => { + const flatten = (arr: any[]): { code: string; label: string }[] => { + const result: { code: string; label: string }[] = []; + for (const v of arr) { result.push({ code: v.valueCode, label: v.valueLabel }); if (v.children?.length) result.push(...flatten(v.children)); } + return result; + }; + (async () => { + try { + const [typeRes, statusRes] = await Promise.all([ + apiClient.get("/table-categories/mold_mng/mold_type/values"), + apiClient.get("/table-categories/mold_mng/operation_status/values"), + ]); + if (typeRes.data?.success) setMoldTypeCatOptions(flatten(typeRes.data.data || [])); + if (statusRes.data?.success) setOperationStatusCatOptions(flatten(statusRes.data.data || [])); + } catch { /* skip */ } + })(); + }, []); + + const resolveMoldType = (code: string) => moldTypeCatOptions.find((o) => o.code === code)?.label || code; + const resolveOpStatus = (code: string) => { + const catLabel = operationStatusCatOptions.find((o) => o.code === code)?.label; + if (catLabel) return catLabel; + const legacyMap: Record = { ACTIVE: "사용중", INACTIVE: "미사용", REPAIR: "수리중", DISPOSED: "폐기", IN_USE: "사용중" }; + return legacyMap[code] || code; + }; + // ─── 검색 필터 ─── const [filterCode, setFilterCode] = useState(""); const [filterName, setFilterName] = useState(""); @@ -426,7 +456,7 @@ export default function MoldInfoPage() { // ─── 카드 렌더링 ─── const renderCard = (mold: any) => { const pct = calcLifePct(mold); - const st = STATUS_MAP[mold.operation_status] || { label: mold.operation_status || "-", variant: "secondary" as const }; + const stLabel = resolveOpStatus(mold.operation_status); const isSelected = selectedMoldCode === mold.mold_code; return ( @@ -460,7 +490,7 @@ export default function MoldInfoPage() { )}
- {st.label} + {stLabel}
@@ -470,7 +500,7 @@ export default function MoldInfoPage() {

{mold.mold_code}

{mold.mold_name}

{mold.mold_type && ( - {mold.mold_type} + {resolveMoldType(mold.mold_type)} )} @@ -531,10 +561,7 @@ export default function MoldInfoPage() { 전체 - 사출금형 - 프레스금형 - 다이캐스팅 - 단조금형 + {moldTypeCatOptions.map((o) => {o.label})} @@ -546,10 +573,7 @@ export default function MoldInfoPage() { 전체 - 사용중 - 점검중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} @@ -670,13 +694,13 @@ export default function MoldInfoPage() {

{selectedMold.mold_name}

{selectedMold.mold_type && ( - {selectedMold.mold_type} + {resolveMoldType(selectedMold.mold_type)} )} {selectedMold.category && ( {selectedMold.category} )} - - {STATUS_MAP[selectedMold.operation_status]?.label || selectedMold.operation_status || "-"} + + {resolveOpStatus(selectedMold.operation_status) || "-"}
@@ -811,15 +835,15 @@ export default function MoldInfoPage() { {serials.map((s: any) => { - const ss = SERIAL_STATUS_MAP[s.status] || { label: s.status || "-", variant: "secondary" as const }; - const maxShot = detail?.shot_count || 0; + const ssLabel = resolveOpStatus(s.status); + const maxShot = selectedMold?.shot_count || 0; const curShot = s.current_shot_count || 0; const pct = maxShot > 0 ? Math.min(Math.round((curShot / maxShot) * 100), 100) : 0; return ( {s.serial_number} - {ss.label} + {ssLabel} {maxShot > 0 ? ( @@ -1043,10 +1067,7 @@ export default function MoldInfoPage() { - 사출금형 - 프레스금형 - 다이캐스팅 - 단조금형 + {moldTypeCatOptions.map((o) => {o.label})} @@ -1117,10 +1138,7 @@ export default function MoldInfoPage() { - 사용중 - 점검중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} @@ -1175,10 +1193,7 @@ export default function MoldInfoPage() { - 사용중 - 보관중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} diff --git a/frontend/app/(main)/COMPANY_8/outsourcing/subcontractor/page.tsx b/frontend/app/(main)/COMPANY_8/outsourcing/subcontractor/page.tsx index 2a5684b0..4a86ef27 100644 --- a/frontend/app/(main)/COMPANY_8/outsourcing/subcontractor/page.tsx +++ b/frontend/app/(main)/COMPANY_8/outsourcing/subcontractor/page.tsx @@ -1096,6 +1096,33 @@ export default function SubcontractorManagementPage() { /> {formErrors.business_number &&

{formErrors.business_number}

} +
+ + setSubcontractorForm((p) => ({ ...p, representative: e.target.value }))} + placeholder="대표이름" + className="h-9 text-sm" + /> +
+
+ + handleFormChange("phone", e.target.value)} + placeholder="02-0000-0000" + className="h-9 text-sm" + /> +
+
+ + handleFormChange("fax", e.target.value)} + placeholder="02-0000-0000" + className="h-9 text-sm" + /> +
) : (
- {/* 상세 카드 */} -
-
-

BOM 상세정보

- -
- {detailLoading ? ( -
- -
- ) : bomHeader ? ( -
-
- 품목코드 - {bomHeader.item_code || bomHeader.item_number || "-"} -
-
- 품명 - {bomHeader.item_name || "-"} -
-
- BOM 유형 - {BOM_TYPE_OPTIONS.find((o) => o.code === bomHeader.bom_type)?.label || bomHeader.bom_type || "-"} -
-
- 버전 - {bomHeader.version || "-"} -
-
- 기준수량 - {bomHeader.base_qty || "1"} {bomHeader.unit || ""} -
-
- 상태 - {renderStatusBadge(bomHeader.status)} -
-
- 메모 - {bomHeader.remark || "-"} -
-
- ) : null} -
- {/* 하단 탭: 트리뷰 / 버전 / 이력 */}
{ diff --git a/frontend/app/(main)/COMPANY_8/production/process-info/ItemRoutingTab.tsx b/frontend/app/(main)/COMPANY_8/production/process-info/ItemRoutingTab.tsx index e50e27d5..4eefd66c 100644 --- a/frontend/app/(main)/COMPANY_8/production/process-info/ItemRoutingTab.tsx +++ b/frontend/app/(main)/COMPANY_8/production/process-info/ItemRoutingTab.tsx @@ -92,6 +92,7 @@ export function ItemRoutingTab() { const [formWorkType, setFormWorkType] = useState("내부"); const [formStandardTime, setFormStandardTime] = useState(""); const [formOutsource, setFormOutsource] = useState(""); + const [subcontractorOptions, setSubcontractorOptions] = useState<{ code: string; name: string }[]>([]); const [detailSubmitting, setDetailSubmitting] = useState(false); const [registerDialogOpen, setRegisterDialogOpen] = useState(false); @@ -107,6 +108,19 @@ export function ItemRoutingTab() { return () => window.clearTimeout(t); }, [searchInput]); + // 외주사 목록 로드 + useEffect(() => { + (async () => { + try { + const res = await apiClient.post("/table-management/tables/subcontractor_mng/data", { + page: 1, size: 500, autoFilter: true, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setSubcontractorOptions(rows.map((r: any) => ({ code: r.subcontractor_code || r.id, name: r.subcontractor_name || "" }))); + } catch { /* skip */ } + })(); + }, []); + useEffect(() => { const t = window.setTimeout(() => setRegisterSearchDebounced(registerSearch.trim()), 300); return () => window.clearTimeout(t); @@ -469,9 +483,9 @@ export function ItemRoutingTab() { details.map((d) => ({ ...d, process_display: d.process_name || d.process_code, - outsource_display: d.outsource_supplier || "—", + outsource_display: subcontractorOptions.find((s) => s.code === d.outsource_supplier)?.name || d.outsource_supplier || "—", })), - [details], + [details, subcontractorOptions], ); return ( @@ -896,12 +910,14 @@ export function ItemRoutingTab() { {showOutsourceField && (
- setFormOutsource(e.target.value)} - placeholder="외주 업체명" - className="h-9" - /> +
)}
diff --git a/frontend/app/(main)/COMPANY_8/production/process-info/ProcessMasterTab.tsx b/frontend/app/(main)/COMPANY_8/production/process-info/ProcessMasterTab.tsx index cfbee962..ab75ae0d 100644 --- a/frontend/app/(main)/COMPANY_8/production/process-info/ProcessMasterTab.tsx +++ b/frontend/app/(main)/COMPANY_8/production/process-info/ProcessMasterTab.tsx @@ -221,18 +221,29 @@ export function ProcessMasterTab() { }; const openEdit = () => { - if (!selectedProcess) { - toast.message("수정할 공정을 좌측 목록에서 선택해주세요"); + // 체크박스 기준: 1개만 체크된 경우 수정 + if (selectedIds.size === 0) { + toast.message("수정할 공정을 체크박스로 선택해주세요"); + return; + } + if (selectedIds.size > 1) { + toast.message("수정은 1건만 선택해주세요"); + return; + } + const targetId = Array.from(selectedIds)[0]; + const target = processes.find((p) => p.id === targetId); + if (!target) { + toast.error("선택한 공정을 찾을 수 없습니다"); return; } setFormMode("edit"); - setEditingId(selectedProcess.id); - setFormProcessCode(selectedProcess.process_code); - setFormProcessName(selectedProcess.process_name); - setFormProcessType(selectedProcess.process_type); - setFormStandardTime(selectedProcess.standard_time ?? ""); - setFormWorkerCount(selectedProcess.worker_count ?? ""); - setFormUseYn(selectedProcess.use_yn); + setEditingId(target.id); + setFormProcessCode(target.process_code); + setFormProcessName(target.process_name); + setFormProcessType(target.process_type); + setFormStandardTime(target.standard_time ?? ""); + setFormWorkerCount(target.worker_count ?? ""); + setFormUseYn(target.use_yn); setFormOpen(true); }; diff --git a/frontend/app/(main)/COMPANY_8/purchase/supplier/page.tsx b/frontend/app/(main)/COMPANY_8/purchase/supplier/page.tsx index 964e7e09..355a67e5 100644 --- a/frontend/app/(main)/COMPANY_8/purchase/supplier/page.tsx +++ b/frontend/app/(main)/COMPANY_8/purchase/supplier/page.tsx @@ -1896,6 +1896,33 @@ export default function SupplierManagementPage() { /> {formErrors.business_number &&

{formErrors.business_number}

}
+
+ + setSupplierForm((p) => ({ ...p, representative_name: e.target.value }))} + placeholder="대표이름" + className="h-9" + /> +
+
+ + handleFormChange("phone", e.target.value)} + placeholder="02-0000-0000" + className="h-9" + /> +
+
+ + handleFormChange("fax_number", e.target.value)} + placeholder="02-0000-0000" + className="h-9" + /> +
{formErrors.business_number &&

{formErrors.business_number}

}
+
+ + setCustomerForm((p) => ({ ...p, representative_name: e.target.value }))} + placeholder="대표이름" + className="h-9" + /> +
+
+ + handleFormChange("phone", e.target.value)} + placeholder="02-0000-0000" + className="h-9" + /> +
+
+ + handleFormChange("fax", e.target.value)} + placeholder="02-0000-0000" + className="h-9" + /> +
+
+ + handleFormChange("email", e.target.value)} + placeholder="example@email.com" + className={cn("h-9", formErrors.email && "border-destructive")} + /> + {formErrors.email &&

{formErrors.email}

} +
> = { equipment_code: { width: "w-[110px]" }, equipment_name: { minWidth: "min-w-[130px]", truncate: true, render: (v) => v || "-" }, - equipment_type: { width: "w-[90px]", render: (v) => v || "-" }, + equipment_type: { width: "w-[90px]", render: (v) => resolve("equipment_type", v) || v || "-" }, manufacturer: { width: "w-[100px]", render: (v) => v || "-" }, installation_location: { width: "w-[100px]", render: (v) => v || "-" }, - operation_status: { width: "w-[80px]", render: (v) => v || "-" }, + operation_status: { width: "w-[80px]", render: (v) => resolve("operation_status", v) || v || "-" }, }; return ts.visibleColumns.map((col) => ({ key: col.key, label: col.label, ...colProps[col.key], })); - }, [ts.visibleColumns]); + }, [ts.visibleColumns, catOptions]); // 설비 조회 const fetchEquipments = useCallback(async () => { @@ -170,11 +170,7 @@ export default function EquipmentInfoPage() { autoFilter: true, }); const raw = res.data?.data?.data || res.data?.data?.rows || []; - setEquipments(raw.map((r: any) => ({ - ...r, - equipment_type: resolve("equipment_type", r.equipment_type), - operation_status: resolve("operation_status", r.operation_status), - }))); + setEquipments(raw); setEquipCount(res.data?.data?.total || raw.length); } catch { toast.error("설비 목록 조회 실패"); } finally { setEquipLoading(false); } }, [searchFilters, catOptions]); @@ -437,9 +433,9 @@ export default function EquipmentInfoPage() { const handleExcelDownload = async () => { if (equipments.length === 0) return; await exportToExcel(equipments.map((e) => ({ - 설비코드: e.equipment_code, 설비명: e.equipment_name, 설비유형: e.equipment_type, + 설비코드: e.equipment_code, 설비명: e.equipment_name, 설비유형: resolve("equipment_type", e.equipment_type), 제조사: e.manufacturer, 모델명: e.model_name, 설치장소: e.installation_location, - 도입일자: e.introduction_date, 가동상태: e.operation_status, + 도입일자: e.introduction_date, 가동상태: resolve("operation_status", e.operation_status), })), "설비정보.xlsx", "설비"); toast.success("다운로드 완료"); }; diff --git a/frontend/app/(main)/COMPANY_9/master-data/company/page.tsx b/frontend/app/(main)/COMPANY_9/master-data/company/page.tsx index 0e4de0cd..347ea2ea 100644 --- a/frontend/app/(main)/COMPANY_9/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_9/master-data/company/page.tsx @@ -563,10 +563,6 @@ export default function CompanyPage() { {/* 기본 정보 그리드 (2열) */}
-
- - -
@@ -470,7 +500,7 @@ export default function MoldInfoPage() {

{mold.mold_code}

{mold.mold_name}

{mold.mold_type && ( - {mold.mold_type} + {resolveMoldType(mold.mold_type)} )}
@@ -531,10 +561,7 @@ export default function MoldInfoPage() { 전체 - 사출금형 - 프레스금형 - 다이캐스팅 - 단조금형 + {moldTypeCatOptions.map((o) => {o.label})}
@@ -546,10 +573,7 @@ export default function MoldInfoPage() { 전체 - 사용중 - 점검중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})}
@@ -670,13 +694,13 @@ export default function MoldInfoPage() {

{selectedMold.mold_name}

{selectedMold.mold_type && ( - {selectedMold.mold_type} + {resolveMoldType(selectedMold.mold_type)} )} {selectedMold.category && ( {selectedMold.category} )} - - {STATUS_MAP[selectedMold.operation_status]?.label || selectedMold.operation_status || "-"} + + {resolveOpStatus(selectedMold.operation_status) || "-"}
@@ -811,15 +835,15 @@ export default function MoldInfoPage() { {serials.map((s: any) => { - const ss = SERIAL_STATUS_MAP[s.status] || { label: s.status || "-", variant: "secondary" as const }; - const maxShot = detail?.shot_count || 0; + const ssLabel = resolveOpStatus(s.status); + const maxShot = selectedMold?.shot_count || 0; const curShot = s.current_shot_count || 0; const pct = maxShot > 0 ? Math.min(Math.round((curShot / maxShot) * 100), 100) : 0; return ( {s.serial_number} - {ss.label} + {ssLabel} {maxShot > 0 ? ( @@ -1043,10 +1067,7 @@ export default function MoldInfoPage() { - 사출금형 - 프레스금형 - 다이캐스팅 - 단조금형 + {moldTypeCatOptions.map((o) => {o.label})} @@ -1117,10 +1138,7 @@ export default function MoldInfoPage() { - 사용중 - 점검중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} @@ -1175,10 +1193,7 @@ export default function MoldInfoPage() { - 사용중 - 보관중 - 수리중 - 폐기 + {operationStatusCatOptions.map((o) => {o.label})} diff --git a/frontend/app/(main)/COMPANY_9/production/bom/page.tsx b/frontend/app/(main)/COMPANY_9/production/bom/page.tsx index 9b2e32c8..81356855 100644 --- a/frontend/app/(main)/COMPANY_9/production/bom/page.tsx +++ b/frontend/app/(main)/COMPANY_9/production/bom/page.tsx @@ -1530,53 +1530,6 @@ export default function BomManagementPage() { ) : (
- {/* 상세 카드 */} -
-
-

BOM 상세정보

- -
- {detailLoading ? ( -
- -
- ) : bomHeader ? ( -
-
- 품목코드 - {bomHeader.item_code || bomHeader.item_number || "-"} -
-
- 품명 - {bomHeader.item_name || "-"} -
-
- BOM 유형 - {BOM_TYPE_OPTIONS.find((o) => o.code === bomHeader.bom_type)?.label || bomHeader.bom_type || "-"} -
-
- 버전 - {bomHeader.version || "-"} -
-
- 기준수량 - {bomHeader.base_qty || "1"} {bomHeader.unit || ""} -
-
- 상태 - {renderStatusBadge(bomHeader.status)} -
-
- 메모 - {bomHeader.remark || "-"} -
-
- ) : null} -
- {/* 하단 탭: 트리뷰 / 버전 / 이력 */}
{ diff --git a/frontend/app/(main)/COMPANY_9/production/process-info/ItemRoutingTab.tsx b/frontend/app/(main)/COMPANY_9/production/process-info/ItemRoutingTab.tsx index e50e27d5..4eefd66c 100644 --- a/frontend/app/(main)/COMPANY_9/production/process-info/ItemRoutingTab.tsx +++ b/frontend/app/(main)/COMPANY_9/production/process-info/ItemRoutingTab.tsx @@ -92,6 +92,7 @@ export function ItemRoutingTab() { const [formWorkType, setFormWorkType] = useState("내부"); const [formStandardTime, setFormStandardTime] = useState(""); const [formOutsource, setFormOutsource] = useState(""); + const [subcontractorOptions, setSubcontractorOptions] = useState<{ code: string; name: string }[]>([]); const [detailSubmitting, setDetailSubmitting] = useState(false); const [registerDialogOpen, setRegisterDialogOpen] = useState(false); @@ -107,6 +108,19 @@ export function ItemRoutingTab() { return () => window.clearTimeout(t); }, [searchInput]); + // 외주사 목록 로드 + useEffect(() => { + (async () => { + try { + const res = await apiClient.post("/table-management/tables/subcontractor_mng/data", { + page: 1, size: 500, autoFilter: true, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setSubcontractorOptions(rows.map((r: any) => ({ code: r.subcontractor_code || r.id, name: r.subcontractor_name || "" }))); + } catch { /* skip */ } + })(); + }, []); + useEffect(() => { const t = window.setTimeout(() => setRegisterSearchDebounced(registerSearch.trim()), 300); return () => window.clearTimeout(t); @@ -469,9 +483,9 @@ export function ItemRoutingTab() { details.map((d) => ({ ...d, process_display: d.process_name || d.process_code, - outsource_display: d.outsource_supplier || "—", + outsource_display: subcontractorOptions.find((s) => s.code === d.outsource_supplier)?.name || d.outsource_supplier || "—", })), - [details], + [details, subcontractorOptions], ); return ( @@ -896,12 +910,14 @@ export function ItemRoutingTab() { {showOutsourceField && (
- setFormOutsource(e.target.value)} - placeholder="외주 업체명" - className="h-9" - /> +
)}
diff --git a/frontend/app/(main)/COMPANY_9/production/process-info/ProcessMasterTab.tsx b/frontend/app/(main)/COMPANY_9/production/process-info/ProcessMasterTab.tsx index cfbee962..207fa3ad 100644 --- a/frontend/app/(main)/COMPANY_9/production/process-info/ProcessMasterTab.tsx +++ b/frontend/app/(main)/COMPANY_9/production/process-info/ProcessMasterTab.tsx @@ -221,18 +221,28 @@ export function ProcessMasterTab() { }; const openEdit = () => { - if (!selectedProcess) { - toast.message("수정할 공정을 좌측 목록에서 선택해주세요"); + if (selectedIds.size === 0) { + toast.message("수정할 공정을 체크박스로 선택해주세요"); + return; + } + if (selectedIds.size > 1) { + toast.message("수정은 1건만 선택해주세요"); + return; + } + const targetId = Array.from(selectedIds)[0]; + const target = processes.find((p) => p.id === targetId); + if (!target) { + toast.error("선택한 공정을 찾을 수 없습니다"); return; } setFormMode("edit"); - setEditingId(selectedProcess.id); - setFormProcessCode(selectedProcess.process_code); - setFormProcessName(selectedProcess.process_name); - setFormProcessType(selectedProcess.process_type); - setFormStandardTime(selectedProcess.standard_time ?? ""); - setFormWorkerCount(selectedProcess.worker_count ?? ""); - setFormUseYn(selectedProcess.use_yn); + setEditingId(target.id); + setFormProcessCode(target.process_code); + setFormProcessName(target.process_name); + setFormProcessType(target.process_type); + setFormStandardTime(target.standard_time ?? ""); + setFormWorkerCount(target.worker_count ?? ""); + setFormUseYn(target.use_yn); setFormOpen(true); }; diff --git a/frontend/components/common/EDataTable.tsx b/frontend/components/common/EDataTable.tsx index 1b16a772..c1a47615 100644 --- a/frontend/components/common/EDataTable.tsx +++ b/frontend/components/common/EDataTable.tsx @@ -715,16 +715,17 @@ export function EDataTable = any>({ const id = getRowId(row, rowKey); const isSelected = selectedId != null && String(selectedId) === String(id); const isChecked = checkedIds.includes(id); - const highlighted = isSelected || isChecked; return ( { onSelect?.(id); From 173b85b4765c84595720bfe105ab05f7ebf482e7 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 17 Apr 2026 13:11:01 +0900 Subject: [PATCH 2/3] feat: Implement copy functionality for item inspection information - Added a modal for copying inspection information from a selected item to multiple target items. - Implemented search and selection logic for target items to facilitate the copying process. - Included validation to ensure a source item is selected and that target items are valid before proceeding with the copy operation. - Enhanced user feedback with toast notifications for successful and error states during the copy process. - Updated BOM management to include unit label handling for better clarity in item representation. --- .../tableCategoryValueController.ts | 4 +- .../src/services/categoryTreeService.ts | 5 +- .../src/services/tableCategoryValueService.ts | 7 +- .../COMPANY_10/master-data/options/page.tsx | 97 +++++++- .../(main)/COMPANY_10/production/bom/page.tsx | 13 +- .../COMPANY_10/quality/inspection/page.tsx | 141 ++++++----- .../quality/item-inspection/page.tsx | 230 +++++++++++++++++- .../COMPANY_16/master-data/options/page.tsx | 99 +++++++- .../(main)/COMPANY_16/production/bom/page.tsx | 13 +- .../COMPANY_16/quality/inspection/page.tsx | 141 ++++++----- .../quality/item-inspection/page.tsx | 230 +++++++++++++++++- .../COMPANY_29/master-data/options/page.tsx | 97 +++++++- .../(main)/COMPANY_29/production/bom/page.tsx | 13 +- .../COMPANY_29/quality/inspection/page.tsx | 141 ++++++----- .../quality/item-inspection/page.tsx | 230 +++++++++++++++++- .../COMPANY_30/master-data/options/page.tsx | 97 +++++++- .../(main)/COMPANY_30/production/bom/page.tsx | 13 +- .../COMPANY_30/quality/inspection/page.tsx | 141 ++++++----- .../quality/item-inspection/page.tsx | 230 +++++++++++++++++- .../(main)/COMPANY_30/sales/customer/page.tsx | 2 +- .../(main)/COMPANY_30/sales/order/page.tsx | 2 +- .../COMPANY_7/master-data/options/page.tsx | 97 +++++++- .../(main)/COMPANY_7/production/bom/page.tsx | 13 +- .../COMPANY_7/quality/inspection/page.tsx | 141 ++++++----- .../quality/item-inspection/page.tsx | 230 +++++++++++++++++- .../COMPANY_8/master-data/options/page.tsx | 97 +++++++- .../(main)/COMPANY_8/production/bom/page.tsx | 13 +- .../COMPANY_8/quality/inspection/page.tsx | 141 ++++++----- .../quality/item-inspection/page.tsx | 230 +++++++++++++++++- .../COMPANY_9/master-data/options/page.tsx | 97 +++++++- .../(main)/COMPANY_9/production/bom/page.tsx | 13 +- .../COMPANY_9/quality/inspection/page.tsx | 141 ++++++----- .../quality/item-inspection/page.tsx | 230 +++++++++++++++++- .../(main)/COMPANY_9/sales/customer/page.tsx | 2 +- .../app/(main)/COMPANY_9/sales/order/page.tsx | 2 +- frontend/components/common/EDataTable.tsx | 4 +- frontend/lib/api/tableCategoryValue.ts | 6 +- 37 files changed, 2944 insertions(+), 459 deletions(-) diff --git a/backend-node/src/controllers/tableCategoryValueController.ts b/backend-node/src/controllers/tableCategoryValueController.ts index cff7ccfa..508b3159 100644 --- a/backend-node/src/controllers/tableCategoryValueController.ts +++ b/backend-node/src/controllers/tableCategoryValueController.ts @@ -67,6 +67,7 @@ export const getCategoryValues = async (req: AuthenticatedRequest, res: Response const includeInactive = req.query.includeInactive === "true"; const menuObjid = req.query.menuObjid ? Number(req.query.menuObjid) : undefined; const filterCompanyCode = req.query.filterCompanyCode as string | undefined; + const topLevelOnly = req.query.topLevelOnly === "true"; // 최고관리자가 특정 회사 기준 필터링을 요청한 경우 해당 회사 코드 사용 const effectiveCompanyCode = (userCompanyCode === "*" && filterCompanyCode) @@ -86,7 +87,8 @@ export const getCategoryValues = async (req: AuthenticatedRequest, res: Response columnName, effectiveCompanyCode, includeInactive, - menuObjid + menuObjid, + topLevelOnly ); return res.json({ diff --git a/backend-node/src/services/categoryTreeService.ts b/backend-node/src/services/categoryTreeService.ts index 462a5191..b236a7f5 100644 --- a/backend-node/src/services/categoryTreeService.ts +++ b/backend-node/src/services/categoryTreeService.ts @@ -223,13 +223,14 @@ class CategoryTreeService { const query = ` INSERT INTO category_values ( - table_name, column_name, value_code, value_label, value_order, + value_id, table_name, column_name, value_code, value_label, value_order, parent_value_id, depth, path, description, color, icon, is_active, is_default, company_code, created_by, updated_by ) VALUES ( + (SELECT COALESCE(MAX(value_id), 0) + 1 FROM category_values), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $15 ) - RETURNING + RETURNING value_id AS "valueId", table_name AS "tableName", column_name AS "columnName", diff --git a/backend-node/src/services/tableCategoryValueService.ts b/backend-node/src/services/tableCategoryValueService.ts index 16bc75a2..712bf646 100644 --- a/backend-node/src/services/tableCategoryValueService.ts +++ b/backend-node/src/services/tableCategoryValueService.ts @@ -167,7 +167,8 @@ class TableCategoryValueService { columnName: string, companyCode: string, includeInactive: boolean = false, - menuObjid?: number + menuObjid?: number, + topLevelOnly: boolean = false ): Promise { try { logger.info("카테고리 값 목록 조회 (메뉴 스코프)", { @@ -235,6 +236,10 @@ class TableCategoryValueService { query += ` AND is_active = true`; } + if (topLevelOnly) { + query += ` AND (depth = 1 OR depth IS NULL OR parent_value_id IS NULL)`; + } + query += ` ORDER BY value_order, value_label`; const result = await pool.query(query, params); diff --git a/frontend/app/(main)/COMPANY_10/master-data/options/page.tsx b/frontend/app/(main)/COMPANY_10/master-data/options/page.tsx index 7506ad94..6053f27f 100644 --- a/frontend/app/(main)/COMPANY_10/master-data/options/page.tsx +++ b/frontend/app/(main)/COMPANY_10/master-data/options/page.tsx @@ -5,7 +5,12 @@ import { Settings2, Tags, Hash } from "lucide-react"; import { cn } from "@/lib/utils"; import { CategoryColumnList } from "@/components/table-category/CategoryColumnList"; import { CategoryValueManager } from "@/components/table-category/CategoryValueManager"; +import { CategoryValueManagerTree } from "@/components/table-category/CategoryValueManagerTree"; import { NumberingRuleDesigner } from "@/components/numbering-rule/NumberingRuleDesigner"; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; +import { useConfirmDialog } from "@/components/common/ConfirmDialog"; +import { getCategoryValues } from "@/lib/api/tableCategoryValue"; const TABS = [ { id: "category", label: "카테고리 설정", icon: Tags }, @@ -21,6 +26,12 @@ export default function OptionsSettingPage() { const [selectedColumnLabel, setSelectedColumnLabel] = useState(""); const [selectedTableName, setSelectedTableName] = useState(""); + const [useHierarchy, setUseHierarchy] = useState(false); + const [hasChildRows, setHasChildRows] = useState(false); + const [detectingHierarchy, setDetectingHierarchy] = useState(false); + + const { confirm, ConfirmDialogComponent } = useConfirmDialog(); + const [leftWidth, setLeftWidth] = useState(340); const [isDragging, setIsDragging] = useState(false); const containerRef = useRef(null); @@ -51,6 +62,71 @@ export default function OptionsSettingPage() { }; }, [isDragging]); + useEffect(() => { + if (!selectedColumn || !selectedTableName) { + setUseHierarchy(false); + setHasChildRows(false); + return; + } + const columnNameOnly = selectedColumn.includes(".") + ? selectedColumn.split(".").pop()! + : selectedColumn; + let cancelled = false; + setDetectingHierarchy(true); + (async () => { + const res = await getCategoryValues(selectedTableName, columnNameOnly, true); + if (cancelled) return; + const values = (res as any)?.data || []; + const hasChild = Array.isArray(values) + ? values.some( + (v: any) => + (typeof v.depth === "number" && v.depth > 1) || + (v.parentValueId !== null && v.parentValueId !== undefined), + ) + : false; + setHasChildRows(hasChild); + setUseHierarchy(hasChild); + setDetectingHierarchy(false); + })(); + return () => { + cancelled = true; + }; + }, [selectedColumn, selectedTableName]); + + const handleToggleHierarchy = useCallback( + async (checked: boolean) => { + if (!checked && hasChildRows) { + const ok = await confirm( + "이미 등록된 하위분류(중/소분류)가 있습니다.\n하위분류 사용을 해제해도 기존 데이터는 삭제되지 않으며, 다시 사용 설정 시 그대로 복원됩니다.\n계속하시겠습니까?", + { variant: "destructive", confirmText: "해제" }, + ); + if (!ok) return; + } + setUseHierarchy(checked); + }, + [hasChildRows, confirm], + ); + + const columnNameOnly = selectedColumn + ? selectedColumn.includes(".") + ? selectedColumn.split(".").pop()! + : selectedColumn + : ""; + + const headerRight = selectedColumn ? ( +
+ + +
+ ) : null; + return (
@@ -108,11 +184,21 @@ export default function OptionsSettingPage() {
{selectedColumn && selectedTableName ? ( - + useHierarchy ? ( + + ) : ( + + ) ) : (
@@ -131,6 +217,7 @@ export default function OptionsSettingPage() {
)}
+ {ConfirmDialogComponent}
); } diff --git a/frontend/app/(main)/COMPANY_10/production/bom/page.tsx b/frontend/app/(main)/COMPANY_10/production/bom/page.tsx index 8c7b582b..c8618672 100644 --- a/frontend/app/(main)/COMPANY_10/production/bom/page.tsx +++ b/frontend/app/(main)/COMPANY_10/production/bom/page.tsx @@ -497,12 +497,14 @@ export default function BomManagementPage() { const c = code.trim(); return categoryOptions["division"]?.find((o) => o.code === c)?.label || c; }).filter((v: string) => v && v !== "s").join(", "); + const rawUnit = d.unit || item?.inventory_unit || ""; + const unitLabel = categoryOptions["inventory_unit"]?.find((o) => o.code === rawUnit)?.label || rawUnit; return { ...d, item_number: item?.item_number || "", item_name: item?.item_name || "", item_type: divisionLabel, - unit: d.unit || item?.inventory_unit || "", + unit: unitLabel, spec: item?.size || item?.spec || "", writer: d.writer || "", updated_date: d.updated_at || d.updated_date || "", @@ -818,6 +820,15 @@ export default function BomManagementPage() { return; } + // 같은 레벨(같은 부모) 중복 품목 체크 + const siblings = addTargetParentId + ? (findNodeById(editingTree, addTargetParentId)?.children || []) + : editingTree; + if (siblings.some((n) => n.child_item_id === item.id)) { + toast.error("같은 레벨에 이미 동일 품목이 존재합니다"); + return; + } + const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2)}`; const parentNode = addTargetParentId ? findNodeById(editingTree, addTargetParentId) : null; const newLevel = parentNode ? ((parentNode._level ?? parentNode.level ?? 0) as number) + 1 : 0; diff --git a/frontend/app/(main)/COMPANY_10/quality/inspection/page.tsx b/frontend/app/(main)/COMPANY_10/quality/inspection/page.tsx index 4602a719..b1998a64 100644 --- a/frontend/app/(main)/COMPANY_10/quality/inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_10/quality/inspection/page.tsx @@ -31,6 +31,7 @@ import { Settings2, } from "lucide-react"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { ImageUpload } from "@/components/common/ImageUpload"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; @@ -59,11 +60,12 @@ const DEFECT_TABLE = "defect_standard_mng"; const EQUIPMENT_TABLE = "inspection_equipment_mng"; /* ───── 카테고리 flatten ───── */ -const flattenCategories = (vals: any[]): { code: string; label: string }[] => { - const result: { code: string; label: string }[] = []; +type CatOption = { code: string; label: string; depth: number; parentCode?: string }; +const flattenCategories = (vals: any[], parentCode?: string): CatOption[] => { + const result: CatOption[] = []; for (const v of vals) { - result.push({ code: v.valueCode, label: v.valueLabel }); - if (v.children?.length) result.push(...flattenCategories(v.children)); + result.push({ code: v.valueCode, label: v.valueLabel, depth: v.depth ?? 1, parentCode }); + if (v.children?.length) result.push(...flattenCategories(v.children, v.valueCode)); } return result; }; @@ -113,13 +115,13 @@ export default function InspectionManagementPage() { const [previewCode, setPreviewCode] = useState(null); /* ───── 카테고리 옵션 ───── */ - const [catOptions, setCatOptions] = useState>({}); + const [catOptions, setCatOptions] = useState>({}); const [userOptions, setUserOptions] = useState<{ code: string; label: string }[]>([]); /* ═══════════════════ 카테고리 로드 ═══════════════════ */ useEffect(() => { const load = async () => { - const optMap: Record = {}; + const optMap: Record = {}; const catList = [ { table: INSPECTION_TABLE, col: "inspection_type" }, { table: INSPECTION_TABLE, col: "apply_type" }, @@ -867,7 +869,7 @@ export default function InspectionManagementPage() { .filter(Boolean) .map((t: string) => ( - {t} + {getCatLabel(DEFECT_TABLE, "inspection_type", t)} )) : "-"} @@ -945,6 +947,9 @@ export default function InspectionManagementPage() { onCheckedChange={(v) => setEqChecked(v ? filteredEquipments.map((r) => r.id) : [])} /> + + 이미지 + 장비코드 @@ -980,13 +985,13 @@ export default function InspectionManagementPage() { {eqLoading ? ( - + ) : filteredEquipments.length === 0 ? ( - +

등록된 검사장비가 없어요

@@ -1015,6 +1020,18 @@ export default function InspectionManagementPage() { } />
+ + {row.image_path ? ( + { (e.target as HTMLImageElement).style.display = "none"; }} + /> + ) : ( +
+ )} + {row.equipment_code || "-"} {row.equipment_name || "-"} @@ -1421,24 +1438,26 @@ export default function InspectionManagementPage() { 검사유형 * (다중선택)
- {(catOptions[`${DEFECT_TABLE}.inspection_type`] || []).map((o) => { - const types: string[] = defForm.inspection_type - ? defForm.inspection_type.split(",").filter(Boolean) - : []; - const checked = types.includes(o.code); - return ( -
- { - const next = v ? [...types, o.code] : types.filter((t) => t !== o.code); - setDefForm((p) => ({ ...p, inspection_type: next.join(","), apply_target: "" })); - }} - /> - -
- ); - })} + {(catOptions[`${DEFECT_TABLE}.inspection_type`] || []) + .filter((o) => o.depth === 1) + .map((o) => { + const types: string[] = defForm.inspection_type + ? defForm.inspection_type.split(",").filter(Boolean) + : []; + const checked = types.includes(o.code); + return ( +
+ { + const next = v ? [...types, o.code] : types.filter((t) => t !== o.code); + setDefForm((p) => ({ ...p, inspection_type: next.join(","), apply_target: "" })); + }} + /> + +
+ ); + })}
{/* 적용대상 (다중선택, 검사유형별 동적) */} @@ -1451,38 +1470,37 @@ export default function InspectionManagementPage() { : []; if (selectedTypes.length === 0) return

검사유형을 먼저 선택하세요

; - const typeTargetMap: Record = {}; const defInspOpts = catOptions[`${DEFECT_TABLE}.inspection_type`] || []; - for (const code of selectedTypes) { - const label = defInspOpts.find((o) => o.code === code)?.label || ""; - if (label.includes("수입")) - typeTargetMap[label] = ["구매입고", "외주입고", "반품입고", "무상입고", "기타입고"]; - else if (label.includes("공정")) - typeTargetMap[label] = ["가공", "조립", "도장", "열처리", "표면처리", "용접"]; - else if (label.includes("출하")) - typeTargetMap[label] = ["국내출하", "수출출하", "반품출하", "샘플출하"]; - else if (label.includes("최종")) typeTargetMap[label] = ["완제품", "반제품", "부품"]; - } const targets: string[] = defForm.apply_target ? defForm.apply_target.split(",").filter(Boolean) : []; - return Object.entries(typeTargetMap).map(([typeName, opts]) => ( -
-

{typeName}

-
- {opts.map((t) => ( -
- { - const next = v ? [...targets, t] : targets.filter((x) => x !== t); - setDefForm((p) => ({ ...p, apply_target: next.join(",") })); - }} - /> - + return selectedTypes.map((parentCode: string) => { + const parentLabel = defInspOpts.find((o) => o.code === parentCode)?.label || parentCode; + const children = defInspOpts.filter((o) => o.parentCode === parentCode); + return ( +
+

{parentLabel}

+ {children.length === 0 ? ( +

+ 하위분류가 없습니다. 옵션설정에서 "{parentLabel}"의 하위분류를 등록해주세요. +

+ ) : ( +
+ {children.map((t) => ( +
+ { + const next = v ? [...targets, t.code] : targets.filter((x) => x !== t.code); + setDefForm((p) => ({ ...p, apply_target: next.join(",") })); + }} + /> + +
+ ))}
- ))} + )}
-
- )); + ); + }); })()}
@@ -1710,7 +1728,18 @@ export default function InspectionManagementPage() {
- {/* Row 5: 비고 (full width) */} + {/* Row 5: 이미지 (full width) */} +
+ + setEqForm((p) => ({ ...p, image_path: v }))} + tableName={EQUIPMENT_TABLE} + recordId={eqForm.id} + columnName="image_path" + /> +
+ {/* Row 6: 비고 (full width) */}