From 2a23cadb41bd6991f7eba1f3d072788fb02939e9 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 8 Apr 2026 15:33:09 +0900 Subject: [PATCH] feat: Enhance user management and reporting features - Added `end_date` field to user management for better tracking of user status. - Updated SQL queries in `adminController` to include `end_date` during user save operations. - Improved purchase report data handling by refining the logic for received quantities. - Enhanced file preview functionality to streamline file path handling. - Updated outbound and receiving controllers to ensure accurate updates to shipment and purchase order details. These changes aim to improve the overall functionality and user experience in managing user data and reporting processes. --- .../src/controllers/adminController.ts | 7 +- .../controllers/analyticsReportController.ts | 4 +- .../src/controllers/fileController.ts | 58 +- .../src/controllers/outboundController.ts | 22 +- .../src/controllers/receivingController.ts | 18 +- .../src/services/productionPlanService.ts | 14 +- .../(main)/COMPANY_10/equipment/info/page.tsx | 33 +- .../logistics/material-status/page.tsx | 137 +++-- .../COMPANY_10/logistics/outbound/page.tsx | 157 +++-- .../COMPANY_10/logistics/packaging/page.tsx | 26 +- .../COMPANY_10/logistics/receiving/page.tsx | 159 +++-- .../COMPANY_10/master-data/company/page.tsx | 89 --- .../master-data/department/page.tsx | 68 ++- .../COMPANY_10/master-data/item-info/page.tsx | 67 +- .../outsourcing/subcontractor-item/page.tsx | 27 +- .../production/plan-management/page.tsx | 99 +-- .../(main)/COMPANY_10/purchase/order/page.tsx | 54 +- .../purchase/purchase-item/page.tsx | 26 +- .../COMPANY_10/purchase/supplier/page.tsx | 81 ++- .../quality/item-inspection/page.tsx | 56 +- .../(main)/COMPANY_10/sales/customer/page.tsx | 93 ++- .../(main)/COMPANY_10/sales/order/page.tsx | 571 +++++------------- .../COMPANY_10/sales/shipping-order/page.tsx | 2 +- .../(main)/COMPANY_16/equipment/info/page.tsx | 33 +- .../logistics/material-status/page.tsx | 137 +++-- .../COMPANY_16/logistics/outbound/page.tsx | 157 +++-- .../COMPANY_16/logistics/packaging/page.tsx | 26 +- .../COMPANY_16/logistics/receiving/page.tsx | 159 +++-- .../COMPANY_16/master-data/company/page.tsx | 89 --- .../master-data/department/page.tsx | 68 ++- .../COMPANY_16/master-data/item-info/page.tsx | 67 +- .../outsourcing/subcontractor-item/page.tsx | 27 +- .../production/plan-management/page.tsx | 99 +-- .../(main)/COMPANY_16/purchase/order/page.tsx | 54 +- .../purchase/purchase-item/page.tsx | 26 +- .../COMPANY_16/purchase/supplier/page.tsx | 81 ++- .../quality/item-inspection/page.tsx | 56 +- .../(main)/COMPANY_16/sales/customer/page.tsx | 93 ++- .../(main)/COMPANY_16/sales/order/page.tsx | 571 +++++------------- .../COMPANY_16/sales/shipping-order/page.tsx | 2 +- .../(main)/COMPANY_29/equipment/info/page.tsx | 33 +- .../logistics/material-status/page.tsx | 137 +++-- .../COMPANY_29/logistics/outbound/page.tsx | 157 +++-- .../COMPANY_29/logistics/packaging/page.tsx | 26 +- .../COMPANY_29/logistics/receiving/page.tsx | 159 +++-- .../COMPANY_29/master-data/company/page.tsx | 89 --- .../master-data/department/page.tsx | 68 ++- .../COMPANY_29/master-data/item-info/page.tsx | 67 +- .../outsourcing/subcontractor-item/page.tsx | 27 +- .../production/plan-management/page.tsx | 99 +-- .../(main)/COMPANY_29/purchase/order/page.tsx | 54 +- .../purchase/purchase-item/page.tsx | 26 +- .../COMPANY_29/purchase/supplier/page.tsx | 81 ++- .../quality/item-inspection/page.tsx | 56 +- .../(main)/COMPANY_29/sales/customer/page.tsx | 93 ++- .../(main)/COMPANY_29/sales/order/page.tsx | 571 +++++------------- .../COMPANY_29/sales/shipping-order/page.tsx | 2 +- .../(main)/COMPANY_30/equipment/info/page.tsx | 33 +- .../logistics/material-status/page.tsx | 137 +++-- .../COMPANY_30/logistics/outbound/page.tsx | 157 +++-- .../COMPANY_30/logistics/packaging/page.tsx | 26 +- .../COMPANY_30/logistics/receiving/page.tsx | 159 +++-- .../COMPANY_30/master-data/company/page.tsx | 89 --- .../master-data/department/page.tsx | 68 ++- .../COMPANY_30/master-data/item-info/page.tsx | 67 +- .../outsourcing/subcontractor-item/page.tsx | 27 +- .../production/plan-management/page.tsx | 99 +-- .../(main)/COMPANY_30/purchase/order/page.tsx | 54 +- .../purchase/purchase-item/page.tsx | 26 +- .../COMPANY_30/purchase/supplier/page.tsx | 81 ++- .../quality/item-inspection/page.tsx | 56 +- .../(main)/COMPANY_30/sales/customer/page.tsx | 93 ++- .../COMPANY_30/sales/shipping-order/page.tsx | 2 +- .../(main)/COMPANY_7/equipment/info/page.tsx | 33 +- .../logistics/material-status/page.tsx | 137 +++-- .../COMPANY_7/logistics/outbound/page.tsx | 157 +++-- .../COMPANY_7/logistics/packaging/page.tsx | 26 +- .../COMPANY_7/logistics/receiving/page.tsx | 159 +++-- .../COMPANY_7/master-data/company/page.tsx | 89 --- .../COMPANY_7/master-data/department/page.tsx | 68 ++- .../COMPANY_7/master-data/item-info/page.tsx | 67 +- .../outsourcing/subcontractor-item/page.tsx | 27 +- .../production/plan-management/page.tsx | 99 +-- .../(main)/COMPANY_7/purchase/order/page.tsx | 54 +- .../COMPANY_7/purchase/purchase-item/page.tsx | 26 +- .../COMPANY_7/purchase/supplier/page.tsx | 81 ++- .../quality/item-inspection/page.tsx | 56 +- .../(main)/COMPANY_7/sales/customer/page.tsx | 93 ++- .../app/(main)/COMPANY_7/sales/order/page.tsx | 571 +++++------------- .../COMPANY_7/sales/shipping-order/page.tsx | 2 +- .../(main)/COMPANY_8/equipment/info/page.tsx | 33 +- .../logistics/material-status/page.tsx | 137 +++-- .../COMPANY_8/logistics/outbound/page.tsx | 157 +++-- .../COMPANY_8/logistics/packaging/page.tsx | 26 +- .../COMPANY_8/logistics/receiving/page.tsx | 159 +++-- .../COMPANY_8/master-data/company/page.tsx | 89 --- .../COMPANY_8/master-data/department/page.tsx | 68 ++- .../COMPANY_8/master-data/item-info/page.tsx | 67 +- .../outsourcing/subcontractor-item/page.tsx | 27 +- .../production/plan-management/page.tsx | 99 +-- .../(main)/COMPANY_8/purchase/order/page.tsx | 54 +- .../COMPANY_8/purchase/purchase-item/page.tsx | 26 +- .../COMPANY_8/purchase/supplier/page.tsx | 81 ++- .../quality/item-inspection/page.tsx | 56 +- .../(main)/COMPANY_8/sales/customer/page.tsx | 93 ++- .../app/(main)/COMPANY_8/sales/order/page.tsx | 571 +++++------------- .../COMPANY_8/sales/shipping-order/page.tsx | 2 +- .../(main)/COMPANY_9/equipment/info/page.tsx | 33 +- .../logistics/material-status/page.tsx | 137 +++-- .../COMPANY_9/logistics/outbound/page.tsx | 157 +++-- .../COMPANY_9/logistics/packaging/page.tsx | 26 +- .../COMPANY_9/logistics/receiving/page.tsx | 159 +++-- .../COMPANY_9/master-data/company/page.tsx | 89 --- .../COMPANY_9/master-data/department/page.tsx | 68 ++- .../COMPANY_9/master-data/item-info/page.tsx | 67 +- .../outsourcing/subcontractor-item/page.tsx | 27 +- .../production/plan-management/page.tsx | 99 +-- .../(main)/COMPANY_9/purchase/order/page.tsx | 54 +- .../COMPANY_9/purchase/purchase-item/page.tsx | 26 +- .../COMPANY_9/purchase/supplier/page.tsx | 81 ++- .../quality/item-inspection/page.tsx | 56 +- .../(main)/COMPANY_9/sales/customer/page.tsx | 93 ++- .../COMPANY_9/sales/shipping-order/page.tsx | 2 +- frontend/components/common/ImageUpload.tsx | 7 +- 124 files changed, 5541 insertions(+), 5662 deletions(-) diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts index 2b04b8e7..97dcab9d 100644 --- a/backend-node/src/controllers/adminController.ts +++ b/backend-node/src/controllers/adminController.ts @@ -4108,6 +4108,7 @@ interface UserWithDeptRequest { dept_name?: string; position_code?: string; position_name?: string; + end_date?: string | null; }; mainDept?: { dept_code: string; @@ -4199,6 +4200,7 @@ export const saveUserWithDept = async ( dept_name: deptName, position_code: userInfo.position_code, position_name: positionName, + end_date: userInfo.end_date !== undefined ? (userInfo.end_date ? `${userInfo.end_date.substring(0, 10)}T00:00:00+09:00` : null) : undefined, company_code: companyCode !== "*" ? companyCode : undefined, }; @@ -4230,8 +4232,8 @@ export const saveUserWithDept = async ( email, tel, cell_phone, sabun, user_type, user_type_name, status, locale, dept_code, dept_name, position_code, position_name, - company_code, regdate - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, NOW())`, + company_code, end_date, regdate + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, NOW())`, [ userInfo.user_id, userInfo.user_name, @@ -4250,6 +4252,7 @@ export const saveUserWithDept = async ( userInfo.position_code || null, positionName, companyCode !== "*" ? companyCode : null, + userInfo.end_date ? `${userInfo.end_date.substring(0, 10)}T00:00:00+09:00` : null, ] ); } diff --git a/backend-node/src/controllers/analyticsReportController.ts b/backend-node/src/controllers/analyticsReportController.ts index 33109dc2..c893226c 100644 --- a/backend-node/src/controllers/analyticsReportController.ts +++ b/backend-node/src/controllers/analyticsReportController.ts @@ -256,11 +256,11 @@ export async function getPurchaseReportData(req: any, res: Response): Promise= 6) { - dateFolder = `${filePathParts[3]}/${filePathParts[4]}/${filePathParts[5]}`; + const dbFilePath = fileRecord.file_path || ""; + const uploadsIdx = dbFilePath.indexOf("/uploads/"); + let finalPath: string; + if (uploadsIdx !== -1) { + const relativePath = dbFilePath.substring(uploadsIdx + "/uploads/".length); + finalPath = path.join(baseUploadDir, relativePath); + } else { + // fallback: 기존 방식 + const filePathParts = dbFilePath.split("/"); + let fileCompanyCode = filePathParts[2] || "DEFAULT"; + if (fileCompanyCode === "company_*") { + fileCompanyCode = "company_*"; + } + let dateFolder = ""; + if (filePathParts.length >= 6) { + dateFolder = `${filePathParts[3]}/${filePathParts[4]}/${filePathParts[5]}`; + } + const companyUploadDir = getCompanyUploadDir( + fileCompanyCode, + dateFolder || undefined + ); + finalPath = path.join(companyUploadDir, fileName); } - const companyUploadDir = getCompanyUploadDir( - fileCompanyCode, - dateFolder || undefined - ); - const filePath = path.join(companyUploadDir, fileName); - console.log("🔍 파일 미리보기 경로 확인:", { objid: objid, filePathFromDB: fileRecord.file_path, companyCode: companyCode, - dateFolder: dateFolder, - fileName: fileName, - companyUploadDir: companyUploadDir, - finalFilePath: filePath, - fileExists: fs.existsSync(filePath), + finalFilePath: finalPath, + fileExists: fs.existsSync(finalPath), }); - if (!fs.existsSync(filePath)) { - console.error("❌ 파일 없음:", filePath); + if (!fs.existsSync(finalPath)) { + console.error("❌ 파일 없음:", finalPath); res.status(404).json({ success: false, - message: `실제 파일을 찾을 수 없습니다: ${filePath}`, + message: `실제 파일을 찾을 수 없습니다: ${finalPath}`, }); return; } @@ -929,7 +929,7 @@ export const previewFile = async ( res.setHeader("Content-Type", mimeType); // 파일 스트림으로 전송 - const fileStream = fs.createReadStream(filePath); + const fileStream = fs.createReadStream(finalPath); fileStream.pipe(res); } catch (error) { console.error("파일 미리보기 오류:", error); diff --git a/backend-node/src/controllers/outboundController.ts b/backend-node/src/controllers/outboundController.ts index 7e77974c..c2d8ed00 100644 --- a/backend-node/src/controllers/outboundController.ts +++ b/backend-node/src/controllers/outboundController.ts @@ -229,15 +229,33 @@ export async function create(req: AuthenticatedRequest, res: Response) { ); } - // 판매출고인 경우 출하지시의 ship_qty 업데이트 + // 판매출고인 경우 출하지시의 ship_qty 업데이트 + 수주상세 ship_qty 반영 if (item.outbound_type === "판매출고" && item.source_id && item.source_type === "shipment_instruction_detail") { + const outQtyNum = Number(item.outbound_qty) || 0; await client.query( `UPDATE shipment_instruction_detail SET ship_qty = COALESCE(ship_qty, 0) + $1, updated_date = NOW() WHERE id = $2 AND company_code = $3`, - [item.outbound_qty || 0, item.source_id, companyCode] + [outQtyNum, item.source_id, companyCode] ); + + // 출하지시 상세의 detail_id로 수주상세(sales_order_detail) ship_qty도 업데이트 + const sidRes = await client.query( + `SELECT detail_id FROM shipment_instruction_detail WHERE id = $1 AND company_code = $2`, + [item.source_id, companyCode] + ); + const detailId = sidRes.rows[0]?.detail_id; + if (detailId) { + await client.query( + `UPDATE sales_order_detail + SET ship_qty = (COALESCE(NULLIF(ship_qty,'')::numeric, 0) + $1)::text, + balance_qty = (COALESCE(NULLIF(qty,'')::numeric, 0) - COALESCE(NULLIF(ship_qty,'')::numeric, 0) - $1)::text, + updated_date = NOW() + WHERE id = $2 AND company_code = $3`, + [outQtyNum, detailId, companyCode] + ); + } } } diff --git a/backend-node/src/controllers/receivingController.ts b/backend-node/src/controllers/receivingController.ts index fb358a06..9084160f 100644 --- a/backend-node/src/controllers/receivingController.ts +++ b/backend-node/src/controllers/receivingController.ts @@ -332,8 +332,24 @@ export async function create(req: AuthenticatedRequest, res: Response) { [purchaseNo, companyCode] ); const newStatus = unreceived.rows.length === 0 ? '입고완료' : '부분입고'; + // 발주 헤더의 received_qty도 디테일 합계로 동기화 await client.query( - `UPDATE purchase_order_mng SET status = $1, updated_date = NOW() + `UPDATE purchase_order_mng SET + status = $1, + received_qty = ( + SELECT CAST(COALESCE(SUM(CAST(NULLIF(received_qty, '') AS numeric)), 0) AS text) + FROM purchase_detail + WHERE purchase_no = $2 AND company_code = $3 + ), + remain_qty = ( + SELECT CAST(COALESCE(SUM( + COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0) + - COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) + ), 0) AS text) + FROM purchase_detail + WHERE purchase_no = $2 AND company_code = $3 + ), + updated_date = NOW() WHERE purchase_no = $2 AND company_code = $3`, [newStatus, purchaseNo, companyCode] ); diff --git a/backend-node/src/services/productionPlanService.ts b/backend-node/src/services/productionPlanService.ts index 705033cc..b827001b 100644 --- a/backend-node/src/services/productionPlanService.ts +++ b/backend-node/src/services/productionPlanService.ts @@ -46,20 +46,22 @@ export async function getOrderSummary( const itemLeadTimeCte = hasLeadTime ? `item_lead_time AS ( - SELECT + SELECT DISTINCT ON (item_number) item_number, id AS item_id, COALESCE(lead_time::int, 0) AS lead_time FROM item_info WHERE company_code = $1 + ORDER BY item_number, created_date DESC ),` : `item_lead_time AS ( - SELECT + SELECT DISTINCT ON (item_number) item_number, id AS item_id, 0 AS lead_time FROM item_info WHERE company_code = $1 + ORDER BY item_number, created_date DESC ),`; const query = ` @@ -97,6 +99,12 @@ export async function getOrderSummary( WHERE sd.company_code = $1 AND sd.part_code IS NOT NULL AND sd.part_code != '' ), + distinct_item AS ( + SELECT DISTINCT ON (item_number, company_code) + item_number, item_name, company_code + FROM item_info + ORDER BY item_number, company_code, created_date DESC + ), order_summary AS ( SELECT ao.part_code AS item_code, @@ -107,7 +115,7 @@ export async function getOrderSummary( COUNT(*) AS order_count, MIN(ao.due_date) AS earliest_due_date FROM all_orders ao - LEFT JOIN item_info ii ON ao.part_code = ii.item_number AND ao.company_code = ii.company_code + LEFT JOIN distinct_item ii ON ao.part_code = ii.item_number AND ao.company_code = ii.company_code GROUP BY ao.part_code, COALESCE(NULLIF(ao.part_name, ''), ii.item_name, ao.part_code) ), ${itemLeadTimeCte} diff --git a/frontend/app/(main)/COMPANY_10/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_10/equipment/info/page.tsx index eeb63844..cd53e9b1 100644 --- a/frontend/app/(main)/COMPANY_10/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_10/equipment/info/page.tsx @@ -142,15 +142,20 @@ export default function EquipmentInfoPage() { }; const mainTableColumns = useMemo(() => { - const cols: EDataTableColumn[] = []; - if (ts.isVisible("equipment_code")) cols.push({ key: "equipment_code", label: "설비코드", width: "w-[110px]" }); - if (ts.isVisible("equipment_name")) cols.push({ key: "equipment_name", label: "설비명", minWidth: "min-w-[130px]", truncate: true, render: (v) => v || "-" }); - if (ts.isVisible("equipment_type")) cols.push({ key: "equipment_type", label: "설비유형", width: "w-[90px]", render: (v) => v || "-" }); - if (ts.isVisible("manufacturer")) cols.push({ key: "manufacturer", label: "제조사", width: "w-[100px]", render: (v) => v || "-" }); - if (ts.isVisible("installation_location")) cols.push({ key: "installation_location", label: "설치장소", width: "w-[100px]", render: (v) => v || "-" }); - if (ts.isVisible("operation_status")) cols.push({ key: "operation_status", label: "가동상태", width: "w-[80px]", render: (v) => v || "-" }); - return cols; - }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + 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 || "-" }, + manufacturer: { width: "w-[100px]", render: (v) => v || "-" }, + installation_location: { width: "w-[100px]", render: (v) => v || "-" }, + operation_status: { width: "w-[80px]", render: (v) => v || "-" }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 설비 조회 const fetchEquipments = useCallback(async () => { @@ -272,8 +277,8 @@ export default function EquipmentInfoPage() { if (!inspectionForm.inspection_cycle) { toast.error("점검주기는 필수입니다."); return; } if (!inspectionForm.inspection_method) { toast.error("점검방법은 필수입니다."); return; } const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; - if (isNumeric && !inspectionForm.unit) { toast.error("숫자 점검방법은 측정단위가 필수입니다."); return; } + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); + if (isNumeric && !inspectionForm.unit) { toast.error("측정단위가 필수입니다."); return; } // 기준값/오차범위 → 하한치/상한치 자동 계산 const saveData = { ...inspectionForm }; if (isNumeric && saveData.standard_value) { @@ -739,7 +744,7 @@ export default function EquipmentInfoPage() {
{catSelect("inspection_method", inspectionForm.inspection_method, (v) => { const label = resolve("inspection_method", v); - const isNum = label === "숫자" || v === "숫자"; + const isNum = ["숫자", "치수검사"].includes(label) || ["숫자", "치수검사"].includes(v); if (!isNum) { setInspectionForm((p) => ({ ...p, inspection_method: v, unit: "", standard_value: "", tolerance: "", lower_limit: "", upper_limit: "" })); } else { @@ -748,7 +753,7 @@ export default function EquipmentInfoPage() { }, "점검방법")}
{(() => { const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); if (!isNumeric) return null; return (
@@ -758,7 +763,7 @@ export default function EquipmentInfoPage() {
{(() => { const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); if (!isNumeric) return null; return (
diff --git a/frontend/app/(main)/COMPANY_10/logistics/material-status/page.tsx b/frontend/app/(main)/COMPANY_10/logistics/material-status/page.tsx index 46de306c..eb87ba92 100644 --- a/frontend/app/(main)/COMPANY_10/logistics/material-status/page.tsx +++ b/frontend/app/(main)/COMPANY_10/logistics/material-status/page.tsx @@ -333,69 +333,90 @@ export default function MaterialStatusPage() {

) : ( - workOrders.map((wo) => ( -
handleSelectWo(wo.id)} - > + ts.groupData(workOrders).map((wo) => { + if ((wo as any)._isGroupSummary || (wo as any)._isGroupHeader) return null; + return (
e.stopPropagation()} + key={wo.id} + className={cn( + "flex gap-3 rounded-lg border p-3 transition-all cursor-pointer", + "hover:border-primary/50 hover:shadow-sm", + selectedWoId === wo.id + ? "border-primary bg-primary/5 shadow-sm" + : "border-border" + )} + onClick={() => handleSelectWo(wo.id)} > - - handleCheckWo(wo.id, c as boolean) - } - /> -
-
-
- - {wo.plan_no || wo.work_order_no || `WO-${wo.id}`} - - e.stopPropagation()} + > + + handleCheckWo(wo.id, c as boolean) + } + /> +
+
+
+ {ts.isVisible("plan_no") && ( + + {wo.plan_no || wo.work_order_no || `WO-${wo.id}`} + )} - > - {getStatusLabel(wo.status)} - -
-
- - {wo.item_name} - - - ({wo.item_code}) - -
-
- 수량: - - {Number(wo.plan_qty).toLocaleString()}개 - - | - 일자: - - {wo.plan_date - ? new Date(wo.plan_date) - .toISOString() - .slice(0, 10) - : "-"} - + {ts.isVisible("status") && ( + + {getStatusLabel(wo.status)} + + )} +
+
+ {ts.isVisible("item_name") && ( + + {wo.item_name} + + )} + {ts.isVisible("item_code") && ( + + ({wo.item_code}) + + )} +
+
+ {ts.isVisible("plan_qty") && ( + <> + 수량: + + {Number(wo.plan_qty).toLocaleString()}개 + + + )} + {ts.isVisible("plan_qty") && ts.isVisible("plan_date") && ( + | + )} + {ts.isVisible("plan_date") && ( + <> + 일자: + + {wo.plan_date + ? new Date(wo.plan_date) + .toISOString() + .slice(0, 10) + : "-"} + + + )} +
-
- )) + ); + }) )} diff --git a/frontend/app/(main)/COMPANY_10/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_10/logistics/outbound/page.tsx index 5ab46a8a..c1ffbd40 100644 --- a/frontend/app/(main)/COMPANY_10/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_10/logistics/outbound/page.tsx @@ -140,8 +140,16 @@ const DETAIL_HEADER_COLS = [ // 마스터 필드 키 목록 (필터 분류용) const MASTER_KEYS = new Set(["outbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 출고번호(1) + 마스터필드(7) = 10 -const TOTAL_COLS = 10; +// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) +const DETAIL_KEY_MAP: Record = { + source_type: "source_type", + item_number: "item_code", + item_name: "item_name", + spec: "specification", + outbound_qty: "outbound_qty", + unit_price: "unit_price", + total_amount: "total_amount", +}; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -248,6 +256,31 @@ interface SelectedSourceItem { export default function OutboundPage() { const ts = useTableSettings("c16-outbound", "outbound_mng", GRID_COLUMNS); + + // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 + const visibleMasterLayout = useMemo(() => { + const ordered: typeof MASTER_BODY_LAYOUT = []; + for (const vc of ts.visibleColumns) { + const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); + if (m) ordered.push(m); + } + return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; + }, [ts.visibleColumns]); + + const visibleDetailCols = useMemo(() => { + const ordered: typeof DETAIL_HEADER_COLS = []; + for (const vc of ts.visibleColumns) { + const detailKey = DETAIL_KEY_MAP[vc.key]; + if (detailKey) { + const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); + if (d) ordered.push(d); + } + } + return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; + }, [ts.visibleColumns]); + + const TOTAL_COLS = 3 + visibleMasterLayout.length; + // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -900,8 +933,15 @@ export default function OutboundPage() {
- - +
+ + + + + {visibleMasterLayout.map((col) => ( + + ))} + - {/* 마스터 필드 헤더 */} - {MASTER_BODY_LAYOUT.map((col) => ( + {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => (
handleSort(col.key)}> @@ -1039,38 +1079,51 @@ export default function OutboundPage() { {outboundNo} ({group.details.length}) - {/* 출고유형 */} - - - {master.outbound_type || "-"} - - - {/* 출고일 */} - - {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} - - {/* 참조번호 */} - - {master.reference_number || ""} - - {/* 거래처 */} - - {master.customer_name || ""} - - {/* 창고 */} - - {master.warehouse_name || master.warehouse_code || ""} - - {/* 출고상태 */} - - - {master.outbound_status || "-"} - - - {/* 비고 */} - - {master.memo || ""} - + {/* 마스터 필드 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => { + switch (col.key) { + case "outbound_type": return ( + + + {master.outbound_type || "-"} + + + ); + case "outbound_date": return ( + + {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} + + ); + case "reference_number": return ( + + {master.reference_number || ""} + + ); + case "customer_name": return ( + + {master.customer_name || ""} + + ); + case "warehouse_name": return ( + + {master.warehouse_name || master.warehouse_code || ""} + + ); + case "outbound_status": return ( + + + {master.outbound_status || "-"} + + + ); + case "memo": return ( + + {master.memo || ""} + + ); + default: return {(master as any)[col.key] ?? ""}; + } + })} {/* 디테일 서브 헤더 (펼쳤을 때만) */} @@ -1084,7 +1137,7 @@ export default function OutboundPage() { - {DETAIL_HEADER_COLS.map((col) => { + {visibleDetailCols.map((col) => { const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); const isSorted = sortState?.key === col.key; const uniqueVals = Array.from(new Set( @@ -1163,20 +1216,18 @@ export default function OutboundPage() {
- {/* 출처 */} - {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"} - {/* 품목코드 */} - {row.item_code || ""} - {/* 품목명 */} - {row.item_name || ""} - {/* 규격 */} - {row.specification || ""} - {/* 출고수량 */} - {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""} - {/* 단가 */} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {/* 금액 */} - {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {visibleDetailCols.map((col) => { + switch (col.key) { + case "source_type": return {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"}; + case "item_code": return {row.item_code || ""}; + case "item_name": return {row.item_name || ""}; + case "specification": return {row.specification || ""}; + case "outbound_qty": return {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""}; + case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; + case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; + default: return {(row as any)[col.key] ?? ""}; + } + })} ); })} diff --git a/frontend/app/(main)/COMPANY_10/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_10/logistics/packaging/page.tsx index 5d4d5787..6ae340aa 100644 --- a/frontend/app/(main)/COMPANY_10/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_10/logistics/packaging/page.tsx @@ -460,18 +460,20 @@ export default function PackagingPage() { {/* 포장재 목록 테이블 */}
PKG_TYPE_LABEL[v] || v || "-" }, - { key: "size", label: "크기(mm)", width: "w-[100px]", render: (_v, row) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, - { key: "max_load_kg", label: "최대중량", width: "w-[80px]", align: "right", render: (v) => Number(v || 0) > 0 ? `${v}kg` : "-" }, - { key: "status", label: "상태", width: "w-[60px]", align: "center", render: (v) => ( - - {STATUS_LABEL[v] || v} - - )}, - ] as EDataTableColumn[]} + columns={ts.visibleColumns.map((col): EDataTableColumn => { + const renderMap: Record>> = { + pkg_type: { width: "w-[80px]", render: (v: any) => PKG_TYPE_LABEL[v] || v || "-" }, + size: { width: "w-[100px]", render: (_v: any, row: any) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, + max_weight: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, + max_load_kg: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, + status: { width: "w-[60px]", align: "center", render: (v: any) => ( + + {STATUS_LABEL[v] || v} + + )}, + }; + return { key: col.key, label: col.label, ...renderMap[col.key] }; + })} data={ts.groupData(filteredPkgUnits)} rowKey={(row) => String(row.id)} loading={pkgLoading} diff --git a/frontend/app/(main)/COMPANY_10/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_10/logistics/receiving/page.tsx index a8d5fc2c..85cdc23c 100644 --- a/frontend/app/(main)/COMPANY_10/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_10/logistics/receiving/page.tsx @@ -117,12 +117,20 @@ const DETAIL_HEADER_COLS = [ { key: "total_amount", label: "금액" }, ]; -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 입고번호(1) + 디테일(7) = 10 -const TOTAL_COLS = 10; - // 마스터 필드 키 목록 (필터 분류용) const MASTER_KEYS = new Set(["inbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); +// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) +const DETAIL_KEY_MAP: Record = { + source_type: "source_table", + item_number: "item_number", + item_name: "item_name", + spec: "spec", + inbound_qty: "inbound_qty", + unit_price: "unit_price", + total_amount: "total_amount", +}; + // 헤더 필터 Popover function HeaderFilterPopover({ colKey, colLabel, uniqueValues, filterValues, onToggle, onClear, @@ -278,6 +286,31 @@ interface SelectedSourceItem { export default function ReceivingPage() { const ts = useTableSettings("c16-receiving", "inbound_mng", GRID_COLUMNS); + + // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 + const visibleMasterLayout = useMemo(() => { + const ordered: typeof MASTER_BODY_LAYOUT = []; + for (const vc of ts.visibleColumns) { + const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); + if (m) ordered.push(m); + } + return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; + }, [ts.visibleColumns]); + + const visibleDetailCols = useMemo(() => { + const ordered: typeof DETAIL_HEADER_COLS = []; + for (const vc of ts.visibleColumns) { + const detailKey = DETAIL_KEY_MAP[vc.key]; + if (detailKey) { + const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); + if (d) ordered.push(d); + } + } + return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; + }, [ts.visibleColumns]); + + const TOTAL_COLS = 3 + visibleMasterLayout.length; + // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -847,8 +880,15 @@ export default function ReceivingPage() {
-
- +
+ + + + + {visibleMasterLayout.map((col) => ( + + ))} + - {/* 마스터 필드 헤더 (colSpan으로 디테일 컬럼 위에 맵핑) */} - {MASTER_BODY_LAYOUT.map((col) => ( + {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => (
handleSort(col.key)}> @@ -985,38 +1025,51 @@ export default function ReceivingPage() { {inboundNo} ({group.details.length}) - {/* 입고유형 */} - - - {resolveInboundType(master.inbound_type)} - - - {/* 입고일 */} - - {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} - - {/* 참조번호 */} - - {master.reference_number || ""} - - {/* 공급처 */} - - {master.supplier_name || ""} - - {/* 창고 */} - - {master.warehouse_name || master.warehouse_code || ""} - - {/* 입고상태 */} - - - {master.inbound_status || "-"} - - - {/* 비고 */} - - {master.memo || ""} - + {/* 마스터 필드 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => { + switch (col.key) { + case "inbound_type": return ( + + + {resolveInboundType(master.inbound_type)} + + + ); + case "inbound_date": return ( + + {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} + + ); + case "reference_number": return ( + + {master.reference_number || ""} + + ); + case "supplier_name": return ( + + {master.supplier_name || ""} + + ); + case "warehouse_name": return ( + + {master.warehouse_name || master.warehouse_code || ""} + + ); + case "inbound_status": return ( + + + {master.inbound_status || "-"} + + + ); + case "memo": return ( + + {master.memo || ""} + + ); + default: return {(master as any)[col.key] ?? ""}; + } + })} {/* 디테일 서브 헤더 (펼쳤을 때만) */} @@ -1030,7 +1083,7 @@ export default function ReceivingPage() { - {DETAIL_HEADER_COLS.map((col) => { + {visibleDetailCols.map((col) => { const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); const isSorted = sortState?.key === col.key; const uniqueVals = Array.from(new Set( @@ -1108,20 +1161,18 @@ export default function ReceivingPage() {
- {/* 출처 */} - {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"} - {/* 품목코드 */} - {row.item_number || ""} - {/* 품목명 */} - {row.item_name || ""} - {/* 규격 */} - {row.spec || ""} - {/* 입고수량 */} - {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""} - {/* 단가 */} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {/* 금액 */} - {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {visibleDetailCols.map((col) => { + switch (col.key) { + case "source_table": return {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"}; + case "item_number": return {row.item_number || ""}; + case "item_name": return {row.item_name || ""}; + case "spec": return {row.spec || ""}; + case "inbound_qty": return {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""}; + case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; + case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; + default: return {(row as any)[col.key] ?? ""}; + } + })} ); })} 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 dfd1b666..9d7f2dea 100644 --- a/frontend/app/(main)/COMPANY_10/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_10/master-data/company/page.tsx @@ -491,12 +491,6 @@ export default function CompanyPage() { > 회사정보 - - 부서관리 -
@@ -635,89 +629,6 @@ export default function CompanyPage() {
- {/* ===================== Tab 2: 부서관리 ===================== */} - -
- - {/* 좌측: 부서 트리 */} - -
-
-
- - 부서 - {depts.length}건 -
-
- - - -
-
-
- {deptLoading ? ( -
- -
- ) : deptTree.length === 0 ? ( -
- - 등록된 부서가 없어요 -
- ) : ( - renderTree(deptTree) - )} -
-
-
- - - - {/* 우측: 사원 목록 */} - -
-
-
- - {selectedDept ? "부서 인원" : "부서를 선택해주세요"} - {selectedDept && {selectedDept.dept_name}} - {members.length > 0 && {members.length}명} -
- {selectedDeptCode && ( - - )} -
- {selectedDeptCode ? ( - row.user_id || row.id} - loading={memberLoading} - emptyMessage="소속 사원이 없어요" - emptyIcon={} - onRowDoubleClick={(row) => openUserModal(row)} - showPagination={false} - draggableColumns={false} - /> - ) : ( -
- - 좌측에서 부서를 선택해주세요 -
- )} -
-
-
-
-
{/* ── 부서 등록/수정 모달 ── */} diff --git a/frontend/app/(main)/COMPANY_10/master-data/department/page.tsx b/frontend/app/(main)/COMPANY_10/master-data/department/page.tsx index a2bbcba5..3245571e 100644 --- a/frontend/app/(main)/COMPANY_10/master-data/department/page.tsx +++ b/frontend/app/(main)/COMPANY_10/master-data/department/page.tsx @@ -9,7 +9,7 @@ * 모달: 부서 등록(dept_info), 사원 추가(user_info) */ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -279,6 +279,7 @@ export default function DepartmentPage() { dept_code: userForm.dept_code || undefined, dept_name: depts.find((d) => d.dept_code === userForm.dept_code)?.dept_name || undefined, status: userForm.status || "active", + end_date: userForm.end_date || null, }, mainDept: userForm.dept_code ? { dept_code: userForm.dept_code, @@ -312,37 +313,40 @@ export default function DepartmentPage() { const activeMembers = members.filter((m) => !m.end_date || m.end_date.substring(0, 10) >= today); const resignedMembers = members.filter((m) => m.end_date && m.end_date.substring(0, 10) < today); - const isColVisible = (key: string) => ts.isVisible(key); - - // EDataTable 컬럼 정의 (부서 목록) - const deptColumns: EDataTableColumn[] = [ - { key: "dept_code", label: "부서코드", width: "w-[120px]" }, - { key: "dept_name", label: "부서명", minWidth: "min-w-[140px]" }, - ...(isColVisible("parent_dept_code") - ? [{ - key: "parent_dept_code", - label: "상위부서", - width: "w-[110px]", - render: (val: any) => {val || "\u2014"}, - }] - : []), - ...(isColVisible("status") - ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val === "active" ? "활성" : (val || "\u2014")} - - ) : null, - }] - : []), - ]; + // EDataTable 컬럼 정의 (부서 목록) — ts.visibleColumns 순서를 따름 + const deptColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + dept_code: { width: "w-[120px]" }, + dept_name: { minWidth: "min-w-[140px]" }, + parent_dept_code: { + width: "w-[110px]", + render: (val: any) => {val || "\u2014"}, + }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val === "active" ? "활성" : (val || "\u2014")} + + ) : null, + }, + }; + // dept_code, dept_name은 항상 표시 (DEPT_COLUMNS에 포함되지 않으므로 visibleColumns에 없음) + const fixedCols: EDataTableColumn[] = [ + { key: "dept_code", label: "부서코드", ...colProps["dept_code"] }, + { key: "dept_name", label: "부서명", ...colProps["dept_name"] }, + ]; + const dynamicCols = ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + return [...fixedCols, ...dynamicCols]; + }, [ts.visibleColumns]); return (
diff --git a/frontend/app/(main)/COMPANY_10/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_10/master-data/item-info/page.tsx index 3f037275..375fd900 100644 --- a/frontend/app/(main)/COMPANY_10/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_10/master-data/item-info/page.tsx @@ -84,6 +84,56 @@ function CategoryCombobox({ options, value, onChange, placeholder }: { ); } +// 다중 선택 카테고리 콤보박스 +function MultiCategoryCombobox({ options, value, onChange, placeholder }: { + options: { code: string; label: string }[]; + value: string; + onChange: (v: string) => void; + placeholder: string; +}) { + const [open, setOpen] = useState(false); + const selectedCodes = value ? value.split(",").map((c) => c.trim()).filter(Boolean) : []; + const selectedLabels = selectedCodes.map((code) => options.find((o) => o.code === code)?.label || code).filter(Boolean); + + const toggle = (code: string) => { + const next = selectedCodes.includes(code) + ? selectedCodes.filter((c) => c !== code) + : [...selectedCodes, code]; + onChange(next.join(",")); + }; + + return ( + + + + + + + + + 검색 결과가 없어요 + + {options.map((opt) => ( + toggle(opt.code)}> + + {opt.label} + + ))} + + + + + + ); +} + const TABLE_NAME = "item_info"; const GRID_COLUMNS = [ @@ -108,7 +158,7 @@ const GRID_COLUMNS = [ const FORM_FIELDS = [ { key: "item_number", label: "품목코드", type: "text", required: true, disabled: true, placeholder: "자동 채번" }, { key: "item_name", label: "품명", type: "text", required: true }, - { key: "division", label: "관리품목", type: "category" }, + { key: "division", label: "관리품목", type: "multi-category" }, { key: "type", label: "품목구분", type: "category" }, { key: "size", label: "규격", type: "text" }, { key: "unit", label: "단위", type: "category" }, @@ -137,6 +187,7 @@ export default function ItemInfoPage() { const { user } = useAuth(); const ts = useTableSettings("c16-item-info", TABLE_NAME, GRID_COLUMNS); const [items, setItems] = useState([]); + const [rawItems, setRawItems] = useState([]); const [loading, setLoading] = useState(false); // 검색 필터 (DynamicSearchFilter) @@ -215,6 +266,7 @@ export default function ItemInfoPage() { } return categoryOptions[col]?.find((o) => o.code === code)?.label || code; }; + setRawItems(raw); const data = raw.map((r: any) => { const converted = { ...r }; for (const col of CATEGORY_COLUMNS) { @@ -261,7 +313,8 @@ export default function ItemInfoPage() { // 수정 모달 열기 const openEditModal = (item: any) => { - setFormData({ ...item }); + const raw = rawItems.find((r) => r.id === item.id) || item; + setFormData({ ...raw }); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); @@ -269,7 +322,8 @@ export default function ItemInfoPage() { // 복사 모달 열기 const openCopyModal = async (item: any) => { - const { id, item_number, created_date, updated_date, writer, ...rest } = item; + const raw = rawItems.find((r) => r.id === item.id) || item; + const { id, item_number, created_date, updated_date, writer, ...rest } = raw; setFormData(rest); setIsEditMode(false); setEditId(null); @@ -459,6 +513,13 @@ export default function ItemInfoPage() { columnName={field.key} height="h-32" /> + ) : field.type === "multi-category" ? ( + setFormData((prev) => ({ ...prev, [field.key]: v }))} + placeholder={`${field.label} 선택`} + /> ) : field.type === "category" ? ( (() => { - const cols: EDataTableColumn[] = []; - if (ts.isVisible("item_number")) cols.push({ key: "item_number", label: "품번", width: "w-[110px]" }); - if (ts.isVisible("item_name")) cols.push({ key: "item_name", label: "품명", minWidth: "min-w-[130px]", render: (v) => v || "-" }); - if (ts.isVisible("size")) cols.push({ key: "size", label: "규격", width: "w-[90px]", render: (v) => v || "-" }); - if (ts.isVisible("unit")) cols.push({ key: "unit", label: "단위", width: "w-[60px]", render: (v) => v || "-" }); - if (ts.isVisible("standard_price")) cols.push({ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }); - if (ts.isVisible("selling_price")) cols.push({ key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true }); - if (ts.isVisible("currency_code")) cols.push({ key: "currency_code", label: "통화", width: "w-[50px]", render: (v) => v || "-" }); - if (ts.isVisible("status")) cols.push({ key: "status", label: "상태", width: "w-[60px]", render: (v) => v || "-" }); - return cols; - }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + const colProps: Record> = { + item_number: { width: "w-[110px]" }, + item_name: { minWidth: "min-w-[130px]", render: (v) => v || "-" }, + size: { width: "w-[90px]", render: (v) => v || "-" }, + unit: { width: "w-[60px]", render: (v) => v || "-" }, + standard_price: { width: "w-[90px]", align: "right", formatNumber: true }, + selling_price: { width: "w-[90px]", align: "right", formatNumber: true }, + currency_code: { width: "w-[50px]", render: (v) => v || "-" }, + status: { width: "w-[60px]", render: (v) => v || "-" }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 좌측: 품목 조회 (division이 "외주관리"인 품목만 필터링) const outsourcingDivisionCode = categoryOptions["division"]?.find( diff --git a/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx index 4a0d341a..6bd63176 100644 --- a/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx @@ -919,9 +919,7 @@ export default function ProductionPlanManagementPage() { // 숫자 포맷 const formatNumber = (num: number | string) => Number(num).toLocaleString(); - // 컬럼 표시 여부 - const isColVisible = (key: string) => ts.isVisible(key); - const orderColSpan = 4 + ORDER_COLUMNS.filter((c) => isColVisible(c.key)).length; + // (컬럼 표시는 ts.visibleColumns 순서를 따름) return (
@@ -1019,6 +1017,38 @@ export default function ProductionPlanManagementPage() {
) : (
+ {(() => { + // 디테일 행에서 개별 값을 표시하는 컬럼 매핑 + const DETAIL_VALUE_MAP: Record = { + total_order_qty: "order_qty", + total_ship_qty: "ship_qty", + total_balance_qty: "balance_qty", + }; + + // 그룹 행에서 특수 렌더링이 필요한 컬럼 + const renderGroupCell = (col: { key: string }, item: any) => { + if (col.key === "required_plan_qty") { + return ( + 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> + {formatNumber(item.required_plan_qty)} + + ); + } + if (col.key === "lead_time") { + return ( + toggleItemExpand(item.item_code)}> + {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"} + + ); + } + return ( + toggleItemExpand(item.item_code)}> + {formatNumber(item[col.key])} + + ); + }; + + return (
@@ -1028,15 +1058,11 @@ export default function ProductionPlanManagementPage() { 품목코드 품목명 - {isColVisible("total_order_qty") && 총수주량} - {isColVisible("total_ship_qty") && 출고량} - {isColVisible("total_balance_qty") && 잔량} - {isColVisible("current_stock") && 현재고} - {isColVisible("safety_stock") && 안전재고} - {isColVisible("existing_plan_qty") && 기생산계획량} - {isColVisible("in_progress_qty") && 생산진행} - {isColVisible("required_plan_qty") && 필요생산계획} - {isColVisible("lead_time") && 리드타임(일)} + {ts.visibleColumns.map((col) => ( + + {col.label} + + ))} @@ -1046,6 +1072,7 @@ export default function ProductionPlanManagementPage() { + {ts.visibleColumns.map((col) => { const v = (item as any)[col.key]; return ( @@ -1068,25 +1095,14 @@ export default function ProductionPlanManagementPage() { toggleItemExpand(item.item_code)}>{item.item_code} toggleItemExpand(item.item_code)}>{item.item_name} - {isColVisible("total_order_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_order_qty)}} - {isColVisible("total_ship_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_ship_qty)}} - {isColVisible("total_balance_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_balance_qty)}} - {isColVisible("current_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.current_stock)}} - {isColVisible("safety_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.safety_stock)}} - {isColVisible("existing_plan_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.existing_plan_qty)}} - {isColVisible("in_progress_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.in_progress_qty)}} - {isColVisible("required_plan_qty") && ( - 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> - {formatNumber(item.required_plan_qty)} - - )} - {isColVisible("lead_time") && ( - toggleItemExpand(item.item_code)}> - {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"} - - )} + {ts.visibleColumns.map((col) => renderGroupCell(col, item))} - {expandedItems.has(item.item_code) && item.orders?.map((detail) => ( + {expandedItems.has(item.item_code) && item.orders?.map((detail: any) => { + let remainColSpan = 0; + for (const col of ts.visibleColumns) { + if (!DETAIL_VALUE_MAP[col.key]) remainColSpan++; + } + return ( @@ -1101,19 +1117,28 @@ export default function ProductionPlanManagementPage() { - {isColVisible("total_order_qty") && {formatNumber(detail.order_qty)}} - {isColVisible("total_ship_qty") && {formatNumber(detail.ship_qty)}} - {isColVisible("total_balance_qty") && {formatNumber(detail.balance_qty)}} - - 납기일: {detail.due_date || "-"} - + {ts.visibleColumns.map((col) => { + const detailKey = DETAIL_VALUE_MAP[col.key]; + if (detailKey) { + return {formatNumber(detail[detailKey])}; + } + return null; + })} + {remainColSpan > 0 && ( + + 납기일: {detail.due_date || "-"} + + )} - ))} + ); + })} ); })}
+ ); + })()}
)} diff --git a/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx index fa0e08c5..1bc3bc88 100644 --- a/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx @@ -742,10 +742,24 @@ export default function PurchaseOrderPage() { ) : ( (() => { const MASTER_KEYS = new Set(["purchase_no", "order_date", "supplier_name", "status", "memo"]); - const detailCols = ts.visibleColumns.filter(c => !MASTER_KEYS.has(c.key)); - const masterCols = ts.visibleColumns.filter(c => MASTER_KEYS.has(c.key)); const numCols = new Set(["order_qty", "received_qty", "remain_qty", "unit_price", "amount"]); + // ts.visibleColumns 순서를 따르되, 마스터/디테일 컬럼을 분리 + // 고정 컬럼(품목수)은 마스터 선행 컬럼 뒤에 배치 + const leadingMaster: typeof ts.visibleColumns = []; + const detailCols: typeof ts.visibleColumns = []; + const trailingMaster: typeof ts.visibleColumns = []; + let passedFirstDetail = false; + for (const col of ts.visibleColumns) { + if (MASTER_KEYS.has(col.key)) { + if (passedFirstDetail) trailingMaster.push(col); + else leadingMaster.push(col); + } else { + passedFirstDetail = true; + detailCols.push(col); + } + } + const renderDetailCell = (row: any, key: string) => { const val = row[key]; if (key === "status") return val ? {val} : "-"; @@ -753,23 +767,35 @@ export default function PurchaseOrderPage() { return val || "-"; }; + const renderMasterHead = (col: { key: string; label: string }) => ( + + {col.label} + + ); + + const renderMasterCell = (col: { key: string }, m: any, purchaseNo: string) => { + if (col.key === "purchase_no") return {purchaseNo}; + if (col.key === "order_date") return {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}; + if (col.key === "supplier_name") return {m.supplier_name || "-"}; + if (col.key === "status") return {m.status && {m.status}}; + if (col.key === "memo") return {m.memo || ""}; + return ; + }; + return ( - {ts.isVisible("purchase_no") && 발주번호} - {ts.isVisible("order_date") && 발주일} - {ts.isVisible("supplier_name") && 공급업체} + {leadingMaster.map(renderMasterHead)} 품목수 {detailCols.map(col => ( {col.label}{col.key === "order_qty" || col.key === "amount" ? " 합계" : ""} ))} - {ts.isVisible("status") && 상태} - {ts.isVisible("memo") && 메모} + {trailingMaster.map(renderMasterHead)} @@ -795,9 +821,7 @@ export default function PurchaseOrderPage() { { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !detailIds.includes(id)) : [...new Set([...prev, ...detailIds])]); }}> {}} /> - {ts.isVisible("purchase_no") && {purchaseNo}} - {ts.isVisible("order_date") && {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}} - {ts.isVisible("supplier_name") && {m.supplier_name || "-"}} + {leadingMaster.map(col => renderMasterCell(col, m, purchaseNo))} {group.details.length}건 {detailCols.map(col => ( @@ -806,8 +830,7 @@ export default function PurchaseOrderPage() { : ""} ))} - {ts.isVisible("status") && {m.status && {m.status}}} - {ts.isVisible("memo") && {m.memo || ""}} + {trailingMaster.map(col => renderMasterCell(col, m, purchaseNo))} {isExpanded && group.details.map((row) => ( @@ -815,17 +838,14 @@ export default function PurchaseOrderPage() { { e.stopPropagation(); setCheckedIds(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id]); }}> {}} /> - {ts.isVisible("purchase_no") && } - {ts.isVisible("order_date") && } - {ts.isVisible("supplier_name") && } + {leadingMaster.map(col => )} {detailCols.map(col => ( {renderDetailCell(row, col.key)} ))} - {ts.isVisible("status") && } - {ts.isVisible("memo") && } + {trailingMaster.map(col => )} ))} diff --git a/frontend/app/(main)/COMPANY_10/purchase/purchase-item/page.tsx b/frontend/app/(main)/COMPANY_10/purchase/purchase-item/page.tsx index 1f7c4137..7f211a88 100644 --- a/frontend/app/(main)/COMPANY_10/purchase/purchase-item/page.tsx +++ b/frontend/app/(main)/COMPANY_10/purchase/purchase-item/page.tsx @@ -617,17 +617,21 @@ export default function PurchaseItemPage() { toast.success("다운로드 완료"); }; - // EDataTable 컬럼 정의 (구매품목) - const itemColumns: EDataTableColumn[] = [ - { key: "item_number", label: "품번", width: "w-[110px]" }, - { key: "item_name", label: "품명", minWidth: "min-w-[130px]" }, - { key: "size", label: "규격", width: "w-[80px]" }, - { key: "unit", label: "단위", width: "w-[60px]" }, - { key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }, - { key: "standard_price", label: "구매단가", width: "w-[90px]", align: "right", formatNumber: true }, - { key: "currency_code", label: "통화", width: "w-[50px]" }, - { key: "status", label: "상태", width: "w-[60px]" }, - ]; + // EDataTable 컬럼 정의 (구매품목) — ts.visibleColumns 기반 + const COLUMN_RENDER_MAP: Record> = { + item_number: { width: "w-[110px]" }, + item_name: { minWidth: "min-w-[130px]" }, + size: { width: "w-[80px]" }, + unit: { width: "w-[60px]" }, + standard_price: { width: "w-[90px]", align: "right", formatNumber: true }, + currency_code: { width: "w-[50px]" }, + status: { width: "w-[60px]" }, + }; + const itemColumns: EDataTableColumn[] = ts.visibleColumns.map((col): EDataTableColumn => ({ + key: col.key, + label: col.label, + ...COLUMN_RENDER_MAP[col.key], + })); return (
diff --git a/frontend/app/(main)/COMPANY_10/purchase/supplier/page.tsx b/frontend/app/(main)/COMPANY_10/purchase/supplier/page.tsx index 51c50aa5..521f770e 100644 --- a/frontend/app/(main)/COMPANY_10/purchase/supplier/page.tsx +++ b/frontend/app/(main)/COMPANY_10/purchase/supplier/page.tsx @@ -12,7 +12,7 @@ * - 납품처 등록 (delivery_destination) */ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -1229,47 +1229,44 @@ export default function SupplierManagementPage() { } }; - // 컬럼 가시성 헬퍼 - const isColumnVisible = (key: string) => ts.isVisible(key); - - const supplierColSpan = 1 + ["supplier_code", "supplier_name", "contact_person", "contact_phone", "division", "status"] - .filter((k) => isColumnVisible(k)).length; - - // EDataTable 컬럼 정의 (공급업체 목록) - const supplierColumns: EDataTableColumn[] = [ - ...(isColumnVisible("supplier_code") ? [{ key: "supplier_code", label: "공급업체코드", width: "w-[120px]" }] : []), - ...(isColumnVisible("supplier_name") ? [{ key: "supplier_name", label: "공급업체명", minWidth: "min-w-[140px]" }] : []), - ...(isColumnVisible("division") ? [{ - key: "division", - label: "공급업체유형", - width: "w-[80px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ...(isColumnVisible("contact_person") ? [{ key: "contact_person", label: "담당자", width: "w-[80px]" }] : []), - ...(isColumnVisible("contact_phone") ? [{ key: "contact_phone", label: "전화번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("email") ? [{ key: "email", label: "이메일", width: "w-[160px]" }] : []), - ...(isColumnVisible("business_number") ? [{ key: "business_number", label: "사업자번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("address") ? [{ key: "address", label: "주소", minWidth: "min-w-[150px]" }] : []), - ...(isColumnVisible("status") ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ]; + // EDataTable 컬럼 정의 (공급업체 목록) — ts.visibleColumns 순서를 따름 + const supplierColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + supplier_code: { width: "w-[120px]" }, + supplier_name: { minWidth: "min-w-[140px]" }, + division: { + width: "w-[80px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + contact_person: { width: "w-[80px]" }, + contact_phone: { width: "w-[120px]" }, + email: { width: "w-[160px]" }, + business_number: { width: "w-[120px]" }, + address: { minWidth: "min-w-[150px]" }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 엑셀 다운로드 const handleExcelDownload = async () => { diff --git a/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx index 5baf35d3..2c0e1338 100644 --- a/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx @@ -28,6 +28,7 @@ const GRID_COLUMNS = [ { key: "item_code", label: "품목코드" }, { key: "item_name", label: "품목명" }, { key: "inspection_type", label: "검사유형" }, + { key: "item_count", label: "항목수" }, { key: "is_active", label: "사용여부" }, ]; const ITEM_TABLE = "item_info"; @@ -420,18 +421,41 @@ export default function ItemInspectionInfoPage() { 0 && checkedIds.length === data.length} onCheckedChange={(v) => setCheckedIds(v ? data.map(r => r.id) : [])} /> - 품목코드 - 품목명 - 검사유형 - 항목수 - 사용여부 + {ts.visibleColumns.map((col) => ( + + {col.label} + + ))} - {groupedData.map((group) => { + {ts.groupData(groupedData).map((group) => { + if ((group as any)._isGroupSummary || (group as any)._isGroupHeader) return null; const isExpanded = expandedItems.has(group.item_code); - const groupIds = group.rows.map(r => r.id); - const allChecked = groupIds.every(id => checkedIds.includes(id)); + const groupIds = group.rows.map((r: any) => r.id); + const allChecked = groupIds.every((id: string) => checkedIds.includes(id)); + const renderCell = (key: string) => { + switch (key) { + case "item_code": return {group.item_code}; + case "item_name": return {group.item_name}; + case "inspection_type": return ( + +
+ {group.types.map((t: string) => {t})} +
+
+ ); + case "item_count": return {group.rows.filter((r: any) => r.inspection_standard_id).length}; + case "is_active": return ( + + + {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} + + + ); + default: return {(group as any)[key] ?? ""}; + } + }; return ( { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !groupIds.includes(id)) : [...new Set([...prev, ...groupIds])]); }}> {}} /> - {group.item_code} - {group.item_name} - -
- {group.types.map(t => {t})} -
-
- {group.rows.filter(r => r.inspection_standard_id).length} - - - {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} - - + {ts.visibleColumns.map((col) => renderCell(col.key))}
- {isExpanded && group.rows.filter(r => r.inspection_standard_id).map((row, i) => ( + {isExpanded && group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( diff --git a/frontend/app/(main)/COMPANY_10/sales/customer/page.tsx b/frontend/app/(main)/COMPANY_10/sales/customer/page.tsx index bddc7730..20b98727 100644 --- a/frontend/app/(main)/COMPANY_10/sales/customer/page.tsx +++ b/frontend/app/(main)/COMPANY_10/sales/customer/page.tsx @@ -12,7 +12,7 @@ * - 납품처 등록 (delivery_destination) */ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -820,12 +820,14 @@ export default function CustomerManagementPage() { const allItems = res.data?.data?.data || res.data?.data?.rows || []; setItemTotalCount(allItems.length); const existingItemIds = new Set(priceItems.map((p: any) => p.item_id || p.item_number)); - const SALES_CODES = ["CAT_ML8ZFVEL_1TOR"]; // 영업관리 카테고리 코드 - setItemSearchResults(allItems.filter((item: any) => { + const seenNumbers = new Set(); + const deduped = allItems.filter((item: any) => { if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false; - const divCodes = (item.division || "").split(",").map((c: string) => c.trim()); - return divCodes.some((code: string) => SALES_CODES.includes(code)); - })); + if (item.item_number && seenNumbers.has(item.item_number)) return false; + if (item.item_number) seenNumbers.add(item.item_number); + return true; + }); + setItemSearchResults(deduped); } catch { /* skip */ } finally { setItemSearchLoading(false); } }; @@ -1229,47 +1231,44 @@ export default function CustomerManagementPage() { } }; - // 컬럼 가시성 헬퍼 - const isColumnVisible = (key: string) => ts.isVisible(key); - - const customerColSpan = 1 + ["customer_code", "customer_name", "contact_person", "contact_phone", "division", "status"] - .filter((k) => isColumnVisible(k)).length; - - // EDataTable 컬럼 정의 (거래처 목록) - const customerColumns: EDataTableColumn[] = [ - ...(isColumnVisible("customer_code") ? [{ key: "customer_code", label: "거래처코드", width: "w-[120px]" }] : []), - ...(isColumnVisible("customer_name") ? [{ key: "customer_name", label: "거래처명", minWidth: "min-w-[140px]" }] : []), - ...(isColumnVisible("division") ? [{ - key: "division", - label: "거래유형", - width: "w-[80px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ...(isColumnVisible("contact_person") ? [{ key: "contact_person", label: "담당자", width: "w-[80px]" }] : []), - ...(isColumnVisible("contact_phone") ? [{ key: "contact_phone", label: "전화번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("email") ? [{ key: "email", label: "이메일", width: "w-[160px]" }] : []), - ...(isColumnVisible("business_number") ? [{ key: "business_number", label: "사업자번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("address") ? [{ key: "address", label: "주소", minWidth: "min-w-[150px]" }] : []), - ...(isColumnVisible("status") ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ]; + // EDataTable 컬럼 정의 (거래처 목록) — ts.visibleColumns 순서를 따름 + const customerColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + customer_code: { width: "w-[120px]" }, + customer_name: { minWidth: "min-w-[140px]" }, + division: { + width: "w-[80px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + contact_person: { width: "w-[80px]" }, + contact_phone: { width: "w-[120px]" }, + email: { width: "w-[160px]" }, + business_number: { width: "w-[120px]" }, + address: { minWidth: "min-w-[150px]" }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 엑셀 다운로드 const handleExcelDownload = async () => { diff --git a/frontend/app/(main)/COMPANY_10/sales/order/page.tsx b/frontend/app/(main)/COMPANY_10/sales/order/page.tsx index f896bbfd..f9a358d4 100644 --- a/frontend/app/(main)/COMPANY_10/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_10/sales/order/page.tsx @@ -13,7 +13,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, ClipboardList, Pencil, Search, X, Truck, Package, - ChevronLeft, ChevronRight, ChevronDown, ChevronsLeft, ChevronsRight, + ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, RotateCcw, Filter, Check, ArrowUp, ArrowDown, } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; @@ -42,41 +42,30 @@ const formatNumber = (val: string) => { }; const parseNumber = (val: string) => val.replace(/,/g, ""); -// 마스터 헤더 레이아웃 (수주번호 뒤, 디테일 11컬럼 위에 colSpan으로 맵핑) -// 순서: 거래처 | 단가방식 | 납품처 | 납품장소 | 수주일 | 담당자 → 합계 colSpan = 11 -const MASTER_BODY_LAYOUT = [ - { key: "partner_id", label: "거래처", colSpan: 2 }, - { key: "price_mode", label: "단가방식", colSpan: 1 }, - { key: "delivery_partner_id", label: "납품처", colSpan: 2 }, - { key: "delivery_address", label: "납품장소", colSpan: 2 }, - { key: "order_date", label: "수주일", colSpan: 2 }, - { key: "manager_id", label: "담당자", colSpan: 2 }, +// 플랫 테이블 컬럼 정의 (마스터+디테일 통합) +const FLAT_COLUMNS = [ + { key: "order_no", label: "수주번호", source: "master" }, + { key: "partner_id", label: "거래처", source: "master" }, + { key: "order_date", label: "수주일", source: "master" }, + { key: "part_code", label: "품번", source: "detail" }, + { key: "part_name", label: "품명", source: "detail" }, + { key: "spec", label: "규격", source: "detail" }, + { key: "unit", label: "단위", source: "detail" }, + { key: "qty", label: "수량", source: "detail" }, + { key: "ship_qty", label: "출하수량", source: "detail" }, + { key: "balance_qty", label: "잔량", source: "detail" }, + { key: "unit_price", label: "단가", source: "detail" }, + { key: "amount", label: "금액", source: "detail" }, + { key: "due_date", label: "납기일", source: "detail" }, + { key: "memo", label: "메모", source: "master" }, ]; -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "part_code", label: "품번" }, - { key: "part_name", label: "품명" }, - { key: "spec", label: "규격" }, - { key: "unit", label: "단위" }, - { key: "qty", label: "수량" }, - { key: "ship_qty", label: "출하수량" }, - { key: "balance_qty", label: "잔량" }, - { key: "unit_price", label: "단가" }, - { key: "amount", label: "금액" }, - { key: "currency_code", label: "통화" }, - { key: "due_date", label: "납기일" }, -]; +const DETAIL_HEADER_COLS = FLAT_COLUMNS.filter((c) => c.source === "detail"); // 필터용 전체 키 -const GRID_COLUMNS_CONFIG = [ - { key: "order_no", label: "수주번호" }, - ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label })), - ...DETAIL_HEADER_COLS, - { key: "memo", label: "메모" }, -]; +const GRID_COLUMNS_CONFIG = FLAT_COLUMNS.map(({ key, label }) => ({ key, label })); -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 수주번호(1) + 디테일(11) + 메모(1) = 15 +// 총 컬럼 수: 체크박스(1) + 플랫 컬럼(14) = 15 const TOTAL_COLS = 15; // 헤더 필터 Popover @@ -180,8 +169,6 @@ export default function SalesOrderPage() { const [masterForm, setMasterForm] = useState>({}); const [detailRows, setDetailRows] = useState([]); const [allowPriceEdit, setAllowPriceEdit] = useState(true); - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); // 품목 선택 모달 const [itemSelectOpen, setItemSelectOpen] = useState(false); @@ -376,25 +363,8 @@ export default function SalesOrderPage() { useEffect(() => { fetchOrders(); }, [fetchOrders]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) - const columnUniqueValues = useMemo(() => { - const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { - const values = new Set(); - orders.forEach((row) => { - const val = row[col.key]; - if (val !== null && val !== undefined && val !== "") values.add(String(val)); - }); - result[col.key] = Array.from(values).sort(); - } - return result; - }, [orders]); - - // 마스터 필드 키 목록 (필터 분류용) - const MASTER_KEYS = new Set(["order_no", ...MASTER_BODY_LAYOUT.map((c) => c.key), "memo"]); - - // 카테고리 코드→라벨 변환 (마스터 필터용) - const resolveMasterLabel = useCallback((key: string, code: string) => { + // 카테고리 코드→라벨 변환 + const resolveLabel = useCallback((key: string, code: string) => { if (!code) return ""; if (key === "partner_id" || key === "manager_id" || key === "price_mode") { return categoryOptions[key]?.find((o) => o.code === code)?.label || code; @@ -402,106 +372,60 @@ export default function SalesOrderPage() { return code; }, [categoryOptions]); - // 필터 + 정렬 적용된 데이터 → 그룹핑 - const filteredOrderGroups = useMemo(() => { - // 1차: order_no 기준 그룹핑 (필터 전) - const allGroups: Record = {}; - for (const row of orders) { - const key = row.order_no || "_no_order"; - if (!allGroups[key]) { - allGroups[key] = { master: row._master || {}, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위 필터링) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - const raw = group.master?.[colKey] ?? ""; - const label = resolveMasterLabel(colKey, String(raw)); - return values.has(label) || values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위 필터링) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([orderNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - const cellVal = row[colKey] != null ? String(row[colKey]) : ""; - return values.has(cellVal); - }) - ); - return [orderNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - // 마스터 필드 정렬 → 그룹 단위 - entries.sort(([, a], [, b]) => { - const av = a.master?.[key] ?? ""; - const bv = b.master?.[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - // 디테일 필드 정렬 → 각 그룹 내 디테일 정렬 - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - const av = a[key] ?? ""; - const bv = b[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [orders, headerFilters, sortState, resolveMasterLabel]); - - // 마스터 컬럼별 고유값 (마스터 헤더 필터용) - const masterUniqueValues = useMemo(() => { - const result: Record = {}; - // 필터 전 전체 마스터에서 고유값 추출 - const seenMasters = new Map(); - orders.forEach((row) => { - if (row.order_no && row._master && !seenMasters.has(row.order_no)) { - seenMasters.set(row.order_no, row._master); - } + // 플랫 행 생성 (마스터 필드를 각 디테일 행에 병합) + const flatRows = useMemo(() => { + return orders.map((row) => { + const master = row._master || {}; + return { + ...row, + partner_id: resolveLabel("partner_id", master.partner_id || row.partner_id || ""), + order_date: master.order_date || row.order_date || "", + memo: row.memo || master.memo || "", + }; }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "order_no", label: "수주번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label })), { key: "memo", label: "메모" }]) { + }, [orders, resolveLabel]); + + // 컬럼별 고유값 (헤더 필터용) + const columnUniqueValues = useMemo(() => { + const result: Record = {}; + for (const col of FLAT_COLUMNS) { const values = new Set(); - masters.forEach((m) => { - const val = m?.[col.key]; - if (val !== null && val !== undefined && val !== "") { - values.add(resolveMasterLabel(col.key, String(val))); - } + flatRows.forEach((row) => { + const val = row[col.key]; + if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [orders, resolveMasterLabel]); + }, [flatRows]); + + // 필터 + 정렬 적용된 플랫 데이터 + const filteredFlatRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = row[colKey] != null ? String(row[colKey]) : ""; + return values.has(cellVal); + }); + } + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = a[key] ?? ""; + const bv = b[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -965,111 +889,70 @@ export default function SalesOrderPage() {
- {/* 데이터 테이블 (트리 구조) */} + {/* 데이터 테이블 (플랫 리스트) */}
- {/* 체크박스 */} - {/* 펼침 화살표 */} - {/* 수주번호 */} - {/* 품번 / 거래처 */} - {/* 품명 / 거래처(cont) */} - {/* 규격 / 단가방식 */} - {/* 단위 / 납품처 */} - {/* 수량 / 납품처(cont) */} - {/* 출하수량 / 납품장소 */} - {/* 잔량 / 납품장소(cont) */} - {/* 단가 / 수주일 */} - {/* 금액 / 수주일(cont) */} - {/* 통화 / 담당자 */} - {/* 납기일 / 담당자(cont) */} - {/* 메모 */} + + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredOrderGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredFlatRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredOrderGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredFlatRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 수주번호 (별도 컬럼) */} - -
-
handleSort("order_no")}> - 수주번호 - {sortState?.key === "order_no" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["order_no"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (colSpan으로 디테일 컬럼 위에 맵핑) */} - {MASTER_BODY_LAYOUT.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {FLAT_COLUMNS.map((col) => { + const isRight = ["qty", "ship_qty", "balance_qty", "unit_price", "amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} - {/* 메모 (마스터) */} - -
-
handleSort("memo")}> - 메모 - {sortState?.key === "memo" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["memo"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
+ + ); + })} @@ -1079,7 +962,7 @@ export default function SalesOrderPage() { - ) : Object.keys(filteredOrderGroups).length === 0 ? ( + ) : filteredFlatRows.length === 0 ? (
@@ -1089,200 +972,48 @@ export default function SalesOrderPage() { ) : ( - Object.entries(filteredOrderGroups).map(([orderNo, group]) => { - const isExpanded = expandedOrders.has(orderNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredFlatRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 — 마스터 테이블 필드만 표시 */} - { - if (expandedOrders.has(orderNo)) { - setClosingOrders((prev) => new Set(prev).add(orderNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(orderNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(orderNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(orderNo)); - } - }} - onDoubleClick={() => openEditModal(orderNo)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 수주번호 */} - - {orderNo} - ({group.details.length}) - - {/* 거래처 (colSpan=2) */} - - - {master.partner_id ? (categoryOptions["partner_id"]?.find((o) => o.code === master.partner_id)?.label || master.partner_id) : ""} - - - {/* 단가방식 (colSpan=1) */} - - - {master.price_mode ? (categoryOptions["price_mode"]?.find((o) => o.code === master.price_mode)?.label || master.price_mode) : ""} - - - {/* 납품처 (colSpan=2) */} - - {master.delivery_partner_id || ""} - - {/* 납품장소 (colSpan=2) */} - - {master.delivery_address || ""} - - {/* 수주일 (colSpan=2) */} - - {master.order_date || ""} - - {/* 담당자 (colSpan=2) */} - - - {master.manager_id ? (categoryOptions["manager_id"]?.find((o) => o.code === master.manager_id)?.label || master.manager_id) : ""} - - - {/* 메모 */} - - {master.memo || ""} - - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - {/* 수주번호 컬럼 빈 셀 */} - {DETAIL_HEADER_COLS.map((col) => { - const isRight = ["qty", "ship_qty", "balance_qty", "unit_price", "amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => d[col.key]).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} - -
+ { - const isClosing = closingOrders.has(orderNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - onDoubleClick={() => openEditModal(row.order_no)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - {/* 수주번호 컬럼 빈 셀 */} - {row.part_code} - {row.part_name} - {row.spec} - {row.unit} - {row.qty ? Number(row.qty).toLocaleString() : ""} - {row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""} - {row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {row.amount ? Number(row.amount).toLocaleString() : ""} - {row.currency_code || ""} - {row.due_date || ""} - - + onClick={() => { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] ); - })} - + }} + onDoubleClick={() => openEditModal(row.order_no)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + > + {}} /> + + {row.order_no} + {row.partner_id || ""} + {row.order_date || ""} + {row.part_code} + {row.part_name} + {row.spec} + {row.unit} + {row.qty ? Number(row.qty).toLocaleString() : ""} + {row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""} + {row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.amount ? Number(row.amount).toLocaleString() : ""} + {row.due_date || ""} + {row.memo || ""} + ); }) )} diff --git a/frontend/app/(main)/COMPANY_10/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_10/sales/shipping-order/page.tsx index 4ab5a9ad..2ed29b40 100644 --- a/frontend/app/(main)/COMPANY_10/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_10/sales/shipping-order/page.tsx @@ -363,7 +363,7 @@ export default function ShippingOrderPage() { spec: item.spec, material: item.material, orderQty: item.orderQty, - planQty: item.planQty, + planQty: item.orderQty, shipQty: 0, sourceType: item.sourceType, shipmentPlanId: item.shipmentPlanId, diff --git a/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx index eeb63844..cd53e9b1 100644 --- a/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx @@ -142,15 +142,20 @@ export default function EquipmentInfoPage() { }; const mainTableColumns = useMemo(() => { - const cols: EDataTableColumn[] = []; - if (ts.isVisible("equipment_code")) cols.push({ key: "equipment_code", label: "설비코드", width: "w-[110px]" }); - if (ts.isVisible("equipment_name")) cols.push({ key: "equipment_name", label: "설비명", minWidth: "min-w-[130px]", truncate: true, render: (v) => v || "-" }); - if (ts.isVisible("equipment_type")) cols.push({ key: "equipment_type", label: "설비유형", width: "w-[90px]", render: (v) => v || "-" }); - if (ts.isVisible("manufacturer")) cols.push({ key: "manufacturer", label: "제조사", width: "w-[100px]", render: (v) => v || "-" }); - if (ts.isVisible("installation_location")) cols.push({ key: "installation_location", label: "설치장소", width: "w-[100px]", render: (v) => v || "-" }); - if (ts.isVisible("operation_status")) cols.push({ key: "operation_status", label: "가동상태", width: "w-[80px]", render: (v) => v || "-" }); - return cols; - }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + 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 || "-" }, + manufacturer: { width: "w-[100px]", render: (v) => v || "-" }, + installation_location: { width: "w-[100px]", render: (v) => v || "-" }, + operation_status: { width: "w-[80px]", render: (v) => v || "-" }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 설비 조회 const fetchEquipments = useCallback(async () => { @@ -272,8 +277,8 @@ export default function EquipmentInfoPage() { if (!inspectionForm.inspection_cycle) { toast.error("점검주기는 필수입니다."); return; } if (!inspectionForm.inspection_method) { toast.error("점검방법은 필수입니다."); return; } const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; - if (isNumeric && !inspectionForm.unit) { toast.error("숫자 점검방법은 측정단위가 필수입니다."); return; } + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); + if (isNumeric && !inspectionForm.unit) { toast.error("측정단위가 필수입니다."); return; } // 기준값/오차범위 → 하한치/상한치 자동 계산 const saveData = { ...inspectionForm }; if (isNumeric && saveData.standard_value) { @@ -739,7 +744,7 @@ export default function EquipmentInfoPage() {
{catSelect("inspection_method", inspectionForm.inspection_method, (v) => { const label = resolve("inspection_method", v); - const isNum = label === "숫자" || v === "숫자"; + const isNum = ["숫자", "치수검사"].includes(label) || ["숫자", "치수검사"].includes(v); if (!isNum) { setInspectionForm((p) => ({ ...p, inspection_method: v, unit: "", standard_value: "", tolerance: "", lower_limit: "", upper_limit: "" })); } else { @@ -748,7 +753,7 @@ export default function EquipmentInfoPage() { }, "점검방법")}
{(() => { const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); if (!isNumeric) return null; return (
@@ -758,7 +763,7 @@ export default function EquipmentInfoPage() {
{(() => { const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); if (!isNumeric) return null; return (
diff --git a/frontend/app/(main)/COMPANY_16/logistics/material-status/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/material-status/page.tsx index 46de306c..eb87ba92 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/material-status/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/material-status/page.tsx @@ -333,69 +333,90 @@ export default function MaterialStatusPage() {

) : ( - workOrders.map((wo) => ( -
handleSelectWo(wo.id)} - > + ts.groupData(workOrders).map((wo) => { + if ((wo as any)._isGroupSummary || (wo as any)._isGroupHeader) return null; + return (
e.stopPropagation()} + key={wo.id} + className={cn( + "flex gap-3 rounded-lg border p-3 transition-all cursor-pointer", + "hover:border-primary/50 hover:shadow-sm", + selectedWoId === wo.id + ? "border-primary bg-primary/5 shadow-sm" + : "border-border" + )} + onClick={() => handleSelectWo(wo.id)} > - - handleCheckWo(wo.id, c as boolean) - } - /> -
-
-
- - {wo.plan_no || wo.work_order_no || `WO-${wo.id}`} - - e.stopPropagation()} + > + + handleCheckWo(wo.id, c as boolean) + } + /> +
+
+
+ {ts.isVisible("plan_no") && ( + + {wo.plan_no || wo.work_order_no || `WO-${wo.id}`} + )} - > - {getStatusLabel(wo.status)} - -
-
- - {wo.item_name} - - - ({wo.item_code}) - -
-
- 수량: - - {Number(wo.plan_qty).toLocaleString()}개 - - | - 일자: - - {wo.plan_date - ? new Date(wo.plan_date) - .toISOString() - .slice(0, 10) - : "-"} - + {ts.isVisible("status") && ( + + {getStatusLabel(wo.status)} + + )} +
+
+ {ts.isVisible("item_name") && ( + + {wo.item_name} + + )} + {ts.isVisible("item_code") && ( + + ({wo.item_code}) + + )} +
+
+ {ts.isVisible("plan_qty") && ( + <> + 수량: + + {Number(wo.plan_qty).toLocaleString()}개 + + + )} + {ts.isVisible("plan_qty") && ts.isVisible("plan_date") && ( + | + )} + {ts.isVisible("plan_date") && ( + <> + 일자: + + {wo.plan_date + ? new Date(wo.plan_date) + .toISOString() + .slice(0, 10) + : "-"} + + + )} +
-
- )) + ); + }) )}
diff --git a/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx index 5ab46a8a..c1ffbd40 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx @@ -140,8 +140,16 @@ const DETAIL_HEADER_COLS = [ // 마스터 필드 키 목록 (필터 분류용) const MASTER_KEYS = new Set(["outbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 출고번호(1) + 마스터필드(7) = 10 -const TOTAL_COLS = 10; +// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) +const DETAIL_KEY_MAP: Record = { + source_type: "source_type", + item_number: "item_code", + item_name: "item_name", + spec: "specification", + outbound_qty: "outbound_qty", + unit_price: "unit_price", + total_amount: "total_amount", +}; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -248,6 +256,31 @@ interface SelectedSourceItem { export default function OutboundPage() { const ts = useTableSettings("c16-outbound", "outbound_mng", GRID_COLUMNS); + + // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 + const visibleMasterLayout = useMemo(() => { + const ordered: typeof MASTER_BODY_LAYOUT = []; + for (const vc of ts.visibleColumns) { + const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); + if (m) ordered.push(m); + } + return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; + }, [ts.visibleColumns]); + + const visibleDetailCols = useMemo(() => { + const ordered: typeof DETAIL_HEADER_COLS = []; + for (const vc of ts.visibleColumns) { + const detailKey = DETAIL_KEY_MAP[vc.key]; + if (detailKey) { + const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); + if (d) ordered.push(d); + } + } + return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; + }, [ts.visibleColumns]); + + const TOTAL_COLS = 3 + visibleMasterLayout.length; + // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -900,8 +933,15 @@ export default function OutboundPage() {
-
- +
+ + + + + {visibleMasterLayout.map((col) => ( + + ))} + - {/* 마스터 필드 헤더 */} - {MASTER_BODY_LAYOUT.map((col) => ( + {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => (
handleSort(col.key)}> @@ -1039,38 +1079,51 @@ export default function OutboundPage() { {outboundNo} ({group.details.length}) - {/* 출고유형 */} - - - {master.outbound_type || "-"} - - - {/* 출고일 */} - - {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} - - {/* 참조번호 */} - - {master.reference_number || ""} - - {/* 거래처 */} - - {master.customer_name || ""} - - {/* 창고 */} - - {master.warehouse_name || master.warehouse_code || ""} - - {/* 출고상태 */} - - - {master.outbound_status || "-"} - - - {/* 비고 */} - - {master.memo || ""} - + {/* 마스터 필드 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => { + switch (col.key) { + case "outbound_type": return ( + + + {master.outbound_type || "-"} + + + ); + case "outbound_date": return ( + + {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} + + ); + case "reference_number": return ( + + {master.reference_number || ""} + + ); + case "customer_name": return ( + + {master.customer_name || ""} + + ); + case "warehouse_name": return ( + + {master.warehouse_name || master.warehouse_code || ""} + + ); + case "outbound_status": return ( + + + {master.outbound_status || "-"} + + + ); + case "memo": return ( + + {master.memo || ""} + + ); + default: return {(master as any)[col.key] ?? ""}; + } + })} {/* 디테일 서브 헤더 (펼쳤을 때만) */} @@ -1084,7 +1137,7 @@ export default function OutboundPage() { - {DETAIL_HEADER_COLS.map((col) => { + {visibleDetailCols.map((col) => { const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); const isSorted = sortState?.key === col.key; const uniqueVals = Array.from(new Set( @@ -1163,20 +1216,18 @@ export default function OutboundPage() {
- {/* 출처 */} - {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"} - {/* 품목코드 */} - {row.item_code || ""} - {/* 품목명 */} - {row.item_name || ""} - {/* 규격 */} - {row.specification || ""} - {/* 출고수량 */} - {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""} - {/* 단가 */} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {/* 금액 */} - {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {visibleDetailCols.map((col) => { + switch (col.key) { + case "source_type": return {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"}; + case "item_code": return {row.item_code || ""}; + case "item_name": return {row.item_name || ""}; + case "specification": return {row.specification || ""}; + case "outbound_qty": return {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""}; + case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; + case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; + default: return {(row as any)[col.key] ?? ""}; + } + })} ); })} diff --git a/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx index 5d4d5787..6ae340aa 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx @@ -460,18 +460,20 @@ export default function PackagingPage() { {/* 포장재 목록 테이블 */}
PKG_TYPE_LABEL[v] || v || "-" }, - { key: "size", label: "크기(mm)", width: "w-[100px]", render: (_v, row) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, - { key: "max_load_kg", label: "최대중량", width: "w-[80px]", align: "right", render: (v) => Number(v || 0) > 0 ? `${v}kg` : "-" }, - { key: "status", label: "상태", width: "w-[60px]", align: "center", render: (v) => ( - - {STATUS_LABEL[v] || v} - - )}, - ] as EDataTableColumn[]} + columns={ts.visibleColumns.map((col): EDataTableColumn => { + const renderMap: Record>> = { + pkg_type: { width: "w-[80px]", render: (v: any) => PKG_TYPE_LABEL[v] || v || "-" }, + size: { width: "w-[100px]", render: (_v: any, row: any) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, + max_weight: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, + max_load_kg: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, + status: { width: "w-[60px]", align: "center", render: (v: any) => ( + + {STATUS_LABEL[v] || v} + + )}, + }; + return { key: col.key, label: col.label, ...renderMap[col.key] }; + })} data={ts.groupData(filteredPkgUnits)} rowKey={(row) => String(row.id)} loading={pkgLoading} diff --git a/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx index a8d5fc2c..85cdc23c 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx @@ -117,12 +117,20 @@ const DETAIL_HEADER_COLS = [ { key: "total_amount", label: "금액" }, ]; -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 입고번호(1) + 디테일(7) = 10 -const TOTAL_COLS = 10; - // 마스터 필드 키 목록 (필터 분류용) const MASTER_KEYS = new Set(["inbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); +// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) +const DETAIL_KEY_MAP: Record = { + source_type: "source_table", + item_number: "item_number", + item_name: "item_name", + spec: "spec", + inbound_qty: "inbound_qty", + unit_price: "unit_price", + total_amount: "total_amount", +}; + // 헤더 필터 Popover function HeaderFilterPopover({ colKey, colLabel, uniqueValues, filterValues, onToggle, onClear, @@ -278,6 +286,31 @@ interface SelectedSourceItem { export default function ReceivingPage() { const ts = useTableSettings("c16-receiving", "inbound_mng", GRID_COLUMNS); + + // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 + const visibleMasterLayout = useMemo(() => { + const ordered: typeof MASTER_BODY_LAYOUT = []; + for (const vc of ts.visibleColumns) { + const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); + if (m) ordered.push(m); + } + return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; + }, [ts.visibleColumns]); + + const visibleDetailCols = useMemo(() => { + const ordered: typeof DETAIL_HEADER_COLS = []; + for (const vc of ts.visibleColumns) { + const detailKey = DETAIL_KEY_MAP[vc.key]; + if (detailKey) { + const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); + if (d) ordered.push(d); + } + } + return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; + }, [ts.visibleColumns]); + + const TOTAL_COLS = 3 + visibleMasterLayout.length; + // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -847,8 +880,15 @@ export default function ReceivingPage() {
-
- +
+ + + + + {visibleMasterLayout.map((col) => ( + + ))} + - {/* 마스터 필드 헤더 (colSpan으로 디테일 컬럼 위에 맵핑) */} - {MASTER_BODY_LAYOUT.map((col) => ( + {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => (
handleSort(col.key)}> @@ -985,38 +1025,51 @@ export default function ReceivingPage() { {inboundNo} ({group.details.length}) - {/* 입고유형 */} - - - {resolveInboundType(master.inbound_type)} - - - {/* 입고일 */} - - {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} - - {/* 참조번호 */} - - {master.reference_number || ""} - - {/* 공급처 */} - - {master.supplier_name || ""} - - {/* 창고 */} - - {master.warehouse_name || master.warehouse_code || ""} - - {/* 입고상태 */} - - - {master.inbound_status || "-"} - - - {/* 비고 */} - - {master.memo || ""} - + {/* 마스터 필드 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => { + switch (col.key) { + case "inbound_type": return ( + + + {resolveInboundType(master.inbound_type)} + + + ); + case "inbound_date": return ( + + {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} + + ); + case "reference_number": return ( + + {master.reference_number || ""} + + ); + case "supplier_name": return ( + + {master.supplier_name || ""} + + ); + case "warehouse_name": return ( + + {master.warehouse_name || master.warehouse_code || ""} + + ); + case "inbound_status": return ( + + + {master.inbound_status || "-"} + + + ); + case "memo": return ( + + {master.memo || ""} + + ); + default: return {(master as any)[col.key] ?? ""}; + } + })} {/* 디테일 서브 헤더 (펼쳤을 때만) */} @@ -1030,7 +1083,7 @@ export default function ReceivingPage() { - {DETAIL_HEADER_COLS.map((col) => { + {visibleDetailCols.map((col) => { const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); const isSorted = sortState?.key === col.key; const uniqueVals = Array.from(new Set( @@ -1108,20 +1161,18 @@ export default function ReceivingPage() {
- {/* 출처 */} - {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"} - {/* 품목코드 */} - {row.item_number || ""} - {/* 품목명 */} - {row.item_name || ""} - {/* 규격 */} - {row.spec || ""} - {/* 입고수량 */} - {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""} - {/* 단가 */} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {/* 금액 */} - {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {visibleDetailCols.map((col) => { + switch (col.key) { + case "source_table": return {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"}; + case "item_number": return {row.item_number || ""}; + case "item_name": return {row.item_name || ""}; + case "spec": return {row.spec || ""}; + case "inbound_qty": return {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""}; + case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; + case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; + default: return {(row as any)[col.key] ?? ""}; + } + })} ); })} 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 dfd1b666..9d7f2dea 100644 --- a/frontend/app/(main)/COMPANY_16/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_16/master-data/company/page.tsx @@ -491,12 +491,6 @@ export default function CompanyPage() { > 회사정보 - - 부서관리 -
@@ -635,89 +629,6 @@ export default function CompanyPage() {
- {/* ===================== Tab 2: 부서관리 ===================== */} - -
- - {/* 좌측: 부서 트리 */} - -
-
-
- - 부서 - {depts.length}건 -
-
- - - -
-
-
- {deptLoading ? ( -
- -
- ) : deptTree.length === 0 ? ( -
- - 등록된 부서가 없어요 -
- ) : ( - renderTree(deptTree) - )} -
-
-
- - - - {/* 우측: 사원 목록 */} - -
-
-
- - {selectedDept ? "부서 인원" : "부서를 선택해주세요"} - {selectedDept && {selectedDept.dept_name}} - {members.length > 0 && {members.length}명} -
- {selectedDeptCode && ( - - )} -
- {selectedDeptCode ? ( - row.user_id || row.id} - loading={memberLoading} - emptyMessage="소속 사원이 없어요" - emptyIcon={} - onRowDoubleClick={(row) => openUserModal(row)} - showPagination={false} - draggableColumns={false} - /> - ) : ( -
- - 좌측에서 부서를 선택해주세요 -
- )} -
-
-
-
-
{/* ── 부서 등록/수정 모달 ── */} diff --git a/frontend/app/(main)/COMPANY_16/master-data/department/page.tsx b/frontend/app/(main)/COMPANY_16/master-data/department/page.tsx index a2bbcba5..3245571e 100644 --- a/frontend/app/(main)/COMPANY_16/master-data/department/page.tsx +++ b/frontend/app/(main)/COMPANY_16/master-data/department/page.tsx @@ -9,7 +9,7 @@ * 모달: 부서 등록(dept_info), 사원 추가(user_info) */ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -279,6 +279,7 @@ export default function DepartmentPage() { dept_code: userForm.dept_code || undefined, dept_name: depts.find((d) => d.dept_code === userForm.dept_code)?.dept_name || undefined, status: userForm.status || "active", + end_date: userForm.end_date || null, }, mainDept: userForm.dept_code ? { dept_code: userForm.dept_code, @@ -312,37 +313,40 @@ export default function DepartmentPage() { const activeMembers = members.filter((m) => !m.end_date || m.end_date.substring(0, 10) >= today); const resignedMembers = members.filter((m) => m.end_date && m.end_date.substring(0, 10) < today); - const isColVisible = (key: string) => ts.isVisible(key); - - // EDataTable 컬럼 정의 (부서 목록) - const deptColumns: EDataTableColumn[] = [ - { key: "dept_code", label: "부서코드", width: "w-[120px]" }, - { key: "dept_name", label: "부서명", minWidth: "min-w-[140px]" }, - ...(isColVisible("parent_dept_code") - ? [{ - key: "parent_dept_code", - label: "상위부서", - width: "w-[110px]", - render: (val: any) => {val || "\u2014"}, - }] - : []), - ...(isColVisible("status") - ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val === "active" ? "활성" : (val || "\u2014")} - - ) : null, - }] - : []), - ]; + // EDataTable 컬럼 정의 (부서 목록) — ts.visibleColumns 순서를 따름 + const deptColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + dept_code: { width: "w-[120px]" }, + dept_name: { minWidth: "min-w-[140px]" }, + parent_dept_code: { + width: "w-[110px]", + render: (val: any) => {val || "\u2014"}, + }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val === "active" ? "활성" : (val || "\u2014")} + + ) : null, + }, + }; + // dept_code, dept_name은 항상 표시 (DEPT_COLUMNS에 포함되지 않으므로 visibleColumns에 없음) + const fixedCols: EDataTableColumn[] = [ + { key: "dept_code", label: "부서코드", ...colProps["dept_code"] }, + { key: "dept_name", label: "부서명", ...colProps["dept_name"] }, + ]; + const dynamicCols = ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + return [...fixedCols, ...dynamicCols]; + }, [ts.visibleColumns]); return (
diff --git a/frontend/app/(main)/COMPANY_16/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_16/master-data/item-info/page.tsx index 3f037275..375fd900 100644 --- a/frontend/app/(main)/COMPANY_16/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/master-data/item-info/page.tsx @@ -84,6 +84,56 @@ function CategoryCombobox({ options, value, onChange, placeholder }: { ); } +// 다중 선택 카테고리 콤보박스 +function MultiCategoryCombobox({ options, value, onChange, placeholder }: { + options: { code: string; label: string }[]; + value: string; + onChange: (v: string) => void; + placeholder: string; +}) { + const [open, setOpen] = useState(false); + const selectedCodes = value ? value.split(",").map((c) => c.trim()).filter(Boolean) : []; + const selectedLabels = selectedCodes.map((code) => options.find((o) => o.code === code)?.label || code).filter(Boolean); + + const toggle = (code: string) => { + const next = selectedCodes.includes(code) + ? selectedCodes.filter((c) => c !== code) + : [...selectedCodes, code]; + onChange(next.join(",")); + }; + + return ( + + + + + + + + + 검색 결과가 없어요 + + {options.map((opt) => ( + toggle(opt.code)}> + + {opt.label} + + ))} + + + + + + ); +} + const TABLE_NAME = "item_info"; const GRID_COLUMNS = [ @@ -108,7 +158,7 @@ const GRID_COLUMNS = [ const FORM_FIELDS = [ { key: "item_number", label: "품목코드", type: "text", required: true, disabled: true, placeholder: "자동 채번" }, { key: "item_name", label: "품명", type: "text", required: true }, - { key: "division", label: "관리품목", type: "category" }, + { key: "division", label: "관리품목", type: "multi-category" }, { key: "type", label: "품목구분", type: "category" }, { key: "size", label: "규격", type: "text" }, { key: "unit", label: "단위", type: "category" }, @@ -137,6 +187,7 @@ export default function ItemInfoPage() { const { user } = useAuth(); const ts = useTableSettings("c16-item-info", TABLE_NAME, GRID_COLUMNS); const [items, setItems] = useState([]); + const [rawItems, setRawItems] = useState([]); const [loading, setLoading] = useState(false); // 검색 필터 (DynamicSearchFilter) @@ -215,6 +266,7 @@ export default function ItemInfoPage() { } return categoryOptions[col]?.find((o) => o.code === code)?.label || code; }; + setRawItems(raw); const data = raw.map((r: any) => { const converted = { ...r }; for (const col of CATEGORY_COLUMNS) { @@ -261,7 +313,8 @@ export default function ItemInfoPage() { // 수정 모달 열기 const openEditModal = (item: any) => { - setFormData({ ...item }); + const raw = rawItems.find((r) => r.id === item.id) || item; + setFormData({ ...raw }); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); @@ -269,7 +322,8 @@ export default function ItemInfoPage() { // 복사 모달 열기 const openCopyModal = async (item: any) => { - const { id, item_number, created_date, updated_date, writer, ...rest } = item; + const raw = rawItems.find((r) => r.id === item.id) || item; + const { id, item_number, created_date, updated_date, writer, ...rest } = raw; setFormData(rest); setIsEditMode(false); setEditId(null); @@ -459,6 +513,13 @@ export default function ItemInfoPage() { columnName={field.key} height="h-32" /> + ) : field.type === "multi-category" ? ( + setFormData((prev) => ({ ...prev, [field.key]: v }))} + placeholder={`${field.label} 선택`} + /> ) : field.type === "category" ? ( (() => { - const cols: EDataTableColumn[] = []; - if (ts.isVisible("item_number")) cols.push({ key: "item_number", label: "품번", width: "w-[110px]" }); - if (ts.isVisible("item_name")) cols.push({ key: "item_name", label: "품명", minWidth: "min-w-[130px]", render: (v) => v || "-" }); - if (ts.isVisible("size")) cols.push({ key: "size", label: "규격", width: "w-[90px]", render: (v) => v || "-" }); - if (ts.isVisible("unit")) cols.push({ key: "unit", label: "단위", width: "w-[60px]", render: (v) => v || "-" }); - if (ts.isVisible("standard_price")) cols.push({ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }); - if (ts.isVisible("selling_price")) cols.push({ key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true }); - if (ts.isVisible("currency_code")) cols.push({ key: "currency_code", label: "통화", width: "w-[50px]", render: (v) => v || "-" }); - if (ts.isVisible("status")) cols.push({ key: "status", label: "상태", width: "w-[60px]", render: (v) => v || "-" }); - return cols; - }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + const colProps: Record> = { + item_number: { width: "w-[110px]" }, + item_name: { minWidth: "min-w-[130px]", render: (v) => v || "-" }, + size: { width: "w-[90px]", render: (v) => v || "-" }, + unit: { width: "w-[60px]", render: (v) => v || "-" }, + standard_price: { width: "w-[90px]", align: "right", formatNumber: true }, + selling_price: { width: "w-[90px]", align: "right", formatNumber: true }, + currency_code: { width: "w-[50px]", render: (v) => v || "-" }, + status: { width: "w-[60px]", render: (v) => v || "-" }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 좌측: 품목 조회 (division이 "외주관리"인 품목만 필터링) const outsourcingDivisionCode = categoryOptions["division"]?.find( diff --git a/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx index 4a0d341a..6bd63176 100644 --- a/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx @@ -919,9 +919,7 @@ export default function ProductionPlanManagementPage() { // 숫자 포맷 const formatNumber = (num: number | string) => Number(num).toLocaleString(); - // 컬럼 표시 여부 - const isColVisible = (key: string) => ts.isVisible(key); - const orderColSpan = 4 + ORDER_COLUMNS.filter((c) => isColVisible(c.key)).length; + // (컬럼 표시는 ts.visibleColumns 순서를 따름) return (
@@ -1019,6 +1017,38 @@ export default function ProductionPlanManagementPage() {
) : (
+ {(() => { + // 디테일 행에서 개별 값을 표시하는 컬럼 매핑 + const DETAIL_VALUE_MAP: Record = { + total_order_qty: "order_qty", + total_ship_qty: "ship_qty", + total_balance_qty: "balance_qty", + }; + + // 그룹 행에서 특수 렌더링이 필요한 컬럼 + const renderGroupCell = (col: { key: string }, item: any) => { + if (col.key === "required_plan_qty") { + return ( + 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> + {formatNumber(item.required_plan_qty)} + + ); + } + if (col.key === "lead_time") { + return ( + toggleItemExpand(item.item_code)}> + {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"} + + ); + } + return ( + toggleItemExpand(item.item_code)}> + {formatNumber(item[col.key])} + + ); + }; + + return (
@@ -1028,15 +1058,11 @@ export default function ProductionPlanManagementPage() { 품목코드 품목명 - {isColVisible("total_order_qty") && 총수주량} - {isColVisible("total_ship_qty") && 출고량} - {isColVisible("total_balance_qty") && 잔량} - {isColVisible("current_stock") && 현재고} - {isColVisible("safety_stock") && 안전재고} - {isColVisible("existing_plan_qty") && 기생산계획량} - {isColVisible("in_progress_qty") && 생산진행} - {isColVisible("required_plan_qty") && 필요생산계획} - {isColVisible("lead_time") && 리드타임(일)} + {ts.visibleColumns.map((col) => ( + + {col.label} + + ))} @@ -1046,6 +1072,7 @@ export default function ProductionPlanManagementPage() { + {ts.visibleColumns.map((col) => { const v = (item as any)[col.key]; return ( @@ -1068,25 +1095,14 @@ export default function ProductionPlanManagementPage() { toggleItemExpand(item.item_code)}>{item.item_code} toggleItemExpand(item.item_code)}>{item.item_name} - {isColVisible("total_order_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_order_qty)}} - {isColVisible("total_ship_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_ship_qty)}} - {isColVisible("total_balance_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_balance_qty)}} - {isColVisible("current_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.current_stock)}} - {isColVisible("safety_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.safety_stock)}} - {isColVisible("existing_plan_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.existing_plan_qty)}} - {isColVisible("in_progress_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.in_progress_qty)}} - {isColVisible("required_plan_qty") && ( - 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> - {formatNumber(item.required_plan_qty)} - - )} - {isColVisible("lead_time") && ( - toggleItemExpand(item.item_code)}> - {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"} - - )} + {ts.visibleColumns.map((col) => renderGroupCell(col, item))} - {expandedItems.has(item.item_code) && item.orders?.map((detail) => ( + {expandedItems.has(item.item_code) && item.orders?.map((detail: any) => { + let remainColSpan = 0; + for (const col of ts.visibleColumns) { + if (!DETAIL_VALUE_MAP[col.key]) remainColSpan++; + } + return ( @@ -1101,19 +1117,28 @@ export default function ProductionPlanManagementPage() { - {isColVisible("total_order_qty") && {formatNumber(detail.order_qty)}} - {isColVisible("total_ship_qty") && {formatNumber(detail.ship_qty)}} - {isColVisible("total_balance_qty") && {formatNumber(detail.balance_qty)}} - - 납기일: {detail.due_date || "-"} - + {ts.visibleColumns.map((col) => { + const detailKey = DETAIL_VALUE_MAP[col.key]; + if (detailKey) { + return {formatNumber(detail[detailKey])}; + } + return null; + })} + {remainColSpan > 0 && ( + + 납기일: {detail.due_date || "-"} + + )} - ))} + ); + })} ); })}
+ ); + })()} )} diff --git a/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx index fa0e08c5..1bc3bc88 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx @@ -742,10 +742,24 @@ export default function PurchaseOrderPage() { ) : ( (() => { const MASTER_KEYS = new Set(["purchase_no", "order_date", "supplier_name", "status", "memo"]); - const detailCols = ts.visibleColumns.filter(c => !MASTER_KEYS.has(c.key)); - const masterCols = ts.visibleColumns.filter(c => MASTER_KEYS.has(c.key)); const numCols = new Set(["order_qty", "received_qty", "remain_qty", "unit_price", "amount"]); + // ts.visibleColumns 순서를 따르되, 마스터/디테일 컬럼을 분리 + // 고정 컬럼(품목수)은 마스터 선행 컬럼 뒤에 배치 + const leadingMaster: typeof ts.visibleColumns = []; + const detailCols: typeof ts.visibleColumns = []; + const trailingMaster: typeof ts.visibleColumns = []; + let passedFirstDetail = false; + for (const col of ts.visibleColumns) { + if (MASTER_KEYS.has(col.key)) { + if (passedFirstDetail) trailingMaster.push(col); + else leadingMaster.push(col); + } else { + passedFirstDetail = true; + detailCols.push(col); + } + } + const renderDetailCell = (row: any, key: string) => { const val = row[key]; if (key === "status") return val ? {val} : "-"; @@ -753,23 +767,35 @@ export default function PurchaseOrderPage() { return val || "-"; }; + const renderMasterHead = (col: { key: string; label: string }) => ( + + {col.label} + + ); + + const renderMasterCell = (col: { key: string }, m: any, purchaseNo: string) => { + if (col.key === "purchase_no") return {purchaseNo}; + if (col.key === "order_date") return {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}; + if (col.key === "supplier_name") return {m.supplier_name || "-"}; + if (col.key === "status") return {m.status && {m.status}}; + if (col.key === "memo") return {m.memo || ""}; + return ; + }; + return ( - {ts.isVisible("purchase_no") && 발주번호} - {ts.isVisible("order_date") && 발주일} - {ts.isVisible("supplier_name") && 공급업체} + {leadingMaster.map(renderMasterHead)} 품목수 {detailCols.map(col => ( {col.label}{col.key === "order_qty" || col.key === "amount" ? " 합계" : ""} ))} - {ts.isVisible("status") && 상태} - {ts.isVisible("memo") && 메모} + {trailingMaster.map(renderMasterHead)} @@ -795,9 +821,7 @@ export default function PurchaseOrderPage() { { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !detailIds.includes(id)) : [...new Set([...prev, ...detailIds])]); }}> {}} /> - {ts.isVisible("purchase_no") && {purchaseNo}} - {ts.isVisible("order_date") && {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}} - {ts.isVisible("supplier_name") && {m.supplier_name || "-"}} + {leadingMaster.map(col => renderMasterCell(col, m, purchaseNo))} {group.details.length}건 {detailCols.map(col => ( @@ -806,8 +830,7 @@ export default function PurchaseOrderPage() { : ""} ))} - {ts.isVisible("status") && {m.status && {m.status}}} - {ts.isVisible("memo") && {m.memo || ""}} + {trailingMaster.map(col => renderMasterCell(col, m, purchaseNo))} {isExpanded && group.details.map((row) => ( @@ -815,17 +838,14 @@ export default function PurchaseOrderPage() { { e.stopPropagation(); setCheckedIds(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id]); }}> {}} /> - {ts.isVisible("purchase_no") && } - {ts.isVisible("order_date") && } - {ts.isVisible("supplier_name") && } + {leadingMaster.map(col => )} {detailCols.map(col => ( {renderDetailCell(row, col.key)} ))} - {ts.isVisible("status") && } - {ts.isVisible("memo") && } + {trailingMaster.map(col => )} ))} diff --git a/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx index 1f7c4137..7f211a88 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx @@ -617,17 +617,21 @@ export default function PurchaseItemPage() { toast.success("다운로드 완료"); }; - // EDataTable 컬럼 정의 (구매품목) - const itemColumns: EDataTableColumn[] = [ - { key: "item_number", label: "품번", width: "w-[110px]" }, - { key: "item_name", label: "품명", minWidth: "min-w-[130px]" }, - { key: "size", label: "규격", width: "w-[80px]" }, - { key: "unit", label: "단위", width: "w-[60px]" }, - { key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }, - { key: "standard_price", label: "구매단가", width: "w-[90px]", align: "right", formatNumber: true }, - { key: "currency_code", label: "통화", width: "w-[50px]" }, - { key: "status", label: "상태", width: "w-[60px]" }, - ]; + // EDataTable 컬럼 정의 (구매품목) — ts.visibleColumns 기반 + const COLUMN_RENDER_MAP: Record> = { + item_number: { width: "w-[110px]" }, + item_name: { minWidth: "min-w-[130px]" }, + size: { width: "w-[80px]" }, + unit: { width: "w-[60px]" }, + standard_price: { width: "w-[90px]", align: "right", formatNumber: true }, + currency_code: { width: "w-[50px]" }, + status: { width: "w-[60px]" }, + }; + const itemColumns: EDataTableColumn[] = ts.visibleColumns.map((col): EDataTableColumn => ({ + key: col.key, + label: col.label, + ...COLUMN_RENDER_MAP[col.key], + })); return (
diff --git a/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx index 51c50aa5..521f770e 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx @@ -12,7 +12,7 @@ * - 납품처 등록 (delivery_destination) */ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -1229,47 +1229,44 @@ export default function SupplierManagementPage() { } }; - // 컬럼 가시성 헬퍼 - const isColumnVisible = (key: string) => ts.isVisible(key); - - const supplierColSpan = 1 + ["supplier_code", "supplier_name", "contact_person", "contact_phone", "division", "status"] - .filter((k) => isColumnVisible(k)).length; - - // EDataTable 컬럼 정의 (공급업체 목록) - const supplierColumns: EDataTableColumn[] = [ - ...(isColumnVisible("supplier_code") ? [{ key: "supplier_code", label: "공급업체코드", width: "w-[120px]" }] : []), - ...(isColumnVisible("supplier_name") ? [{ key: "supplier_name", label: "공급업체명", minWidth: "min-w-[140px]" }] : []), - ...(isColumnVisible("division") ? [{ - key: "division", - label: "공급업체유형", - width: "w-[80px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ...(isColumnVisible("contact_person") ? [{ key: "contact_person", label: "담당자", width: "w-[80px]" }] : []), - ...(isColumnVisible("contact_phone") ? [{ key: "contact_phone", label: "전화번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("email") ? [{ key: "email", label: "이메일", width: "w-[160px]" }] : []), - ...(isColumnVisible("business_number") ? [{ key: "business_number", label: "사업자번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("address") ? [{ key: "address", label: "주소", minWidth: "min-w-[150px]" }] : []), - ...(isColumnVisible("status") ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ]; + // EDataTable 컬럼 정의 (공급업체 목록) — ts.visibleColumns 순서를 따름 + const supplierColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + supplier_code: { width: "w-[120px]" }, + supplier_name: { minWidth: "min-w-[140px]" }, + division: { + width: "w-[80px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + contact_person: { width: "w-[80px]" }, + contact_phone: { width: "w-[120px]" }, + email: { width: "w-[160px]" }, + business_number: { width: "w-[120px]" }, + address: { minWidth: "min-w-[150px]" }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 엑셀 다운로드 const handleExcelDownload = async () => { diff --git a/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx index 5baf35d3..2c0e1338 100644 --- a/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx @@ -28,6 +28,7 @@ const GRID_COLUMNS = [ { key: "item_code", label: "품목코드" }, { key: "item_name", label: "품목명" }, { key: "inspection_type", label: "검사유형" }, + { key: "item_count", label: "항목수" }, { key: "is_active", label: "사용여부" }, ]; const ITEM_TABLE = "item_info"; @@ -420,18 +421,41 @@ export default function ItemInspectionInfoPage() { 0 && checkedIds.length === data.length} onCheckedChange={(v) => setCheckedIds(v ? data.map(r => r.id) : [])} /> - 품목코드 - 품목명 - 검사유형 - 항목수 - 사용여부 + {ts.visibleColumns.map((col) => ( + + {col.label} + + ))} - {groupedData.map((group) => { + {ts.groupData(groupedData).map((group) => { + if ((group as any)._isGroupSummary || (group as any)._isGroupHeader) return null; const isExpanded = expandedItems.has(group.item_code); - const groupIds = group.rows.map(r => r.id); - const allChecked = groupIds.every(id => checkedIds.includes(id)); + const groupIds = group.rows.map((r: any) => r.id); + const allChecked = groupIds.every((id: string) => checkedIds.includes(id)); + const renderCell = (key: string) => { + switch (key) { + case "item_code": return {group.item_code}; + case "item_name": return {group.item_name}; + case "inspection_type": return ( + +
+ {group.types.map((t: string) => {t})} +
+
+ ); + case "item_count": return {group.rows.filter((r: any) => r.inspection_standard_id).length}; + case "is_active": return ( + + + {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} + + + ); + default: return {(group as any)[key] ?? ""}; + } + }; return ( { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !groupIds.includes(id)) : [...new Set([...prev, ...groupIds])]); }}> {}} /> - {group.item_code} - {group.item_name} - -
- {group.types.map(t => {t})} -
-
- {group.rows.filter(r => r.inspection_standard_id).length} - - - {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} - - + {ts.visibleColumns.map((col) => renderCell(col.key))}
- {isExpanded && group.rows.filter(r => r.inspection_standard_id).map((row, i) => ( + {isExpanded && group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( diff --git a/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx b/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx index bddc7730..20b98727 100644 --- a/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx @@ -12,7 +12,7 @@ * - 납품처 등록 (delivery_destination) */ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -820,12 +820,14 @@ export default function CustomerManagementPage() { const allItems = res.data?.data?.data || res.data?.data?.rows || []; setItemTotalCount(allItems.length); const existingItemIds = new Set(priceItems.map((p: any) => p.item_id || p.item_number)); - const SALES_CODES = ["CAT_ML8ZFVEL_1TOR"]; // 영업관리 카테고리 코드 - setItemSearchResults(allItems.filter((item: any) => { + const seenNumbers = new Set(); + const deduped = allItems.filter((item: any) => { if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false; - const divCodes = (item.division || "").split(",").map((c: string) => c.trim()); - return divCodes.some((code: string) => SALES_CODES.includes(code)); - })); + if (item.item_number && seenNumbers.has(item.item_number)) return false; + if (item.item_number) seenNumbers.add(item.item_number); + return true; + }); + setItemSearchResults(deduped); } catch { /* skip */ } finally { setItemSearchLoading(false); } }; @@ -1229,47 +1231,44 @@ export default function CustomerManagementPage() { } }; - // 컬럼 가시성 헬퍼 - const isColumnVisible = (key: string) => ts.isVisible(key); - - const customerColSpan = 1 + ["customer_code", "customer_name", "contact_person", "contact_phone", "division", "status"] - .filter((k) => isColumnVisible(k)).length; - - // EDataTable 컬럼 정의 (거래처 목록) - const customerColumns: EDataTableColumn[] = [ - ...(isColumnVisible("customer_code") ? [{ key: "customer_code", label: "거래처코드", width: "w-[120px]" }] : []), - ...(isColumnVisible("customer_name") ? [{ key: "customer_name", label: "거래처명", minWidth: "min-w-[140px]" }] : []), - ...(isColumnVisible("division") ? [{ - key: "division", - label: "거래유형", - width: "w-[80px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ...(isColumnVisible("contact_person") ? [{ key: "contact_person", label: "담당자", width: "w-[80px]" }] : []), - ...(isColumnVisible("contact_phone") ? [{ key: "contact_phone", label: "전화번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("email") ? [{ key: "email", label: "이메일", width: "w-[160px]" }] : []), - ...(isColumnVisible("business_number") ? [{ key: "business_number", label: "사업자번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("address") ? [{ key: "address", label: "주소", minWidth: "min-w-[150px]" }] : []), - ...(isColumnVisible("status") ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ]; + // EDataTable 컬럼 정의 (거래처 목록) — ts.visibleColumns 순서를 따름 + const customerColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + customer_code: { width: "w-[120px]" }, + customer_name: { minWidth: "min-w-[140px]" }, + division: { + width: "w-[80px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + contact_person: { width: "w-[80px]" }, + contact_phone: { width: "w-[120px]" }, + email: { width: "w-[160px]" }, + business_number: { width: "w-[120px]" }, + address: { minWidth: "min-w-[150px]" }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 엑셀 다운로드 const handleExcelDownload = async () => { diff --git a/frontend/app/(main)/COMPANY_16/sales/order/page.tsx b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx index f896bbfd..f9a358d4 100644 --- a/frontend/app/(main)/COMPANY_16/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx @@ -13,7 +13,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, ClipboardList, Pencil, Search, X, Truck, Package, - ChevronLeft, ChevronRight, ChevronDown, ChevronsLeft, ChevronsRight, + ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, RotateCcw, Filter, Check, ArrowUp, ArrowDown, } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; @@ -42,41 +42,30 @@ const formatNumber = (val: string) => { }; const parseNumber = (val: string) => val.replace(/,/g, ""); -// 마스터 헤더 레이아웃 (수주번호 뒤, 디테일 11컬럼 위에 colSpan으로 맵핑) -// 순서: 거래처 | 단가방식 | 납품처 | 납품장소 | 수주일 | 담당자 → 합계 colSpan = 11 -const MASTER_BODY_LAYOUT = [ - { key: "partner_id", label: "거래처", colSpan: 2 }, - { key: "price_mode", label: "단가방식", colSpan: 1 }, - { key: "delivery_partner_id", label: "납품처", colSpan: 2 }, - { key: "delivery_address", label: "납품장소", colSpan: 2 }, - { key: "order_date", label: "수주일", colSpan: 2 }, - { key: "manager_id", label: "담당자", colSpan: 2 }, +// 플랫 테이블 컬럼 정의 (마스터+디테일 통합) +const FLAT_COLUMNS = [ + { key: "order_no", label: "수주번호", source: "master" }, + { key: "partner_id", label: "거래처", source: "master" }, + { key: "order_date", label: "수주일", source: "master" }, + { key: "part_code", label: "품번", source: "detail" }, + { key: "part_name", label: "품명", source: "detail" }, + { key: "spec", label: "규격", source: "detail" }, + { key: "unit", label: "단위", source: "detail" }, + { key: "qty", label: "수량", source: "detail" }, + { key: "ship_qty", label: "출하수량", source: "detail" }, + { key: "balance_qty", label: "잔량", source: "detail" }, + { key: "unit_price", label: "단가", source: "detail" }, + { key: "amount", label: "금액", source: "detail" }, + { key: "due_date", label: "납기일", source: "detail" }, + { key: "memo", label: "메모", source: "master" }, ]; -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "part_code", label: "품번" }, - { key: "part_name", label: "품명" }, - { key: "spec", label: "규격" }, - { key: "unit", label: "단위" }, - { key: "qty", label: "수량" }, - { key: "ship_qty", label: "출하수량" }, - { key: "balance_qty", label: "잔량" }, - { key: "unit_price", label: "단가" }, - { key: "amount", label: "금액" }, - { key: "currency_code", label: "통화" }, - { key: "due_date", label: "납기일" }, -]; +const DETAIL_HEADER_COLS = FLAT_COLUMNS.filter((c) => c.source === "detail"); // 필터용 전체 키 -const GRID_COLUMNS_CONFIG = [ - { key: "order_no", label: "수주번호" }, - ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label })), - ...DETAIL_HEADER_COLS, - { key: "memo", label: "메모" }, -]; +const GRID_COLUMNS_CONFIG = FLAT_COLUMNS.map(({ key, label }) => ({ key, label })); -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 수주번호(1) + 디테일(11) + 메모(1) = 15 +// 총 컬럼 수: 체크박스(1) + 플랫 컬럼(14) = 15 const TOTAL_COLS = 15; // 헤더 필터 Popover @@ -180,8 +169,6 @@ export default function SalesOrderPage() { const [masterForm, setMasterForm] = useState>({}); const [detailRows, setDetailRows] = useState([]); const [allowPriceEdit, setAllowPriceEdit] = useState(true); - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); // 품목 선택 모달 const [itemSelectOpen, setItemSelectOpen] = useState(false); @@ -376,25 +363,8 @@ export default function SalesOrderPage() { useEffect(() => { fetchOrders(); }, [fetchOrders]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) - const columnUniqueValues = useMemo(() => { - const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { - const values = new Set(); - orders.forEach((row) => { - const val = row[col.key]; - if (val !== null && val !== undefined && val !== "") values.add(String(val)); - }); - result[col.key] = Array.from(values).sort(); - } - return result; - }, [orders]); - - // 마스터 필드 키 목록 (필터 분류용) - const MASTER_KEYS = new Set(["order_no", ...MASTER_BODY_LAYOUT.map((c) => c.key), "memo"]); - - // 카테고리 코드→라벨 변환 (마스터 필터용) - const resolveMasterLabel = useCallback((key: string, code: string) => { + // 카테고리 코드→라벨 변환 + const resolveLabel = useCallback((key: string, code: string) => { if (!code) return ""; if (key === "partner_id" || key === "manager_id" || key === "price_mode") { return categoryOptions[key]?.find((o) => o.code === code)?.label || code; @@ -402,106 +372,60 @@ export default function SalesOrderPage() { return code; }, [categoryOptions]); - // 필터 + 정렬 적용된 데이터 → 그룹핑 - const filteredOrderGroups = useMemo(() => { - // 1차: order_no 기준 그룹핑 (필터 전) - const allGroups: Record = {}; - for (const row of orders) { - const key = row.order_no || "_no_order"; - if (!allGroups[key]) { - allGroups[key] = { master: row._master || {}, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위 필터링) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - const raw = group.master?.[colKey] ?? ""; - const label = resolveMasterLabel(colKey, String(raw)); - return values.has(label) || values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위 필터링) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([orderNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - const cellVal = row[colKey] != null ? String(row[colKey]) : ""; - return values.has(cellVal); - }) - ); - return [orderNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - // 마스터 필드 정렬 → 그룹 단위 - entries.sort(([, a], [, b]) => { - const av = a.master?.[key] ?? ""; - const bv = b.master?.[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - // 디테일 필드 정렬 → 각 그룹 내 디테일 정렬 - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - const av = a[key] ?? ""; - const bv = b[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [orders, headerFilters, sortState, resolveMasterLabel]); - - // 마스터 컬럼별 고유값 (마스터 헤더 필터용) - const masterUniqueValues = useMemo(() => { - const result: Record = {}; - // 필터 전 전체 마스터에서 고유값 추출 - const seenMasters = new Map(); - orders.forEach((row) => { - if (row.order_no && row._master && !seenMasters.has(row.order_no)) { - seenMasters.set(row.order_no, row._master); - } + // 플랫 행 생성 (마스터 필드를 각 디테일 행에 병합) + const flatRows = useMemo(() => { + return orders.map((row) => { + const master = row._master || {}; + return { + ...row, + partner_id: resolveLabel("partner_id", master.partner_id || row.partner_id || ""), + order_date: master.order_date || row.order_date || "", + memo: row.memo || master.memo || "", + }; }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "order_no", label: "수주번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label })), { key: "memo", label: "메모" }]) { + }, [orders, resolveLabel]); + + // 컬럼별 고유값 (헤더 필터용) + const columnUniqueValues = useMemo(() => { + const result: Record = {}; + for (const col of FLAT_COLUMNS) { const values = new Set(); - masters.forEach((m) => { - const val = m?.[col.key]; - if (val !== null && val !== undefined && val !== "") { - values.add(resolveMasterLabel(col.key, String(val))); - } + flatRows.forEach((row) => { + const val = row[col.key]; + if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [orders, resolveMasterLabel]); + }, [flatRows]); + + // 필터 + 정렬 적용된 플랫 데이터 + const filteredFlatRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = row[colKey] != null ? String(row[colKey]) : ""; + return values.has(cellVal); + }); + } + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = a[key] ?? ""; + const bv = b[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -965,111 +889,70 @@ export default function SalesOrderPage() {
- {/* 데이터 테이블 (트리 구조) */} + {/* 데이터 테이블 (플랫 리스트) */}
- {/* 체크박스 */} - {/* 펼침 화살표 */} - {/* 수주번호 */} - {/* 품번 / 거래처 */} - {/* 품명 / 거래처(cont) */} - {/* 규격 / 단가방식 */} - {/* 단위 / 납품처 */} - {/* 수량 / 납품처(cont) */} - {/* 출하수량 / 납품장소 */} - {/* 잔량 / 납품장소(cont) */} - {/* 단가 / 수주일 */} - {/* 금액 / 수주일(cont) */} - {/* 통화 / 담당자 */} - {/* 납기일 / 담당자(cont) */} - {/* 메모 */} + + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredOrderGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredFlatRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredOrderGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredFlatRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 수주번호 (별도 컬럼) */} - -
-
handleSort("order_no")}> - 수주번호 - {sortState?.key === "order_no" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["order_no"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (colSpan으로 디테일 컬럼 위에 맵핑) */} - {MASTER_BODY_LAYOUT.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {FLAT_COLUMNS.map((col) => { + const isRight = ["qty", "ship_qty", "balance_qty", "unit_price", "amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} - {/* 메모 (마스터) */} - -
-
handleSort("memo")}> - 메모 - {sortState?.key === "memo" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["memo"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
+ + ); + })} @@ -1079,7 +962,7 @@ export default function SalesOrderPage() { - ) : Object.keys(filteredOrderGroups).length === 0 ? ( + ) : filteredFlatRows.length === 0 ? (
@@ -1089,200 +972,48 @@ export default function SalesOrderPage() { ) : ( - Object.entries(filteredOrderGroups).map(([orderNo, group]) => { - const isExpanded = expandedOrders.has(orderNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredFlatRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 — 마스터 테이블 필드만 표시 */} - { - if (expandedOrders.has(orderNo)) { - setClosingOrders((prev) => new Set(prev).add(orderNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(orderNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(orderNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(orderNo)); - } - }} - onDoubleClick={() => openEditModal(orderNo)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 수주번호 */} - - {orderNo} - ({group.details.length}) - - {/* 거래처 (colSpan=2) */} - - - {master.partner_id ? (categoryOptions["partner_id"]?.find((o) => o.code === master.partner_id)?.label || master.partner_id) : ""} - - - {/* 단가방식 (colSpan=1) */} - - - {master.price_mode ? (categoryOptions["price_mode"]?.find((o) => o.code === master.price_mode)?.label || master.price_mode) : ""} - - - {/* 납품처 (colSpan=2) */} - - {master.delivery_partner_id || ""} - - {/* 납품장소 (colSpan=2) */} - - {master.delivery_address || ""} - - {/* 수주일 (colSpan=2) */} - - {master.order_date || ""} - - {/* 담당자 (colSpan=2) */} - - - {master.manager_id ? (categoryOptions["manager_id"]?.find((o) => o.code === master.manager_id)?.label || master.manager_id) : ""} - - - {/* 메모 */} - - {master.memo || ""} - - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - {/* 수주번호 컬럼 빈 셀 */} - {DETAIL_HEADER_COLS.map((col) => { - const isRight = ["qty", "ship_qty", "balance_qty", "unit_price", "amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => d[col.key]).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} - -
+ { - const isClosing = closingOrders.has(orderNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - onDoubleClick={() => openEditModal(row.order_no)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - {/* 수주번호 컬럼 빈 셀 */} - {row.part_code} - {row.part_name} - {row.spec} - {row.unit} - {row.qty ? Number(row.qty).toLocaleString() : ""} - {row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""} - {row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {row.amount ? Number(row.amount).toLocaleString() : ""} - {row.currency_code || ""} - {row.due_date || ""} - - + onClick={() => { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] ); - })} - + }} + onDoubleClick={() => openEditModal(row.order_no)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + > + {}} /> + + {row.order_no} + {row.partner_id || ""} + {row.order_date || ""} + {row.part_code} + {row.part_name} + {row.spec} + {row.unit} + {row.qty ? Number(row.qty).toLocaleString() : ""} + {row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""} + {row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.amount ? Number(row.amount).toLocaleString() : ""} + {row.due_date || ""} + {row.memo || ""} + ); }) )} diff --git a/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx index 4ab5a9ad..2ed29b40 100644 --- a/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx @@ -363,7 +363,7 @@ export default function ShippingOrderPage() { spec: item.spec, material: item.material, orderQty: item.orderQty, - planQty: item.planQty, + planQty: item.orderQty, shipQty: 0, sourceType: item.sourceType, shipmentPlanId: item.shipmentPlanId, diff --git a/frontend/app/(main)/COMPANY_29/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_29/equipment/info/page.tsx index eeb63844..cd53e9b1 100644 --- a/frontend/app/(main)/COMPANY_29/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_29/equipment/info/page.tsx @@ -142,15 +142,20 @@ export default function EquipmentInfoPage() { }; const mainTableColumns = useMemo(() => { - const cols: EDataTableColumn[] = []; - if (ts.isVisible("equipment_code")) cols.push({ key: "equipment_code", label: "설비코드", width: "w-[110px]" }); - if (ts.isVisible("equipment_name")) cols.push({ key: "equipment_name", label: "설비명", minWidth: "min-w-[130px]", truncate: true, render: (v) => v || "-" }); - if (ts.isVisible("equipment_type")) cols.push({ key: "equipment_type", label: "설비유형", width: "w-[90px]", render: (v) => v || "-" }); - if (ts.isVisible("manufacturer")) cols.push({ key: "manufacturer", label: "제조사", width: "w-[100px]", render: (v) => v || "-" }); - if (ts.isVisible("installation_location")) cols.push({ key: "installation_location", label: "설치장소", width: "w-[100px]", render: (v) => v || "-" }); - if (ts.isVisible("operation_status")) cols.push({ key: "operation_status", label: "가동상태", width: "w-[80px]", render: (v) => v || "-" }); - return cols; - }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + 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 || "-" }, + manufacturer: { width: "w-[100px]", render: (v) => v || "-" }, + installation_location: { width: "w-[100px]", render: (v) => v || "-" }, + operation_status: { width: "w-[80px]", render: (v) => v || "-" }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 설비 조회 const fetchEquipments = useCallback(async () => { @@ -272,8 +277,8 @@ export default function EquipmentInfoPage() { if (!inspectionForm.inspection_cycle) { toast.error("점검주기는 필수입니다."); return; } if (!inspectionForm.inspection_method) { toast.error("점검방법은 필수입니다."); return; } const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; - if (isNumeric && !inspectionForm.unit) { toast.error("숫자 점검방법은 측정단위가 필수입니다."); return; } + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); + if (isNumeric && !inspectionForm.unit) { toast.error("측정단위가 필수입니다."); return; } // 기준값/오차범위 → 하한치/상한치 자동 계산 const saveData = { ...inspectionForm }; if (isNumeric && saveData.standard_value) { @@ -739,7 +744,7 @@ export default function EquipmentInfoPage() {
{catSelect("inspection_method", inspectionForm.inspection_method, (v) => { const label = resolve("inspection_method", v); - const isNum = label === "숫자" || v === "숫자"; + const isNum = ["숫자", "치수검사"].includes(label) || ["숫자", "치수검사"].includes(v); if (!isNum) { setInspectionForm((p) => ({ ...p, inspection_method: v, unit: "", standard_value: "", tolerance: "", lower_limit: "", upper_limit: "" })); } else { @@ -748,7 +753,7 @@ export default function EquipmentInfoPage() { }, "점검방법")}
{(() => { const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); if (!isNumeric) return null; return (
@@ -758,7 +763,7 @@ export default function EquipmentInfoPage() {
{(() => { const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); if (!isNumeric) return null; return (
diff --git a/frontend/app/(main)/COMPANY_29/logistics/material-status/page.tsx b/frontend/app/(main)/COMPANY_29/logistics/material-status/page.tsx index 46de306c..eb87ba92 100644 --- a/frontend/app/(main)/COMPANY_29/logistics/material-status/page.tsx +++ b/frontend/app/(main)/COMPANY_29/logistics/material-status/page.tsx @@ -333,69 +333,90 @@ export default function MaterialStatusPage() {

) : ( - workOrders.map((wo) => ( -
handleSelectWo(wo.id)} - > + ts.groupData(workOrders).map((wo) => { + if ((wo as any)._isGroupSummary || (wo as any)._isGroupHeader) return null; + return (
e.stopPropagation()} + key={wo.id} + className={cn( + "flex gap-3 rounded-lg border p-3 transition-all cursor-pointer", + "hover:border-primary/50 hover:shadow-sm", + selectedWoId === wo.id + ? "border-primary bg-primary/5 shadow-sm" + : "border-border" + )} + onClick={() => handleSelectWo(wo.id)} > - - handleCheckWo(wo.id, c as boolean) - } - /> -
-
-
- - {wo.plan_no || wo.work_order_no || `WO-${wo.id}`} - - e.stopPropagation()} + > + + handleCheckWo(wo.id, c as boolean) + } + /> +
+
+
+ {ts.isVisible("plan_no") && ( + + {wo.plan_no || wo.work_order_no || `WO-${wo.id}`} + )} - > - {getStatusLabel(wo.status)} - -
-
- - {wo.item_name} - - - ({wo.item_code}) - -
-
- 수량: - - {Number(wo.plan_qty).toLocaleString()}개 - - | - 일자: - - {wo.plan_date - ? new Date(wo.plan_date) - .toISOString() - .slice(0, 10) - : "-"} - + {ts.isVisible("status") && ( + + {getStatusLabel(wo.status)} + + )} +
+
+ {ts.isVisible("item_name") && ( + + {wo.item_name} + + )} + {ts.isVisible("item_code") && ( + + ({wo.item_code}) + + )} +
+
+ {ts.isVisible("plan_qty") && ( + <> + 수량: + + {Number(wo.plan_qty).toLocaleString()}개 + + + )} + {ts.isVisible("plan_qty") && ts.isVisible("plan_date") && ( + | + )} + {ts.isVisible("plan_date") && ( + <> + 일자: + + {wo.plan_date + ? new Date(wo.plan_date) + .toISOString() + .slice(0, 10) + : "-"} + + + )} +
-
- )) + ); + }) )}
diff --git a/frontend/app/(main)/COMPANY_29/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_29/logistics/outbound/page.tsx index 5ab46a8a..c1ffbd40 100644 --- a/frontend/app/(main)/COMPANY_29/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_29/logistics/outbound/page.tsx @@ -140,8 +140,16 @@ const DETAIL_HEADER_COLS = [ // 마스터 필드 키 목록 (필터 분류용) const MASTER_KEYS = new Set(["outbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 출고번호(1) + 마스터필드(7) = 10 -const TOTAL_COLS = 10; +// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) +const DETAIL_KEY_MAP: Record = { + source_type: "source_type", + item_number: "item_code", + item_name: "item_name", + spec: "specification", + outbound_qty: "outbound_qty", + unit_price: "unit_price", + total_amount: "total_amount", +}; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -248,6 +256,31 @@ interface SelectedSourceItem { export default function OutboundPage() { const ts = useTableSettings("c16-outbound", "outbound_mng", GRID_COLUMNS); + + // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 + const visibleMasterLayout = useMemo(() => { + const ordered: typeof MASTER_BODY_LAYOUT = []; + for (const vc of ts.visibleColumns) { + const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); + if (m) ordered.push(m); + } + return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; + }, [ts.visibleColumns]); + + const visibleDetailCols = useMemo(() => { + const ordered: typeof DETAIL_HEADER_COLS = []; + for (const vc of ts.visibleColumns) { + const detailKey = DETAIL_KEY_MAP[vc.key]; + if (detailKey) { + const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); + if (d) ordered.push(d); + } + } + return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; + }, [ts.visibleColumns]); + + const TOTAL_COLS = 3 + visibleMasterLayout.length; + // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -900,8 +933,15 @@ export default function OutboundPage() {
-
- +
+ + + + + {visibleMasterLayout.map((col) => ( + + ))} + - {/* 마스터 필드 헤더 */} - {MASTER_BODY_LAYOUT.map((col) => ( + {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => (
handleSort(col.key)}> @@ -1039,38 +1079,51 @@ export default function OutboundPage() { {outboundNo} ({group.details.length}) - {/* 출고유형 */} - - - {master.outbound_type || "-"} - - - {/* 출고일 */} - - {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} - - {/* 참조번호 */} - - {master.reference_number || ""} - - {/* 거래처 */} - - {master.customer_name || ""} - - {/* 창고 */} - - {master.warehouse_name || master.warehouse_code || ""} - - {/* 출고상태 */} - - - {master.outbound_status || "-"} - - - {/* 비고 */} - - {master.memo || ""} - + {/* 마스터 필드 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => { + switch (col.key) { + case "outbound_type": return ( + + + {master.outbound_type || "-"} + + + ); + case "outbound_date": return ( + + {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} + + ); + case "reference_number": return ( + + {master.reference_number || ""} + + ); + case "customer_name": return ( + + {master.customer_name || ""} + + ); + case "warehouse_name": return ( + + {master.warehouse_name || master.warehouse_code || ""} + + ); + case "outbound_status": return ( + + + {master.outbound_status || "-"} + + + ); + case "memo": return ( + + {master.memo || ""} + + ); + default: return {(master as any)[col.key] ?? ""}; + } + })} {/* 디테일 서브 헤더 (펼쳤을 때만) */} @@ -1084,7 +1137,7 @@ export default function OutboundPage() { - {DETAIL_HEADER_COLS.map((col) => { + {visibleDetailCols.map((col) => { const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); const isSorted = sortState?.key === col.key; const uniqueVals = Array.from(new Set( @@ -1163,20 +1216,18 @@ export default function OutboundPage() {
- {/* 출처 */} - {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"} - {/* 품목코드 */} - {row.item_code || ""} - {/* 품목명 */} - {row.item_name || ""} - {/* 규격 */} - {row.specification || ""} - {/* 출고수량 */} - {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""} - {/* 단가 */} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {/* 금액 */} - {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {visibleDetailCols.map((col) => { + switch (col.key) { + case "source_type": return {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"}; + case "item_code": return {row.item_code || ""}; + case "item_name": return {row.item_name || ""}; + case "specification": return {row.specification || ""}; + case "outbound_qty": return {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""}; + case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; + case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; + default: return {(row as any)[col.key] ?? ""}; + } + })} ); })} diff --git a/frontend/app/(main)/COMPANY_29/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_29/logistics/packaging/page.tsx index 5d4d5787..6ae340aa 100644 --- a/frontend/app/(main)/COMPANY_29/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_29/logistics/packaging/page.tsx @@ -460,18 +460,20 @@ export default function PackagingPage() { {/* 포장재 목록 테이블 */}
PKG_TYPE_LABEL[v] || v || "-" }, - { key: "size", label: "크기(mm)", width: "w-[100px]", render: (_v, row) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, - { key: "max_load_kg", label: "최대중량", width: "w-[80px]", align: "right", render: (v) => Number(v || 0) > 0 ? `${v}kg` : "-" }, - { key: "status", label: "상태", width: "w-[60px]", align: "center", render: (v) => ( - - {STATUS_LABEL[v] || v} - - )}, - ] as EDataTableColumn[]} + columns={ts.visibleColumns.map((col): EDataTableColumn => { + const renderMap: Record>> = { + pkg_type: { width: "w-[80px]", render: (v: any) => PKG_TYPE_LABEL[v] || v || "-" }, + size: { width: "w-[100px]", render: (_v: any, row: any) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, + max_weight: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, + max_load_kg: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, + status: { width: "w-[60px]", align: "center", render: (v: any) => ( + + {STATUS_LABEL[v] || v} + + )}, + }; + return { key: col.key, label: col.label, ...renderMap[col.key] }; + })} data={ts.groupData(filteredPkgUnits)} rowKey={(row) => String(row.id)} loading={pkgLoading} diff --git a/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx index a8d5fc2c..85cdc23c 100644 --- a/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx @@ -117,12 +117,20 @@ const DETAIL_HEADER_COLS = [ { key: "total_amount", label: "금액" }, ]; -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 입고번호(1) + 디테일(7) = 10 -const TOTAL_COLS = 10; - // 마스터 필드 키 목록 (필터 분류용) const MASTER_KEYS = new Set(["inbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); +// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) +const DETAIL_KEY_MAP: Record = { + source_type: "source_table", + item_number: "item_number", + item_name: "item_name", + spec: "spec", + inbound_qty: "inbound_qty", + unit_price: "unit_price", + total_amount: "total_amount", +}; + // 헤더 필터 Popover function HeaderFilterPopover({ colKey, colLabel, uniqueValues, filterValues, onToggle, onClear, @@ -278,6 +286,31 @@ interface SelectedSourceItem { export default function ReceivingPage() { const ts = useTableSettings("c16-receiving", "inbound_mng", GRID_COLUMNS); + + // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 + const visibleMasterLayout = useMemo(() => { + const ordered: typeof MASTER_BODY_LAYOUT = []; + for (const vc of ts.visibleColumns) { + const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); + if (m) ordered.push(m); + } + return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; + }, [ts.visibleColumns]); + + const visibleDetailCols = useMemo(() => { + const ordered: typeof DETAIL_HEADER_COLS = []; + for (const vc of ts.visibleColumns) { + const detailKey = DETAIL_KEY_MAP[vc.key]; + if (detailKey) { + const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); + if (d) ordered.push(d); + } + } + return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; + }, [ts.visibleColumns]); + + const TOTAL_COLS = 3 + visibleMasterLayout.length; + // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -847,8 +880,15 @@ export default function ReceivingPage() {
-
- +
+ + + + + {visibleMasterLayout.map((col) => ( + + ))} + - {/* 마스터 필드 헤더 (colSpan으로 디테일 컬럼 위에 맵핑) */} - {MASTER_BODY_LAYOUT.map((col) => ( + {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => (
handleSort(col.key)}> @@ -985,38 +1025,51 @@ export default function ReceivingPage() { {inboundNo} ({group.details.length}) - {/* 입고유형 */} - - - {resolveInboundType(master.inbound_type)} - - - {/* 입고일 */} - - {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} - - {/* 참조번호 */} - - {master.reference_number || ""} - - {/* 공급처 */} - - {master.supplier_name || ""} - - {/* 창고 */} - - {master.warehouse_name || master.warehouse_code || ""} - - {/* 입고상태 */} - - - {master.inbound_status || "-"} - - - {/* 비고 */} - - {master.memo || ""} - + {/* 마스터 필드 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => { + switch (col.key) { + case "inbound_type": return ( + + + {resolveInboundType(master.inbound_type)} + + + ); + case "inbound_date": return ( + + {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} + + ); + case "reference_number": return ( + + {master.reference_number || ""} + + ); + case "supplier_name": return ( + + {master.supplier_name || ""} + + ); + case "warehouse_name": return ( + + {master.warehouse_name || master.warehouse_code || ""} + + ); + case "inbound_status": return ( + + + {master.inbound_status || "-"} + + + ); + case "memo": return ( + + {master.memo || ""} + + ); + default: return {(master as any)[col.key] ?? ""}; + } + })} {/* 디테일 서브 헤더 (펼쳤을 때만) */} @@ -1030,7 +1083,7 @@ export default function ReceivingPage() { - {DETAIL_HEADER_COLS.map((col) => { + {visibleDetailCols.map((col) => { const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); const isSorted = sortState?.key === col.key; const uniqueVals = Array.from(new Set( @@ -1108,20 +1161,18 @@ export default function ReceivingPage() {
- {/* 출처 */} - {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"} - {/* 품목코드 */} - {row.item_number || ""} - {/* 품목명 */} - {row.item_name || ""} - {/* 규격 */} - {row.spec || ""} - {/* 입고수량 */} - {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""} - {/* 단가 */} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {/* 금액 */} - {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {visibleDetailCols.map((col) => { + switch (col.key) { + case "source_table": return {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"}; + case "item_number": return {row.item_number || ""}; + case "item_name": return {row.item_name || ""}; + case "spec": return {row.spec || ""}; + case "inbound_qty": return {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""}; + case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; + case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; + default: return {(row as any)[col.key] ?? ""}; + } + })} ); })} 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 dfd1b666..9d7f2dea 100644 --- a/frontend/app/(main)/COMPANY_29/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_29/master-data/company/page.tsx @@ -491,12 +491,6 @@ export default function CompanyPage() { > 회사정보 - - 부서관리 -
@@ -635,89 +629,6 @@ export default function CompanyPage() {
- {/* ===================== Tab 2: 부서관리 ===================== */} - -
- - {/* 좌측: 부서 트리 */} - -
-
-
- - 부서 - {depts.length}건 -
-
- - - -
-
-
- {deptLoading ? ( -
- -
- ) : deptTree.length === 0 ? ( -
- - 등록된 부서가 없어요 -
- ) : ( - renderTree(deptTree) - )} -
-
-
- - - - {/* 우측: 사원 목록 */} - -
-
-
- - {selectedDept ? "부서 인원" : "부서를 선택해주세요"} - {selectedDept && {selectedDept.dept_name}} - {members.length > 0 && {members.length}명} -
- {selectedDeptCode && ( - - )} -
- {selectedDeptCode ? ( - row.user_id || row.id} - loading={memberLoading} - emptyMessage="소속 사원이 없어요" - emptyIcon={} - onRowDoubleClick={(row) => openUserModal(row)} - showPagination={false} - draggableColumns={false} - /> - ) : ( -
- - 좌측에서 부서를 선택해주세요 -
- )} -
-
-
-
-
{/* ── 부서 등록/수정 모달 ── */} diff --git a/frontend/app/(main)/COMPANY_29/master-data/department/page.tsx b/frontend/app/(main)/COMPANY_29/master-data/department/page.tsx index a2bbcba5..3245571e 100644 --- a/frontend/app/(main)/COMPANY_29/master-data/department/page.tsx +++ b/frontend/app/(main)/COMPANY_29/master-data/department/page.tsx @@ -9,7 +9,7 @@ * 모달: 부서 등록(dept_info), 사원 추가(user_info) */ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -279,6 +279,7 @@ export default function DepartmentPage() { dept_code: userForm.dept_code || undefined, dept_name: depts.find((d) => d.dept_code === userForm.dept_code)?.dept_name || undefined, status: userForm.status || "active", + end_date: userForm.end_date || null, }, mainDept: userForm.dept_code ? { dept_code: userForm.dept_code, @@ -312,37 +313,40 @@ export default function DepartmentPage() { const activeMembers = members.filter((m) => !m.end_date || m.end_date.substring(0, 10) >= today); const resignedMembers = members.filter((m) => m.end_date && m.end_date.substring(0, 10) < today); - const isColVisible = (key: string) => ts.isVisible(key); - - // EDataTable 컬럼 정의 (부서 목록) - const deptColumns: EDataTableColumn[] = [ - { key: "dept_code", label: "부서코드", width: "w-[120px]" }, - { key: "dept_name", label: "부서명", minWidth: "min-w-[140px]" }, - ...(isColVisible("parent_dept_code") - ? [{ - key: "parent_dept_code", - label: "상위부서", - width: "w-[110px]", - render: (val: any) => {val || "\u2014"}, - }] - : []), - ...(isColVisible("status") - ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val === "active" ? "활성" : (val || "\u2014")} - - ) : null, - }] - : []), - ]; + // EDataTable 컬럼 정의 (부서 목록) — ts.visibleColumns 순서를 따름 + const deptColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + dept_code: { width: "w-[120px]" }, + dept_name: { minWidth: "min-w-[140px]" }, + parent_dept_code: { + width: "w-[110px]", + render: (val: any) => {val || "\u2014"}, + }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val === "active" ? "활성" : (val || "\u2014")} + + ) : null, + }, + }; + // dept_code, dept_name은 항상 표시 (DEPT_COLUMNS에 포함되지 않으므로 visibleColumns에 없음) + const fixedCols: EDataTableColumn[] = [ + { key: "dept_code", label: "부서코드", ...colProps["dept_code"] }, + { key: "dept_name", label: "부서명", ...colProps["dept_name"] }, + ]; + const dynamicCols = ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + return [...fixedCols, ...dynamicCols]; + }, [ts.visibleColumns]); return (
diff --git a/frontend/app/(main)/COMPANY_29/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_29/master-data/item-info/page.tsx index 3f037275..375fd900 100644 --- a/frontend/app/(main)/COMPANY_29/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_29/master-data/item-info/page.tsx @@ -84,6 +84,56 @@ function CategoryCombobox({ options, value, onChange, placeholder }: { ); } +// 다중 선택 카테고리 콤보박스 +function MultiCategoryCombobox({ options, value, onChange, placeholder }: { + options: { code: string; label: string }[]; + value: string; + onChange: (v: string) => void; + placeholder: string; +}) { + const [open, setOpen] = useState(false); + const selectedCodes = value ? value.split(",").map((c) => c.trim()).filter(Boolean) : []; + const selectedLabels = selectedCodes.map((code) => options.find((o) => o.code === code)?.label || code).filter(Boolean); + + const toggle = (code: string) => { + const next = selectedCodes.includes(code) + ? selectedCodes.filter((c) => c !== code) + : [...selectedCodes, code]; + onChange(next.join(",")); + }; + + return ( + + + + + + + + + 검색 결과가 없어요 + + {options.map((opt) => ( + toggle(opt.code)}> + + {opt.label} + + ))} + + + + + + ); +} + const TABLE_NAME = "item_info"; const GRID_COLUMNS = [ @@ -108,7 +158,7 @@ const GRID_COLUMNS = [ const FORM_FIELDS = [ { key: "item_number", label: "품목코드", type: "text", required: true, disabled: true, placeholder: "자동 채번" }, { key: "item_name", label: "품명", type: "text", required: true }, - { key: "division", label: "관리품목", type: "category" }, + { key: "division", label: "관리품목", type: "multi-category" }, { key: "type", label: "품목구분", type: "category" }, { key: "size", label: "규격", type: "text" }, { key: "unit", label: "단위", type: "category" }, @@ -137,6 +187,7 @@ export default function ItemInfoPage() { const { user } = useAuth(); const ts = useTableSettings("c16-item-info", TABLE_NAME, GRID_COLUMNS); const [items, setItems] = useState([]); + const [rawItems, setRawItems] = useState([]); const [loading, setLoading] = useState(false); // 검색 필터 (DynamicSearchFilter) @@ -215,6 +266,7 @@ export default function ItemInfoPage() { } return categoryOptions[col]?.find((o) => o.code === code)?.label || code; }; + setRawItems(raw); const data = raw.map((r: any) => { const converted = { ...r }; for (const col of CATEGORY_COLUMNS) { @@ -261,7 +313,8 @@ export default function ItemInfoPage() { // 수정 모달 열기 const openEditModal = (item: any) => { - setFormData({ ...item }); + const raw = rawItems.find((r) => r.id === item.id) || item; + setFormData({ ...raw }); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); @@ -269,7 +322,8 @@ export default function ItemInfoPage() { // 복사 모달 열기 const openCopyModal = async (item: any) => { - const { id, item_number, created_date, updated_date, writer, ...rest } = item; + const raw = rawItems.find((r) => r.id === item.id) || item; + const { id, item_number, created_date, updated_date, writer, ...rest } = raw; setFormData(rest); setIsEditMode(false); setEditId(null); @@ -459,6 +513,13 @@ export default function ItemInfoPage() { columnName={field.key} height="h-32" /> + ) : field.type === "multi-category" ? ( + setFormData((prev) => ({ ...prev, [field.key]: v }))} + placeholder={`${field.label} 선택`} + /> ) : field.type === "category" ? ( (() => { - const cols: EDataTableColumn[] = []; - if (ts.isVisible("item_number")) cols.push({ key: "item_number", label: "품번", width: "w-[110px]" }); - if (ts.isVisible("item_name")) cols.push({ key: "item_name", label: "품명", minWidth: "min-w-[130px]", render: (v) => v || "-" }); - if (ts.isVisible("size")) cols.push({ key: "size", label: "규격", width: "w-[90px]", render: (v) => v || "-" }); - if (ts.isVisible("unit")) cols.push({ key: "unit", label: "단위", width: "w-[60px]", render: (v) => v || "-" }); - if (ts.isVisible("standard_price")) cols.push({ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }); - if (ts.isVisible("selling_price")) cols.push({ key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true }); - if (ts.isVisible("currency_code")) cols.push({ key: "currency_code", label: "통화", width: "w-[50px]", render: (v) => v || "-" }); - if (ts.isVisible("status")) cols.push({ key: "status", label: "상태", width: "w-[60px]", render: (v) => v || "-" }); - return cols; - }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + const colProps: Record> = { + item_number: { width: "w-[110px]" }, + item_name: { minWidth: "min-w-[130px]", render: (v) => v || "-" }, + size: { width: "w-[90px]", render: (v) => v || "-" }, + unit: { width: "w-[60px]", render: (v) => v || "-" }, + standard_price: { width: "w-[90px]", align: "right", formatNumber: true }, + selling_price: { width: "w-[90px]", align: "right", formatNumber: true }, + currency_code: { width: "w-[50px]", render: (v) => v || "-" }, + status: { width: "w-[60px]", render: (v) => v || "-" }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 좌측: 품목 조회 (division이 "외주관리"인 품목만 필터링) const outsourcingDivisionCode = categoryOptions["division"]?.find( diff --git a/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx index 4a0d341a..6bd63176 100644 --- a/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx @@ -919,9 +919,7 @@ export default function ProductionPlanManagementPage() { // 숫자 포맷 const formatNumber = (num: number | string) => Number(num).toLocaleString(); - // 컬럼 표시 여부 - const isColVisible = (key: string) => ts.isVisible(key); - const orderColSpan = 4 + ORDER_COLUMNS.filter((c) => isColVisible(c.key)).length; + // (컬럼 표시는 ts.visibleColumns 순서를 따름) return (
@@ -1019,6 +1017,38 @@ export default function ProductionPlanManagementPage() {
) : (
+ {(() => { + // 디테일 행에서 개별 값을 표시하는 컬럼 매핑 + const DETAIL_VALUE_MAP: Record = { + total_order_qty: "order_qty", + total_ship_qty: "ship_qty", + total_balance_qty: "balance_qty", + }; + + // 그룹 행에서 특수 렌더링이 필요한 컬럼 + const renderGroupCell = (col: { key: string }, item: any) => { + if (col.key === "required_plan_qty") { + return ( + 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> + {formatNumber(item.required_plan_qty)} + + ); + } + if (col.key === "lead_time") { + return ( + toggleItemExpand(item.item_code)}> + {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"} + + ); + } + return ( + toggleItemExpand(item.item_code)}> + {formatNumber(item[col.key])} + + ); + }; + + return (
@@ -1028,15 +1058,11 @@ export default function ProductionPlanManagementPage() { 품목코드 품목명 - {isColVisible("total_order_qty") && 총수주량} - {isColVisible("total_ship_qty") && 출고량} - {isColVisible("total_balance_qty") && 잔량} - {isColVisible("current_stock") && 현재고} - {isColVisible("safety_stock") && 안전재고} - {isColVisible("existing_plan_qty") && 기생산계획량} - {isColVisible("in_progress_qty") && 생산진행} - {isColVisible("required_plan_qty") && 필요생산계획} - {isColVisible("lead_time") && 리드타임(일)} + {ts.visibleColumns.map((col) => ( + + {col.label} + + ))} @@ -1046,6 +1072,7 @@ export default function ProductionPlanManagementPage() { + {ts.visibleColumns.map((col) => { const v = (item as any)[col.key]; return ( @@ -1068,25 +1095,14 @@ export default function ProductionPlanManagementPage() { toggleItemExpand(item.item_code)}>{item.item_code} toggleItemExpand(item.item_code)}>{item.item_name} - {isColVisible("total_order_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_order_qty)}} - {isColVisible("total_ship_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_ship_qty)}} - {isColVisible("total_balance_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_balance_qty)}} - {isColVisible("current_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.current_stock)}} - {isColVisible("safety_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.safety_stock)}} - {isColVisible("existing_plan_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.existing_plan_qty)}} - {isColVisible("in_progress_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.in_progress_qty)}} - {isColVisible("required_plan_qty") && ( - 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> - {formatNumber(item.required_plan_qty)} - - )} - {isColVisible("lead_time") && ( - toggleItemExpand(item.item_code)}> - {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"} - - )} + {ts.visibleColumns.map((col) => renderGroupCell(col, item))} - {expandedItems.has(item.item_code) && item.orders?.map((detail) => ( + {expandedItems.has(item.item_code) && item.orders?.map((detail: any) => { + let remainColSpan = 0; + for (const col of ts.visibleColumns) { + if (!DETAIL_VALUE_MAP[col.key]) remainColSpan++; + } + return ( @@ -1101,19 +1117,28 @@ export default function ProductionPlanManagementPage() { - {isColVisible("total_order_qty") && {formatNumber(detail.order_qty)}} - {isColVisible("total_ship_qty") && {formatNumber(detail.ship_qty)}} - {isColVisible("total_balance_qty") && {formatNumber(detail.balance_qty)}} - - 납기일: {detail.due_date || "-"} - + {ts.visibleColumns.map((col) => { + const detailKey = DETAIL_VALUE_MAP[col.key]; + if (detailKey) { + return {formatNumber(detail[detailKey])}; + } + return null; + })} + {remainColSpan > 0 && ( + + 납기일: {detail.due_date || "-"} + + )} - ))} + ); + })} ); })}
+ ); + })()} )} diff --git a/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx index fa0e08c5..1bc3bc88 100644 --- a/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx @@ -742,10 +742,24 @@ export default function PurchaseOrderPage() { ) : ( (() => { const MASTER_KEYS = new Set(["purchase_no", "order_date", "supplier_name", "status", "memo"]); - const detailCols = ts.visibleColumns.filter(c => !MASTER_KEYS.has(c.key)); - const masterCols = ts.visibleColumns.filter(c => MASTER_KEYS.has(c.key)); const numCols = new Set(["order_qty", "received_qty", "remain_qty", "unit_price", "amount"]); + // ts.visibleColumns 순서를 따르되, 마스터/디테일 컬럼을 분리 + // 고정 컬럼(품목수)은 마스터 선행 컬럼 뒤에 배치 + const leadingMaster: typeof ts.visibleColumns = []; + const detailCols: typeof ts.visibleColumns = []; + const trailingMaster: typeof ts.visibleColumns = []; + let passedFirstDetail = false; + for (const col of ts.visibleColumns) { + if (MASTER_KEYS.has(col.key)) { + if (passedFirstDetail) trailingMaster.push(col); + else leadingMaster.push(col); + } else { + passedFirstDetail = true; + detailCols.push(col); + } + } + const renderDetailCell = (row: any, key: string) => { const val = row[key]; if (key === "status") return val ? {val} : "-"; @@ -753,23 +767,35 @@ export default function PurchaseOrderPage() { return val || "-"; }; + const renderMasterHead = (col: { key: string; label: string }) => ( + + {col.label} + + ); + + const renderMasterCell = (col: { key: string }, m: any, purchaseNo: string) => { + if (col.key === "purchase_no") return {purchaseNo}; + if (col.key === "order_date") return {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}; + if (col.key === "supplier_name") return {m.supplier_name || "-"}; + if (col.key === "status") return {m.status && {m.status}}; + if (col.key === "memo") return {m.memo || ""}; + return ; + }; + return ( - {ts.isVisible("purchase_no") && 발주번호} - {ts.isVisible("order_date") && 발주일} - {ts.isVisible("supplier_name") && 공급업체} + {leadingMaster.map(renderMasterHead)} 품목수 {detailCols.map(col => ( {col.label}{col.key === "order_qty" || col.key === "amount" ? " 합계" : ""} ))} - {ts.isVisible("status") && 상태} - {ts.isVisible("memo") && 메모} + {trailingMaster.map(renderMasterHead)} @@ -795,9 +821,7 @@ export default function PurchaseOrderPage() { { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !detailIds.includes(id)) : [...new Set([...prev, ...detailIds])]); }}> {}} /> - {ts.isVisible("purchase_no") && {purchaseNo}} - {ts.isVisible("order_date") && {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}} - {ts.isVisible("supplier_name") && {m.supplier_name || "-"}} + {leadingMaster.map(col => renderMasterCell(col, m, purchaseNo))} {group.details.length}건 {detailCols.map(col => ( @@ -806,8 +830,7 @@ export default function PurchaseOrderPage() { : ""} ))} - {ts.isVisible("status") && {m.status && {m.status}}} - {ts.isVisible("memo") && {m.memo || ""}} + {trailingMaster.map(col => renderMasterCell(col, m, purchaseNo))} {isExpanded && group.details.map((row) => ( @@ -815,17 +838,14 @@ export default function PurchaseOrderPage() { { e.stopPropagation(); setCheckedIds(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id]); }}> {}} /> - {ts.isVisible("purchase_no") && } - {ts.isVisible("order_date") && } - {ts.isVisible("supplier_name") && } + {leadingMaster.map(col => )} {detailCols.map(col => ( {renderDetailCell(row, col.key)} ))} - {ts.isVisible("status") && } - {ts.isVisible("memo") && } + {trailingMaster.map(col => )} ))} diff --git a/frontend/app/(main)/COMPANY_29/purchase/purchase-item/page.tsx b/frontend/app/(main)/COMPANY_29/purchase/purchase-item/page.tsx index 1f7c4137..7f211a88 100644 --- a/frontend/app/(main)/COMPANY_29/purchase/purchase-item/page.tsx +++ b/frontend/app/(main)/COMPANY_29/purchase/purchase-item/page.tsx @@ -617,17 +617,21 @@ export default function PurchaseItemPage() { toast.success("다운로드 완료"); }; - // EDataTable 컬럼 정의 (구매품목) - const itemColumns: EDataTableColumn[] = [ - { key: "item_number", label: "품번", width: "w-[110px]" }, - { key: "item_name", label: "품명", minWidth: "min-w-[130px]" }, - { key: "size", label: "규격", width: "w-[80px]" }, - { key: "unit", label: "단위", width: "w-[60px]" }, - { key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }, - { key: "standard_price", label: "구매단가", width: "w-[90px]", align: "right", formatNumber: true }, - { key: "currency_code", label: "통화", width: "w-[50px]" }, - { key: "status", label: "상태", width: "w-[60px]" }, - ]; + // EDataTable 컬럼 정의 (구매품목) — ts.visibleColumns 기반 + const COLUMN_RENDER_MAP: Record> = { + item_number: { width: "w-[110px]" }, + item_name: { minWidth: "min-w-[130px]" }, + size: { width: "w-[80px]" }, + unit: { width: "w-[60px]" }, + standard_price: { width: "w-[90px]", align: "right", formatNumber: true }, + currency_code: { width: "w-[50px]" }, + status: { width: "w-[60px]" }, + }; + const itemColumns: EDataTableColumn[] = ts.visibleColumns.map((col): EDataTableColumn => ({ + key: col.key, + label: col.label, + ...COLUMN_RENDER_MAP[col.key], + })); return (
diff --git a/frontend/app/(main)/COMPANY_29/purchase/supplier/page.tsx b/frontend/app/(main)/COMPANY_29/purchase/supplier/page.tsx index 51c50aa5..521f770e 100644 --- a/frontend/app/(main)/COMPANY_29/purchase/supplier/page.tsx +++ b/frontend/app/(main)/COMPANY_29/purchase/supplier/page.tsx @@ -12,7 +12,7 @@ * - 납품처 등록 (delivery_destination) */ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -1229,47 +1229,44 @@ export default function SupplierManagementPage() { } }; - // 컬럼 가시성 헬퍼 - const isColumnVisible = (key: string) => ts.isVisible(key); - - const supplierColSpan = 1 + ["supplier_code", "supplier_name", "contact_person", "contact_phone", "division", "status"] - .filter((k) => isColumnVisible(k)).length; - - // EDataTable 컬럼 정의 (공급업체 목록) - const supplierColumns: EDataTableColumn[] = [ - ...(isColumnVisible("supplier_code") ? [{ key: "supplier_code", label: "공급업체코드", width: "w-[120px]" }] : []), - ...(isColumnVisible("supplier_name") ? [{ key: "supplier_name", label: "공급업체명", minWidth: "min-w-[140px]" }] : []), - ...(isColumnVisible("division") ? [{ - key: "division", - label: "공급업체유형", - width: "w-[80px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ...(isColumnVisible("contact_person") ? [{ key: "contact_person", label: "담당자", width: "w-[80px]" }] : []), - ...(isColumnVisible("contact_phone") ? [{ key: "contact_phone", label: "전화번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("email") ? [{ key: "email", label: "이메일", width: "w-[160px]" }] : []), - ...(isColumnVisible("business_number") ? [{ key: "business_number", label: "사업자번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("address") ? [{ key: "address", label: "주소", minWidth: "min-w-[150px]" }] : []), - ...(isColumnVisible("status") ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ]; + // EDataTable 컬럼 정의 (공급업체 목록) — ts.visibleColumns 순서를 따름 + const supplierColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + supplier_code: { width: "w-[120px]" }, + supplier_name: { minWidth: "min-w-[140px]" }, + division: { + width: "w-[80px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + contact_person: { width: "w-[80px]" }, + contact_phone: { width: "w-[120px]" }, + email: { width: "w-[160px]" }, + business_number: { width: "w-[120px]" }, + address: { minWidth: "min-w-[150px]" }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 엑셀 다운로드 const handleExcelDownload = async () => { diff --git a/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx index 5baf35d3..2c0e1338 100644 --- a/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx @@ -28,6 +28,7 @@ const GRID_COLUMNS = [ { key: "item_code", label: "품목코드" }, { key: "item_name", label: "품목명" }, { key: "inspection_type", label: "검사유형" }, + { key: "item_count", label: "항목수" }, { key: "is_active", label: "사용여부" }, ]; const ITEM_TABLE = "item_info"; @@ -420,18 +421,41 @@ export default function ItemInspectionInfoPage() { 0 && checkedIds.length === data.length} onCheckedChange={(v) => setCheckedIds(v ? data.map(r => r.id) : [])} /> - 품목코드 - 품목명 - 검사유형 - 항목수 - 사용여부 + {ts.visibleColumns.map((col) => ( + + {col.label} + + ))} - {groupedData.map((group) => { + {ts.groupData(groupedData).map((group) => { + if ((group as any)._isGroupSummary || (group as any)._isGroupHeader) return null; const isExpanded = expandedItems.has(group.item_code); - const groupIds = group.rows.map(r => r.id); - const allChecked = groupIds.every(id => checkedIds.includes(id)); + const groupIds = group.rows.map((r: any) => r.id); + const allChecked = groupIds.every((id: string) => checkedIds.includes(id)); + const renderCell = (key: string) => { + switch (key) { + case "item_code": return {group.item_code}; + case "item_name": return {group.item_name}; + case "inspection_type": return ( + +
+ {group.types.map((t: string) => {t})} +
+
+ ); + case "item_count": return {group.rows.filter((r: any) => r.inspection_standard_id).length}; + case "is_active": return ( + + + {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} + + + ); + default: return {(group as any)[key] ?? ""}; + } + }; return ( { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !groupIds.includes(id)) : [...new Set([...prev, ...groupIds])]); }}> {}} /> - {group.item_code} - {group.item_name} - -
- {group.types.map(t => {t})} -
-
- {group.rows.filter(r => r.inspection_standard_id).length} - - - {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} - - + {ts.visibleColumns.map((col) => renderCell(col.key))}
- {isExpanded && group.rows.filter(r => r.inspection_standard_id).map((row, i) => ( + {isExpanded && group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( diff --git a/frontend/app/(main)/COMPANY_29/sales/customer/page.tsx b/frontend/app/(main)/COMPANY_29/sales/customer/page.tsx index bddc7730..20b98727 100644 --- a/frontend/app/(main)/COMPANY_29/sales/customer/page.tsx +++ b/frontend/app/(main)/COMPANY_29/sales/customer/page.tsx @@ -12,7 +12,7 @@ * - 납품처 등록 (delivery_destination) */ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -820,12 +820,14 @@ export default function CustomerManagementPage() { const allItems = res.data?.data?.data || res.data?.data?.rows || []; setItemTotalCount(allItems.length); const existingItemIds = new Set(priceItems.map((p: any) => p.item_id || p.item_number)); - const SALES_CODES = ["CAT_ML8ZFVEL_1TOR"]; // 영업관리 카테고리 코드 - setItemSearchResults(allItems.filter((item: any) => { + const seenNumbers = new Set(); + const deduped = allItems.filter((item: any) => { if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false; - const divCodes = (item.division || "").split(",").map((c: string) => c.trim()); - return divCodes.some((code: string) => SALES_CODES.includes(code)); - })); + if (item.item_number && seenNumbers.has(item.item_number)) return false; + if (item.item_number) seenNumbers.add(item.item_number); + return true; + }); + setItemSearchResults(deduped); } catch { /* skip */ } finally { setItemSearchLoading(false); } }; @@ -1229,47 +1231,44 @@ export default function CustomerManagementPage() { } }; - // 컬럼 가시성 헬퍼 - const isColumnVisible = (key: string) => ts.isVisible(key); - - const customerColSpan = 1 + ["customer_code", "customer_name", "contact_person", "contact_phone", "division", "status"] - .filter((k) => isColumnVisible(k)).length; - - // EDataTable 컬럼 정의 (거래처 목록) - const customerColumns: EDataTableColumn[] = [ - ...(isColumnVisible("customer_code") ? [{ key: "customer_code", label: "거래처코드", width: "w-[120px]" }] : []), - ...(isColumnVisible("customer_name") ? [{ key: "customer_name", label: "거래처명", minWidth: "min-w-[140px]" }] : []), - ...(isColumnVisible("division") ? [{ - key: "division", - label: "거래유형", - width: "w-[80px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ...(isColumnVisible("contact_person") ? [{ key: "contact_person", label: "담당자", width: "w-[80px]" }] : []), - ...(isColumnVisible("contact_phone") ? [{ key: "contact_phone", label: "전화번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("email") ? [{ key: "email", label: "이메일", width: "w-[160px]" }] : []), - ...(isColumnVisible("business_number") ? [{ key: "business_number", label: "사업자번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("address") ? [{ key: "address", label: "주소", minWidth: "min-w-[150px]" }] : []), - ...(isColumnVisible("status") ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ]; + // EDataTable 컬럼 정의 (거래처 목록) — ts.visibleColumns 순서를 따름 + const customerColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + customer_code: { width: "w-[120px]" }, + customer_name: { minWidth: "min-w-[140px]" }, + division: { + width: "w-[80px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + contact_person: { width: "w-[80px]" }, + contact_phone: { width: "w-[120px]" }, + email: { width: "w-[160px]" }, + business_number: { width: "w-[120px]" }, + address: { minWidth: "min-w-[150px]" }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 엑셀 다운로드 const handleExcelDownload = async () => { diff --git a/frontend/app/(main)/COMPANY_29/sales/order/page.tsx b/frontend/app/(main)/COMPANY_29/sales/order/page.tsx index f896bbfd..f9a358d4 100644 --- a/frontend/app/(main)/COMPANY_29/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_29/sales/order/page.tsx @@ -13,7 +13,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, ClipboardList, Pencil, Search, X, Truck, Package, - ChevronLeft, ChevronRight, ChevronDown, ChevronsLeft, ChevronsRight, + ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, RotateCcw, Filter, Check, ArrowUp, ArrowDown, } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; @@ -42,41 +42,30 @@ const formatNumber = (val: string) => { }; const parseNumber = (val: string) => val.replace(/,/g, ""); -// 마스터 헤더 레이아웃 (수주번호 뒤, 디테일 11컬럼 위에 colSpan으로 맵핑) -// 순서: 거래처 | 단가방식 | 납품처 | 납품장소 | 수주일 | 담당자 → 합계 colSpan = 11 -const MASTER_BODY_LAYOUT = [ - { key: "partner_id", label: "거래처", colSpan: 2 }, - { key: "price_mode", label: "단가방식", colSpan: 1 }, - { key: "delivery_partner_id", label: "납품처", colSpan: 2 }, - { key: "delivery_address", label: "납품장소", colSpan: 2 }, - { key: "order_date", label: "수주일", colSpan: 2 }, - { key: "manager_id", label: "담당자", colSpan: 2 }, +// 플랫 테이블 컬럼 정의 (마스터+디테일 통합) +const FLAT_COLUMNS = [ + { key: "order_no", label: "수주번호", source: "master" }, + { key: "partner_id", label: "거래처", source: "master" }, + { key: "order_date", label: "수주일", source: "master" }, + { key: "part_code", label: "품번", source: "detail" }, + { key: "part_name", label: "품명", source: "detail" }, + { key: "spec", label: "규격", source: "detail" }, + { key: "unit", label: "단위", source: "detail" }, + { key: "qty", label: "수량", source: "detail" }, + { key: "ship_qty", label: "출하수량", source: "detail" }, + { key: "balance_qty", label: "잔량", source: "detail" }, + { key: "unit_price", label: "단가", source: "detail" }, + { key: "amount", label: "금액", source: "detail" }, + { key: "due_date", label: "납기일", source: "detail" }, + { key: "memo", label: "메모", source: "master" }, ]; -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "part_code", label: "품번" }, - { key: "part_name", label: "품명" }, - { key: "spec", label: "규격" }, - { key: "unit", label: "단위" }, - { key: "qty", label: "수량" }, - { key: "ship_qty", label: "출하수량" }, - { key: "balance_qty", label: "잔량" }, - { key: "unit_price", label: "단가" }, - { key: "amount", label: "금액" }, - { key: "currency_code", label: "통화" }, - { key: "due_date", label: "납기일" }, -]; +const DETAIL_HEADER_COLS = FLAT_COLUMNS.filter((c) => c.source === "detail"); // 필터용 전체 키 -const GRID_COLUMNS_CONFIG = [ - { key: "order_no", label: "수주번호" }, - ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label })), - ...DETAIL_HEADER_COLS, - { key: "memo", label: "메모" }, -]; +const GRID_COLUMNS_CONFIG = FLAT_COLUMNS.map(({ key, label }) => ({ key, label })); -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 수주번호(1) + 디테일(11) + 메모(1) = 15 +// 총 컬럼 수: 체크박스(1) + 플랫 컬럼(14) = 15 const TOTAL_COLS = 15; // 헤더 필터 Popover @@ -180,8 +169,6 @@ export default function SalesOrderPage() { const [masterForm, setMasterForm] = useState>({}); const [detailRows, setDetailRows] = useState([]); const [allowPriceEdit, setAllowPriceEdit] = useState(true); - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); // 품목 선택 모달 const [itemSelectOpen, setItemSelectOpen] = useState(false); @@ -376,25 +363,8 @@ export default function SalesOrderPage() { useEffect(() => { fetchOrders(); }, [fetchOrders]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) - const columnUniqueValues = useMemo(() => { - const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { - const values = new Set(); - orders.forEach((row) => { - const val = row[col.key]; - if (val !== null && val !== undefined && val !== "") values.add(String(val)); - }); - result[col.key] = Array.from(values).sort(); - } - return result; - }, [orders]); - - // 마스터 필드 키 목록 (필터 분류용) - const MASTER_KEYS = new Set(["order_no", ...MASTER_BODY_LAYOUT.map((c) => c.key), "memo"]); - - // 카테고리 코드→라벨 변환 (마스터 필터용) - const resolveMasterLabel = useCallback((key: string, code: string) => { + // 카테고리 코드→라벨 변환 + const resolveLabel = useCallback((key: string, code: string) => { if (!code) return ""; if (key === "partner_id" || key === "manager_id" || key === "price_mode") { return categoryOptions[key]?.find((o) => o.code === code)?.label || code; @@ -402,106 +372,60 @@ export default function SalesOrderPage() { return code; }, [categoryOptions]); - // 필터 + 정렬 적용된 데이터 → 그룹핑 - const filteredOrderGroups = useMemo(() => { - // 1차: order_no 기준 그룹핑 (필터 전) - const allGroups: Record = {}; - for (const row of orders) { - const key = row.order_no || "_no_order"; - if (!allGroups[key]) { - allGroups[key] = { master: row._master || {}, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위 필터링) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - const raw = group.master?.[colKey] ?? ""; - const label = resolveMasterLabel(colKey, String(raw)); - return values.has(label) || values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위 필터링) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([orderNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - const cellVal = row[colKey] != null ? String(row[colKey]) : ""; - return values.has(cellVal); - }) - ); - return [orderNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - // 마스터 필드 정렬 → 그룹 단위 - entries.sort(([, a], [, b]) => { - const av = a.master?.[key] ?? ""; - const bv = b.master?.[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - // 디테일 필드 정렬 → 각 그룹 내 디테일 정렬 - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - const av = a[key] ?? ""; - const bv = b[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [orders, headerFilters, sortState, resolveMasterLabel]); - - // 마스터 컬럼별 고유값 (마스터 헤더 필터용) - const masterUniqueValues = useMemo(() => { - const result: Record = {}; - // 필터 전 전체 마스터에서 고유값 추출 - const seenMasters = new Map(); - orders.forEach((row) => { - if (row.order_no && row._master && !seenMasters.has(row.order_no)) { - seenMasters.set(row.order_no, row._master); - } + // 플랫 행 생성 (마스터 필드를 각 디테일 행에 병합) + const flatRows = useMemo(() => { + return orders.map((row) => { + const master = row._master || {}; + return { + ...row, + partner_id: resolveLabel("partner_id", master.partner_id || row.partner_id || ""), + order_date: master.order_date || row.order_date || "", + memo: row.memo || master.memo || "", + }; }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "order_no", label: "수주번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label })), { key: "memo", label: "메모" }]) { + }, [orders, resolveLabel]); + + // 컬럼별 고유값 (헤더 필터용) + const columnUniqueValues = useMemo(() => { + const result: Record = {}; + for (const col of FLAT_COLUMNS) { const values = new Set(); - masters.forEach((m) => { - const val = m?.[col.key]; - if (val !== null && val !== undefined && val !== "") { - values.add(resolveMasterLabel(col.key, String(val))); - } + flatRows.forEach((row) => { + const val = row[col.key]; + if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [orders, resolveMasterLabel]); + }, [flatRows]); + + // 필터 + 정렬 적용된 플랫 데이터 + const filteredFlatRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = row[colKey] != null ? String(row[colKey]) : ""; + return values.has(cellVal); + }); + } + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = a[key] ?? ""; + const bv = b[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -965,111 +889,70 @@ export default function SalesOrderPage() {
- {/* 데이터 테이블 (트리 구조) */} + {/* 데이터 테이블 (플랫 리스트) */}
- {/* 체크박스 */} - {/* 펼침 화살표 */} - {/* 수주번호 */} - {/* 품번 / 거래처 */} - {/* 품명 / 거래처(cont) */} - {/* 규격 / 단가방식 */} - {/* 단위 / 납품처 */} - {/* 수량 / 납품처(cont) */} - {/* 출하수량 / 납품장소 */} - {/* 잔량 / 납품장소(cont) */} - {/* 단가 / 수주일 */} - {/* 금액 / 수주일(cont) */} - {/* 통화 / 담당자 */} - {/* 납기일 / 담당자(cont) */} - {/* 메모 */} + + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredOrderGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredFlatRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredOrderGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredFlatRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 수주번호 (별도 컬럼) */} - -
-
handleSort("order_no")}> - 수주번호 - {sortState?.key === "order_no" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["order_no"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (colSpan으로 디테일 컬럼 위에 맵핑) */} - {MASTER_BODY_LAYOUT.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {FLAT_COLUMNS.map((col) => { + const isRight = ["qty", "ship_qty", "balance_qty", "unit_price", "amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} - {/* 메모 (마스터) */} - -
-
handleSort("memo")}> - 메모 - {sortState?.key === "memo" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["memo"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
+ + ); + })} @@ -1079,7 +962,7 @@ export default function SalesOrderPage() { - ) : Object.keys(filteredOrderGroups).length === 0 ? ( + ) : filteredFlatRows.length === 0 ? (
@@ -1089,200 +972,48 @@ export default function SalesOrderPage() { ) : ( - Object.entries(filteredOrderGroups).map(([orderNo, group]) => { - const isExpanded = expandedOrders.has(orderNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredFlatRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 — 마스터 테이블 필드만 표시 */} - { - if (expandedOrders.has(orderNo)) { - setClosingOrders((prev) => new Set(prev).add(orderNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(orderNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(orderNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(orderNo)); - } - }} - onDoubleClick={() => openEditModal(orderNo)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 수주번호 */} - - {orderNo} - ({group.details.length}) - - {/* 거래처 (colSpan=2) */} - - - {master.partner_id ? (categoryOptions["partner_id"]?.find((o) => o.code === master.partner_id)?.label || master.partner_id) : ""} - - - {/* 단가방식 (colSpan=1) */} - - - {master.price_mode ? (categoryOptions["price_mode"]?.find((o) => o.code === master.price_mode)?.label || master.price_mode) : ""} - - - {/* 납품처 (colSpan=2) */} - - {master.delivery_partner_id || ""} - - {/* 납품장소 (colSpan=2) */} - - {master.delivery_address || ""} - - {/* 수주일 (colSpan=2) */} - - {master.order_date || ""} - - {/* 담당자 (colSpan=2) */} - - - {master.manager_id ? (categoryOptions["manager_id"]?.find((o) => o.code === master.manager_id)?.label || master.manager_id) : ""} - - - {/* 메모 */} - - {master.memo || ""} - - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - {/* 수주번호 컬럼 빈 셀 */} - {DETAIL_HEADER_COLS.map((col) => { - const isRight = ["qty", "ship_qty", "balance_qty", "unit_price", "amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => d[col.key]).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} - -
+ { - const isClosing = closingOrders.has(orderNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - onDoubleClick={() => openEditModal(row.order_no)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - {/* 수주번호 컬럼 빈 셀 */} - {row.part_code} - {row.part_name} - {row.spec} - {row.unit} - {row.qty ? Number(row.qty).toLocaleString() : ""} - {row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""} - {row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {row.amount ? Number(row.amount).toLocaleString() : ""} - {row.currency_code || ""} - {row.due_date || ""} - - + onClick={() => { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] ); - })} - + }} + onDoubleClick={() => openEditModal(row.order_no)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + > + {}} /> + + {row.order_no} + {row.partner_id || ""} + {row.order_date || ""} + {row.part_code} + {row.part_name} + {row.spec} + {row.unit} + {row.qty ? Number(row.qty).toLocaleString() : ""} + {row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""} + {row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.amount ? Number(row.amount).toLocaleString() : ""} + {row.due_date || ""} + {row.memo || ""} + ); }) )} diff --git a/frontend/app/(main)/COMPANY_29/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_29/sales/shipping-order/page.tsx index 4ab5a9ad..2ed29b40 100644 --- a/frontend/app/(main)/COMPANY_29/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_29/sales/shipping-order/page.tsx @@ -363,7 +363,7 @@ export default function ShippingOrderPage() { spec: item.spec, material: item.material, orderQty: item.orderQty, - planQty: item.planQty, + planQty: item.orderQty, shipQty: 0, sourceType: item.sourceType, shipmentPlanId: item.shipmentPlanId, diff --git a/frontend/app/(main)/COMPANY_30/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_30/equipment/info/page.tsx index eeb63844..cd53e9b1 100644 --- a/frontend/app/(main)/COMPANY_30/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_30/equipment/info/page.tsx @@ -142,15 +142,20 @@ export default function EquipmentInfoPage() { }; const mainTableColumns = useMemo(() => { - const cols: EDataTableColumn[] = []; - if (ts.isVisible("equipment_code")) cols.push({ key: "equipment_code", label: "설비코드", width: "w-[110px]" }); - if (ts.isVisible("equipment_name")) cols.push({ key: "equipment_name", label: "설비명", minWidth: "min-w-[130px]", truncate: true, render: (v) => v || "-" }); - if (ts.isVisible("equipment_type")) cols.push({ key: "equipment_type", label: "설비유형", width: "w-[90px]", render: (v) => v || "-" }); - if (ts.isVisible("manufacturer")) cols.push({ key: "manufacturer", label: "제조사", width: "w-[100px]", render: (v) => v || "-" }); - if (ts.isVisible("installation_location")) cols.push({ key: "installation_location", label: "설치장소", width: "w-[100px]", render: (v) => v || "-" }); - if (ts.isVisible("operation_status")) cols.push({ key: "operation_status", label: "가동상태", width: "w-[80px]", render: (v) => v || "-" }); - return cols; - }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + 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 || "-" }, + manufacturer: { width: "w-[100px]", render: (v) => v || "-" }, + installation_location: { width: "w-[100px]", render: (v) => v || "-" }, + operation_status: { width: "w-[80px]", render: (v) => v || "-" }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 설비 조회 const fetchEquipments = useCallback(async () => { @@ -272,8 +277,8 @@ export default function EquipmentInfoPage() { if (!inspectionForm.inspection_cycle) { toast.error("점검주기는 필수입니다."); return; } if (!inspectionForm.inspection_method) { toast.error("점검방법은 필수입니다."); return; } const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; - if (isNumeric && !inspectionForm.unit) { toast.error("숫자 점검방법은 측정단위가 필수입니다."); return; } + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); + if (isNumeric && !inspectionForm.unit) { toast.error("측정단위가 필수입니다."); return; } // 기준값/오차범위 → 하한치/상한치 자동 계산 const saveData = { ...inspectionForm }; if (isNumeric && saveData.standard_value) { @@ -739,7 +744,7 @@ export default function EquipmentInfoPage() {
{catSelect("inspection_method", inspectionForm.inspection_method, (v) => { const label = resolve("inspection_method", v); - const isNum = label === "숫자" || v === "숫자"; + const isNum = ["숫자", "치수검사"].includes(label) || ["숫자", "치수검사"].includes(v); if (!isNum) { setInspectionForm((p) => ({ ...p, inspection_method: v, unit: "", standard_value: "", tolerance: "", lower_limit: "", upper_limit: "" })); } else { @@ -748,7 +753,7 @@ export default function EquipmentInfoPage() { }, "점검방법")}
{(() => { const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); if (!isNumeric) return null; return (
@@ -758,7 +763,7 @@ export default function EquipmentInfoPage() {
{(() => { const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); if (!isNumeric) return null; return (
diff --git a/frontend/app/(main)/COMPANY_30/logistics/material-status/page.tsx b/frontend/app/(main)/COMPANY_30/logistics/material-status/page.tsx index 46de306c..eb87ba92 100644 --- a/frontend/app/(main)/COMPANY_30/logistics/material-status/page.tsx +++ b/frontend/app/(main)/COMPANY_30/logistics/material-status/page.tsx @@ -333,69 +333,90 @@ export default function MaterialStatusPage() {

) : ( - workOrders.map((wo) => ( -
handleSelectWo(wo.id)} - > + ts.groupData(workOrders).map((wo) => { + if ((wo as any)._isGroupSummary || (wo as any)._isGroupHeader) return null; + return (
e.stopPropagation()} + key={wo.id} + className={cn( + "flex gap-3 rounded-lg border p-3 transition-all cursor-pointer", + "hover:border-primary/50 hover:shadow-sm", + selectedWoId === wo.id + ? "border-primary bg-primary/5 shadow-sm" + : "border-border" + )} + onClick={() => handleSelectWo(wo.id)} > - - handleCheckWo(wo.id, c as boolean) - } - /> -
-
-
- - {wo.plan_no || wo.work_order_no || `WO-${wo.id}`} - - e.stopPropagation()} + > + + handleCheckWo(wo.id, c as boolean) + } + /> +
+
+
+ {ts.isVisible("plan_no") && ( + + {wo.plan_no || wo.work_order_no || `WO-${wo.id}`} + )} - > - {getStatusLabel(wo.status)} - -
-
- - {wo.item_name} - - - ({wo.item_code}) - -
-
- 수량: - - {Number(wo.plan_qty).toLocaleString()}개 - - | - 일자: - - {wo.plan_date - ? new Date(wo.plan_date) - .toISOString() - .slice(0, 10) - : "-"} - + {ts.isVisible("status") && ( + + {getStatusLabel(wo.status)} + + )} +
+
+ {ts.isVisible("item_name") && ( + + {wo.item_name} + + )} + {ts.isVisible("item_code") && ( + + ({wo.item_code}) + + )} +
+
+ {ts.isVisible("plan_qty") && ( + <> + 수량: + + {Number(wo.plan_qty).toLocaleString()}개 + + + )} + {ts.isVisible("plan_qty") && ts.isVisible("plan_date") && ( + | + )} + {ts.isVisible("plan_date") && ( + <> + 일자: + + {wo.plan_date + ? new Date(wo.plan_date) + .toISOString() + .slice(0, 10) + : "-"} + + + )} +
-
- )) + ); + }) )}
diff --git a/frontend/app/(main)/COMPANY_30/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_30/logistics/outbound/page.tsx index 5ab46a8a..c1ffbd40 100644 --- a/frontend/app/(main)/COMPANY_30/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_30/logistics/outbound/page.tsx @@ -140,8 +140,16 @@ const DETAIL_HEADER_COLS = [ // 마스터 필드 키 목록 (필터 분류용) const MASTER_KEYS = new Set(["outbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 출고번호(1) + 마스터필드(7) = 10 -const TOTAL_COLS = 10; +// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) +const DETAIL_KEY_MAP: Record = { + source_type: "source_type", + item_number: "item_code", + item_name: "item_name", + spec: "specification", + outbound_qty: "outbound_qty", + unit_price: "unit_price", + total_amount: "total_amount", +}; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -248,6 +256,31 @@ interface SelectedSourceItem { export default function OutboundPage() { const ts = useTableSettings("c16-outbound", "outbound_mng", GRID_COLUMNS); + + // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 + const visibleMasterLayout = useMemo(() => { + const ordered: typeof MASTER_BODY_LAYOUT = []; + for (const vc of ts.visibleColumns) { + const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); + if (m) ordered.push(m); + } + return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; + }, [ts.visibleColumns]); + + const visibleDetailCols = useMemo(() => { + const ordered: typeof DETAIL_HEADER_COLS = []; + for (const vc of ts.visibleColumns) { + const detailKey = DETAIL_KEY_MAP[vc.key]; + if (detailKey) { + const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); + if (d) ordered.push(d); + } + } + return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; + }, [ts.visibleColumns]); + + const TOTAL_COLS = 3 + visibleMasterLayout.length; + // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -900,8 +933,15 @@ export default function OutboundPage() {
-
- +
+ + + + + {visibleMasterLayout.map((col) => ( + + ))} + - {/* 마스터 필드 헤더 */} - {MASTER_BODY_LAYOUT.map((col) => ( + {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => (
handleSort(col.key)}> @@ -1039,38 +1079,51 @@ export default function OutboundPage() { {outboundNo} ({group.details.length}) - {/* 출고유형 */} - - - {master.outbound_type || "-"} - - - {/* 출고일 */} - - {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} - - {/* 참조번호 */} - - {master.reference_number || ""} - - {/* 거래처 */} - - {master.customer_name || ""} - - {/* 창고 */} - - {master.warehouse_name || master.warehouse_code || ""} - - {/* 출고상태 */} - - - {master.outbound_status || "-"} - - - {/* 비고 */} - - {master.memo || ""} - + {/* 마스터 필드 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => { + switch (col.key) { + case "outbound_type": return ( + + + {master.outbound_type || "-"} + + + ); + case "outbound_date": return ( + + {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} + + ); + case "reference_number": return ( + + {master.reference_number || ""} + + ); + case "customer_name": return ( + + {master.customer_name || ""} + + ); + case "warehouse_name": return ( + + {master.warehouse_name || master.warehouse_code || ""} + + ); + case "outbound_status": return ( + + + {master.outbound_status || "-"} + + + ); + case "memo": return ( + + {master.memo || ""} + + ); + default: return {(master as any)[col.key] ?? ""}; + } + })} {/* 디테일 서브 헤더 (펼쳤을 때만) */} @@ -1084,7 +1137,7 @@ export default function OutboundPage() { - {DETAIL_HEADER_COLS.map((col) => { + {visibleDetailCols.map((col) => { const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); const isSorted = sortState?.key === col.key; const uniqueVals = Array.from(new Set( @@ -1163,20 +1216,18 @@ export default function OutboundPage() {
- {/* 출처 */} - {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"} - {/* 품목코드 */} - {row.item_code || ""} - {/* 품목명 */} - {row.item_name || ""} - {/* 규격 */} - {row.specification || ""} - {/* 출고수량 */} - {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""} - {/* 단가 */} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {/* 금액 */} - {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {visibleDetailCols.map((col) => { + switch (col.key) { + case "source_type": return {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"}; + case "item_code": return {row.item_code || ""}; + case "item_name": return {row.item_name || ""}; + case "specification": return {row.specification || ""}; + case "outbound_qty": return {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""}; + case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; + case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; + default: return {(row as any)[col.key] ?? ""}; + } + })} ); })} diff --git a/frontend/app/(main)/COMPANY_30/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_30/logistics/packaging/page.tsx index 5d4d5787..6ae340aa 100644 --- a/frontend/app/(main)/COMPANY_30/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_30/logistics/packaging/page.tsx @@ -460,18 +460,20 @@ export default function PackagingPage() { {/* 포장재 목록 테이블 */}
PKG_TYPE_LABEL[v] || v || "-" }, - { key: "size", label: "크기(mm)", width: "w-[100px]", render: (_v, row) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, - { key: "max_load_kg", label: "최대중량", width: "w-[80px]", align: "right", render: (v) => Number(v || 0) > 0 ? `${v}kg` : "-" }, - { key: "status", label: "상태", width: "w-[60px]", align: "center", render: (v) => ( - - {STATUS_LABEL[v] || v} - - )}, - ] as EDataTableColumn[]} + columns={ts.visibleColumns.map((col): EDataTableColumn => { + const renderMap: Record>> = { + pkg_type: { width: "w-[80px]", render: (v: any) => PKG_TYPE_LABEL[v] || v || "-" }, + size: { width: "w-[100px]", render: (_v: any, row: any) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, + max_weight: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, + max_load_kg: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, + status: { width: "w-[60px]", align: "center", render: (v: any) => ( + + {STATUS_LABEL[v] || v} + + )}, + }; + return { key: col.key, label: col.label, ...renderMap[col.key] }; + })} data={ts.groupData(filteredPkgUnits)} rowKey={(row) => String(row.id)} loading={pkgLoading} diff --git a/frontend/app/(main)/COMPANY_30/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_30/logistics/receiving/page.tsx index a8d5fc2c..85cdc23c 100644 --- a/frontend/app/(main)/COMPANY_30/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_30/logistics/receiving/page.tsx @@ -117,12 +117,20 @@ const DETAIL_HEADER_COLS = [ { key: "total_amount", label: "금액" }, ]; -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 입고번호(1) + 디테일(7) = 10 -const TOTAL_COLS = 10; - // 마스터 필드 키 목록 (필터 분류용) const MASTER_KEYS = new Set(["inbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); +// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) +const DETAIL_KEY_MAP: Record = { + source_type: "source_table", + item_number: "item_number", + item_name: "item_name", + spec: "spec", + inbound_qty: "inbound_qty", + unit_price: "unit_price", + total_amount: "total_amount", +}; + // 헤더 필터 Popover function HeaderFilterPopover({ colKey, colLabel, uniqueValues, filterValues, onToggle, onClear, @@ -278,6 +286,31 @@ interface SelectedSourceItem { export default function ReceivingPage() { const ts = useTableSettings("c16-receiving", "inbound_mng", GRID_COLUMNS); + + // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 + const visibleMasterLayout = useMemo(() => { + const ordered: typeof MASTER_BODY_LAYOUT = []; + for (const vc of ts.visibleColumns) { + const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); + if (m) ordered.push(m); + } + return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; + }, [ts.visibleColumns]); + + const visibleDetailCols = useMemo(() => { + const ordered: typeof DETAIL_HEADER_COLS = []; + for (const vc of ts.visibleColumns) { + const detailKey = DETAIL_KEY_MAP[vc.key]; + if (detailKey) { + const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); + if (d) ordered.push(d); + } + } + return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; + }, [ts.visibleColumns]); + + const TOTAL_COLS = 3 + visibleMasterLayout.length; + // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -847,8 +880,15 @@ export default function ReceivingPage() {
-
- +
+ + + + + {visibleMasterLayout.map((col) => ( + + ))} + - {/* 마스터 필드 헤더 (colSpan으로 디테일 컬럼 위에 맵핑) */} - {MASTER_BODY_LAYOUT.map((col) => ( + {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => (
handleSort(col.key)}> @@ -985,38 +1025,51 @@ export default function ReceivingPage() { {inboundNo} ({group.details.length}) - {/* 입고유형 */} - - - {resolveInboundType(master.inbound_type)} - - - {/* 입고일 */} - - {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} - - {/* 참조번호 */} - - {master.reference_number || ""} - - {/* 공급처 */} - - {master.supplier_name || ""} - - {/* 창고 */} - - {master.warehouse_name || master.warehouse_code || ""} - - {/* 입고상태 */} - - - {master.inbound_status || "-"} - - - {/* 비고 */} - - {master.memo || ""} - + {/* 마스터 필드 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => { + switch (col.key) { + case "inbound_type": return ( + + + {resolveInboundType(master.inbound_type)} + + + ); + case "inbound_date": return ( + + {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} + + ); + case "reference_number": return ( + + {master.reference_number || ""} + + ); + case "supplier_name": return ( + + {master.supplier_name || ""} + + ); + case "warehouse_name": return ( + + {master.warehouse_name || master.warehouse_code || ""} + + ); + case "inbound_status": return ( + + + {master.inbound_status || "-"} + + + ); + case "memo": return ( + + {master.memo || ""} + + ); + default: return {(master as any)[col.key] ?? ""}; + } + })} {/* 디테일 서브 헤더 (펼쳤을 때만) */} @@ -1030,7 +1083,7 @@ export default function ReceivingPage() { - {DETAIL_HEADER_COLS.map((col) => { + {visibleDetailCols.map((col) => { const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); const isSorted = sortState?.key === col.key; const uniqueVals = Array.from(new Set( @@ -1108,20 +1161,18 @@ export default function ReceivingPage() {
- {/* 출처 */} - {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"} - {/* 품목코드 */} - {row.item_number || ""} - {/* 품목명 */} - {row.item_name || ""} - {/* 규격 */} - {row.spec || ""} - {/* 입고수량 */} - {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""} - {/* 단가 */} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {/* 금액 */} - {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {visibleDetailCols.map((col) => { + switch (col.key) { + case "source_table": return {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"}; + case "item_number": return {row.item_number || ""}; + case "item_name": return {row.item_name || ""}; + case "spec": return {row.spec || ""}; + case "inbound_qty": return {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""}; + case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; + case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; + default: return {(row as any)[col.key] ?? ""}; + } + })} ); })} 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 dfd1b666..9d7f2dea 100644 --- a/frontend/app/(main)/COMPANY_30/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_30/master-data/company/page.tsx @@ -491,12 +491,6 @@ export default function CompanyPage() { > 회사정보 - - 부서관리 -
@@ -635,89 +629,6 @@ export default function CompanyPage() {
- {/* ===================== Tab 2: 부서관리 ===================== */} - -
- - {/* 좌측: 부서 트리 */} - -
-
-
- - 부서 - {depts.length}건 -
-
- - - -
-
-
- {deptLoading ? ( -
- -
- ) : deptTree.length === 0 ? ( -
- - 등록된 부서가 없어요 -
- ) : ( - renderTree(deptTree) - )} -
-
-
- - - - {/* 우측: 사원 목록 */} - -
-
-
- - {selectedDept ? "부서 인원" : "부서를 선택해주세요"} - {selectedDept && {selectedDept.dept_name}} - {members.length > 0 && {members.length}명} -
- {selectedDeptCode && ( - - )} -
- {selectedDeptCode ? ( - row.user_id || row.id} - loading={memberLoading} - emptyMessage="소속 사원이 없어요" - emptyIcon={} - onRowDoubleClick={(row) => openUserModal(row)} - showPagination={false} - draggableColumns={false} - /> - ) : ( -
- - 좌측에서 부서를 선택해주세요 -
- )} -
-
-
-
-
{/* ── 부서 등록/수정 모달 ── */} diff --git a/frontend/app/(main)/COMPANY_30/master-data/department/page.tsx b/frontend/app/(main)/COMPANY_30/master-data/department/page.tsx index a2bbcba5..3245571e 100644 --- a/frontend/app/(main)/COMPANY_30/master-data/department/page.tsx +++ b/frontend/app/(main)/COMPANY_30/master-data/department/page.tsx @@ -9,7 +9,7 @@ * 모달: 부서 등록(dept_info), 사원 추가(user_info) */ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -279,6 +279,7 @@ export default function DepartmentPage() { dept_code: userForm.dept_code || undefined, dept_name: depts.find((d) => d.dept_code === userForm.dept_code)?.dept_name || undefined, status: userForm.status || "active", + end_date: userForm.end_date || null, }, mainDept: userForm.dept_code ? { dept_code: userForm.dept_code, @@ -312,37 +313,40 @@ export default function DepartmentPage() { const activeMembers = members.filter((m) => !m.end_date || m.end_date.substring(0, 10) >= today); const resignedMembers = members.filter((m) => m.end_date && m.end_date.substring(0, 10) < today); - const isColVisible = (key: string) => ts.isVisible(key); - - // EDataTable 컬럼 정의 (부서 목록) - const deptColumns: EDataTableColumn[] = [ - { key: "dept_code", label: "부서코드", width: "w-[120px]" }, - { key: "dept_name", label: "부서명", minWidth: "min-w-[140px]" }, - ...(isColVisible("parent_dept_code") - ? [{ - key: "parent_dept_code", - label: "상위부서", - width: "w-[110px]", - render: (val: any) => {val || "\u2014"}, - }] - : []), - ...(isColVisible("status") - ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val === "active" ? "활성" : (val || "\u2014")} - - ) : null, - }] - : []), - ]; + // EDataTable 컬럼 정의 (부서 목록) — ts.visibleColumns 순서를 따름 + const deptColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + dept_code: { width: "w-[120px]" }, + dept_name: { minWidth: "min-w-[140px]" }, + parent_dept_code: { + width: "w-[110px]", + render: (val: any) => {val || "\u2014"}, + }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val === "active" ? "활성" : (val || "\u2014")} + + ) : null, + }, + }; + // dept_code, dept_name은 항상 표시 (DEPT_COLUMNS에 포함되지 않으므로 visibleColumns에 없음) + const fixedCols: EDataTableColumn[] = [ + { key: "dept_code", label: "부서코드", ...colProps["dept_code"] }, + { key: "dept_name", label: "부서명", ...colProps["dept_name"] }, + ]; + const dynamicCols = ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + return [...fixedCols, ...dynamicCols]; + }, [ts.visibleColumns]); return (
diff --git a/frontend/app/(main)/COMPANY_30/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_30/master-data/item-info/page.tsx index 3f037275..375fd900 100644 --- a/frontend/app/(main)/COMPANY_30/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_30/master-data/item-info/page.tsx @@ -84,6 +84,56 @@ function CategoryCombobox({ options, value, onChange, placeholder }: { ); } +// 다중 선택 카테고리 콤보박스 +function MultiCategoryCombobox({ options, value, onChange, placeholder }: { + options: { code: string; label: string }[]; + value: string; + onChange: (v: string) => void; + placeholder: string; +}) { + const [open, setOpen] = useState(false); + const selectedCodes = value ? value.split(",").map((c) => c.trim()).filter(Boolean) : []; + const selectedLabels = selectedCodes.map((code) => options.find((o) => o.code === code)?.label || code).filter(Boolean); + + const toggle = (code: string) => { + const next = selectedCodes.includes(code) + ? selectedCodes.filter((c) => c !== code) + : [...selectedCodes, code]; + onChange(next.join(",")); + }; + + return ( + + + + + + + + + 검색 결과가 없어요 + + {options.map((opt) => ( + toggle(opt.code)}> + + {opt.label} + + ))} + + + + + + ); +} + const TABLE_NAME = "item_info"; const GRID_COLUMNS = [ @@ -108,7 +158,7 @@ const GRID_COLUMNS = [ const FORM_FIELDS = [ { key: "item_number", label: "품목코드", type: "text", required: true, disabled: true, placeholder: "자동 채번" }, { key: "item_name", label: "품명", type: "text", required: true }, - { key: "division", label: "관리품목", type: "category" }, + { key: "division", label: "관리품목", type: "multi-category" }, { key: "type", label: "품목구분", type: "category" }, { key: "size", label: "규격", type: "text" }, { key: "unit", label: "단위", type: "category" }, @@ -137,6 +187,7 @@ export default function ItemInfoPage() { const { user } = useAuth(); const ts = useTableSettings("c16-item-info", TABLE_NAME, GRID_COLUMNS); const [items, setItems] = useState([]); + const [rawItems, setRawItems] = useState([]); const [loading, setLoading] = useState(false); // 검색 필터 (DynamicSearchFilter) @@ -215,6 +266,7 @@ export default function ItemInfoPage() { } return categoryOptions[col]?.find((o) => o.code === code)?.label || code; }; + setRawItems(raw); const data = raw.map((r: any) => { const converted = { ...r }; for (const col of CATEGORY_COLUMNS) { @@ -261,7 +313,8 @@ export default function ItemInfoPage() { // 수정 모달 열기 const openEditModal = (item: any) => { - setFormData({ ...item }); + const raw = rawItems.find((r) => r.id === item.id) || item; + setFormData({ ...raw }); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); @@ -269,7 +322,8 @@ export default function ItemInfoPage() { // 복사 모달 열기 const openCopyModal = async (item: any) => { - const { id, item_number, created_date, updated_date, writer, ...rest } = item; + const raw = rawItems.find((r) => r.id === item.id) || item; + const { id, item_number, created_date, updated_date, writer, ...rest } = raw; setFormData(rest); setIsEditMode(false); setEditId(null); @@ -459,6 +513,13 @@ export default function ItemInfoPage() { columnName={field.key} height="h-32" /> + ) : field.type === "multi-category" ? ( + setFormData((prev) => ({ ...prev, [field.key]: v }))} + placeholder={`${field.label} 선택`} + /> ) : field.type === "category" ? ( (() => { - const cols: EDataTableColumn[] = []; - if (ts.isVisible("item_number")) cols.push({ key: "item_number", label: "품번", width: "w-[110px]" }); - if (ts.isVisible("item_name")) cols.push({ key: "item_name", label: "품명", minWidth: "min-w-[130px]", render: (v) => v || "-" }); - if (ts.isVisible("size")) cols.push({ key: "size", label: "규격", width: "w-[90px]", render: (v) => v || "-" }); - if (ts.isVisible("unit")) cols.push({ key: "unit", label: "단위", width: "w-[60px]", render: (v) => v || "-" }); - if (ts.isVisible("standard_price")) cols.push({ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }); - if (ts.isVisible("selling_price")) cols.push({ key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true }); - if (ts.isVisible("currency_code")) cols.push({ key: "currency_code", label: "통화", width: "w-[50px]", render: (v) => v || "-" }); - if (ts.isVisible("status")) cols.push({ key: "status", label: "상태", width: "w-[60px]", render: (v) => v || "-" }); - return cols; - }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + const colProps: Record> = { + item_number: { width: "w-[110px]" }, + item_name: { minWidth: "min-w-[130px]", render: (v) => v || "-" }, + size: { width: "w-[90px]", render: (v) => v || "-" }, + unit: { width: "w-[60px]", render: (v) => v || "-" }, + standard_price: { width: "w-[90px]", align: "right", formatNumber: true }, + selling_price: { width: "w-[90px]", align: "right", formatNumber: true }, + currency_code: { width: "w-[50px]", render: (v) => v || "-" }, + status: { width: "w-[60px]", render: (v) => v || "-" }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 좌측: 품목 조회 (division이 "외주관리"인 품목만 필터링) const outsourcingDivisionCode = categoryOptions["division"]?.find( diff --git a/frontend/app/(main)/COMPANY_30/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_30/production/plan-management/page.tsx index 4a0d341a..6bd63176 100644 --- a/frontend/app/(main)/COMPANY_30/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_30/production/plan-management/page.tsx @@ -919,9 +919,7 @@ export default function ProductionPlanManagementPage() { // 숫자 포맷 const formatNumber = (num: number | string) => Number(num).toLocaleString(); - // 컬럼 표시 여부 - const isColVisible = (key: string) => ts.isVisible(key); - const orderColSpan = 4 + ORDER_COLUMNS.filter((c) => isColVisible(c.key)).length; + // (컬럼 표시는 ts.visibleColumns 순서를 따름) return (
@@ -1019,6 +1017,38 @@ export default function ProductionPlanManagementPage() {
) : (
+ {(() => { + // 디테일 행에서 개별 값을 표시하는 컬럼 매핑 + const DETAIL_VALUE_MAP: Record = { + total_order_qty: "order_qty", + total_ship_qty: "ship_qty", + total_balance_qty: "balance_qty", + }; + + // 그룹 행에서 특수 렌더링이 필요한 컬럼 + const renderGroupCell = (col: { key: string }, item: any) => { + if (col.key === "required_plan_qty") { + return ( + 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> + {formatNumber(item.required_plan_qty)} + + ); + } + if (col.key === "lead_time") { + return ( + toggleItemExpand(item.item_code)}> + {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"} + + ); + } + return ( + toggleItemExpand(item.item_code)}> + {formatNumber(item[col.key])} + + ); + }; + + return (
@@ -1028,15 +1058,11 @@ export default function ProductionPlanManagementPage() { 품목코드 품목명 - {isColVisible("total_order_qty") && 총수주량} - {isColVisible("total_ship_qty") && 출고량} - {isColVisible("total_balance_qty") && 잔량} - {isColVisible("current_stock") && 현재고} - {isColVisible("safety_stock") && 안전재고} - {isColVisible("existing_plan_qty") && 기생산계획량} - {isColVisible("in_progress_qty") && 생산진행} - {isColVisible("required_plan_qty") && 필요생산계획} - {isColVisible("lead_time") && 리드타임(일)} + {ts.visibleColumns.map((col) => ( + + {col.label} + + ))} @@ -1046,6 +1072,7 @@ export default function ProductionPlanManagementPage() { + {ts.visibleColumns.map((col) => { const v = (item as any)[col.key]; return ( @@ -1068,25 +1095,14 @@ export default function ProductionPlanManagementPage() { toggleItemExpand(item.item_code)}>{item.item_code} toggleItemExpand(item.item_code)}>{item.item_name} - {isColVisible("total_order_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_order_qty)}} - {isColVisible("total_ship_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_ship_qty)}} - {isColVisible("total_balance_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_balance_qty)}} - {isColVisible("current_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.current_stock)}} - {isColVisible("safety_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.safety_stock)}} - {isColVisible("existing_plan_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.existing_plan_qty)}} - {isColVisible("in_progress_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.in_progress_qty)}} - {isColVisible("required_plan_qty") && ( - 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> - {formatNumber(item.required_plan_qty)} - - )} - {isColVisible("lead_time") && ( - toggleItemExpand(item.item_code)}> - {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"} - - )} + {ts.visibleColumns.map((col) => renderGroupCell(col, item))} - {expandedItems.has(item.item_code) && item.orders?.map((detail) => ( + {expandedItems.has(item.item_code) && item.orders?.map((detail: any) => { + let remainColSpan = 0; + for (const col of ts.visibleColumns) { + if (!DETAIL_VALUE_MAP[col.key]) remainColSpan++; + } + return ( @@ -1101,19 +1117,28 @@ export default function ProductionPlanManagementPage() { - {isColVisible("total_order_qty") && {formatNumber(detail.order_qty)}} - {isColVisible("total_ship_qty") && {formatNumber(detail.ship_qty)}} - {isColVisible("total_balance_qty") && {formatNumber(detail.balance_qty)}} - - 납기일: {detail.due_date || "-"} - + {ts.visibleColumns.map((col) => { + const detailKey = DETAIL_VALUE_MAP[col.key]; + if (detailKey) { + return {formatNumber(detail[detailKey])}; + } + return null; + })} + {remainColSpan > 0 && ( + + 납기일: {detail.due_date || "-"} + + )} - ))} + ); + })} ); })}
+ ); + })()} )} diff --git a/frontend/app/(main)/COMPANY_30/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_30/purchase/order/page.tsx index fa0e08c5..1bc3bc88 100644 --- a/frontend/app/(main)/COMPANY_30/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_30/purchase/order/page.tsx @@ -742,10 +742,24 @@ export default function PurchaseOrderPage() { ) : ( (() => { const MASTER_KEYS = new Set(["purchase_no", "order_date", "supplier_name", "status", "memo"]); - const detailCols = ts.visibleColumns.filter(c => !MASTER_KEYS.has(c.key)); - const masterCols = ts.visibleColumns.filter(c => MASTER_KEYS.has(c.key)); const numCols = new Set(["order_qty", "received_qty", "remain_qty", "unit_price", "amount"]); + // ts.visibleColumns 순서를 따르되, 마스터/디테일 컬럼을 분리 + // 고정 컬럼(품목수)은 마스터 선행 컬럼 뒤에 배치 + const leadingMaster: typeof ts.visibleColumns = []; + const detailCols: typeof ts.visibleColumns = []; + const trailingMaster: typeof ts.visibleColumns = []; + let passedFirstDetail = false; + for (const col of ts.visibleColumns) { + if (MASTER_KEYS.has(col.key)) { + if (passedFirstDetail) trailingMaster.push(col); + else leadingMaster.push(col); + } else { + passedFirstDetail = true; + detailCols.push(col); + } + } + const renderDetailCell = (row: any, key: string) => { const val = row[key]; if (key === "status") return val ? {val} : "-"; @@ -753,23 +767,35 @@ export default function PurchaseOrderPage() { return val || "-"; }; + const renderMasterHead = (col: { key: string; label: string }) => ( + + {col.label} + + ); + + const renderMasterCell = (col: { key: string }, m: any, purchaseNo: string) => { + if (col.key === "purchase_no") return {purchaseNo}; + if (col.key === "order_date") return {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}; + if (col.key === "supplier_name") return {m.supplier_name || "-"}; + if (col.key === "status") return {m.status && {m.status}}; + if (col.key === "memo") return {m.memo || ""}; + return ; + }; + return ( - {ts.isVisible("purchase_no") && 발주번호} - {ts.isVisible("order_date") && 발주일} - {ts.isVisible("supplier_name") && 공급업체} + {leadingMaster.map(renderMasterHead)} 품목수 {detailCols.map(col => ( {col.label}{col.key === "order_qty" || col.key === "amount" ? " 합계" : ""} ))} - {ts.isVisible("status") && 상태} - {ts.isVisible("memo") && 메모} + {trailingMaster.map(renderMasterHead)} @@ -795,9 +821,7 @@ export default function PurchaseOrderPage() { { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !detailIds.includes(id)) : [...new Set([...prev, ...detailIds])]); }}> {}} /> - {ts.isVisible("purchase_no") && {purchaseNo}} - {ts.isVisible("order_date") && {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}} - {ts.isVisible("supplier_name") && {m.supplier_name || "-"}} + {leadingMaster.map(col => renderMasterCell(col, m, purchaseNo))} {group.details.length}건 {detailCols.map(col => ( @@ -806,8 +830,7 @@ export default function PurchaseOrderPage() { : ""} ))} - {ts.isVisible("status") && {m.status && {m.status}}} - {ts.isVisible("memo") && {m.memo || ""}} + {trailingMaster.map(col => renderMasterCell(col, m, purchaseNo))} {isExpanded && group.details.map((row) => ( @@ -815,17 +838,14 @@ export default function PurchaseOrderPage() { { e.stopPropagation(); setCheckedIds(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id]); }}> {}} /> - {ts.isVisible("purchase_no") && } - {ts.isVisible("order_date") && } - {ts.isVisible("supplier_name") && } + {leadingMaster.map(col => )} {detailCols.map(col => ( {renderDetailCell(row, col.key)} ))} - {ts.isVisible("status") && } - {ts.isVisible("memo") && } + {trailingMaster.map(col => )} ))} diff --git a/frontend/app/(main)/COMPANY_30/purchase/purchase-item/page.tsx b/frontend/app/(main)/COMPANY_30/purchase/purchase-item/page.tsx index 1f7c4137..7f211a88 100644 --- a/frontend/app/(main)/COMPANY_30/purchase/purchase-item/page.tsx +++ b/frontend/app/(main)/COMPANY_30/purchase/purchase-item/page.tsx @@ -617,17 +617,21 @@ export default function PurchaseItemPage() { toast.success("다운로드 완료"); }; - // EDataTable 컬럼 정의 (구매품목) - const itemColumns: EDataTableColumn[] = [ - { key: "item_number", label: "품번", width: "w-[110px]" }, - { key: "item_name", label: "품명", minWidth: "min-w-[130px]" }, - { key: "size", label: "규격", width: "w-[80px]" }, - { key: "unit", label: "단위", width: "w-[60px]" }, - { key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }, - { key: "standard_price", label: "구매단가", width: "w-[90px]", align: "right", formatNumber: true }, - { key: "currency_code", label: "통화", width: "w-[50px]" }, - { key: "status", label: "상태", width: "w-[60px]" }, - ]; + // EDataTable 컬럼 정의 (구매품목) — ts.visibleColumns 기반 + const COLUMN_RENDER_MAP: Record> = { + item_number: { width: "w-[110px]" }, + item_name: { minWidth: "min-w-[130px]" }, + size: { width: "w-[80px]" }, + unit: { width: "w-[60px]" }, + standard_price: { width: "w-[90px]", align: "right", formatNumber: true }, + currency_code: { width: "w-[50px]" }, + status: { width: "w-[60px]" }, + }; + const itemColumns: EDataTableColumn[] = ts.visibleColumns.map((col): EDataTableColumn => ({ + key: col.key, + label: col.label, + ...COLUMN_RENDER_MAP[col.key], + })); return (
diff --git a/frontend/app/(main)/COMPANY_30/purchase/supplier/page.tsx b/frontend/app/(main)/COMPANY_30/purchase/supplier/page.tsx index 51c50aa5..521f770e 100644 --- a/frontend/app/(main)/COMPANY_30/purchase/supplier/page.tsx +++ b/frontend/app/(main)/COMPANY_30/purchase/supplier/page.tsx @@ -12,7 +12,7 @@ * - 납품처 등록 (delivery_destination) */ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -1229,47 +1229,44 @@ export default function SupplierManagementPage() { } }; - // 컬럼 가시성 헬퍼 - const isColumnVisible = (key: string) => ts.isVisible(key); - - const supplierColSpan = 1 + ["supplier_code", "supplier_name", "contact_person", "contact_phone", "division", "status"] - .filter((k) => isColumnVisible(k)).length; - - // EDataTable 컬럼 정의 (공급업체 목록) - const supplierColumns: EDataTableColumn[] = [ - ...(isColumnVisible("supplier_code") ? [{ key: "supplier_code", label: "공급업체코드", width: "w-[120px]" }] : []), - ...(isColumnVisible("supplier_name") ? [{ key: "supplier_name", label: "공급업체명", minWidth: "min-w-[140px]" }] : []), - ...(isColumnVisible("division") ? [{ - key: "division", - label: "공급업체유형", - width: "w-[80px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ...(isColumnVisible("contact_person") ? [{ key: "contact_person", label: "담당자", width: "w-[80px]" }] : []), - ...(isColumnVisible("contact_phone") ? [{ key: "contact_phone", label: "전화번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("email") ? [{ key: "email", label: "이메일", width: "w-[160px]" }] : []), - ...(isColumnVisible("business_number") ? [{ key: "business_number", label: "사업자번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("address") ? [{ key: "address", label: "주소", minWidth: "min-w-[150px]" }] : []), - ...(isColumnVisible("status") ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ]; + // EDataTable 컬럼 정의 (공급업체 목록) — ts.visibleColumns 순서를 따름 + const supplierColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + supplier_code: { width: "w-[120px]" }, + supplier_name: { minWidth: "min-w-[140px]" }, + division: { + width: "w-[80px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + contact_person: { width: "w-[80px]" }, + contact_phone: { width: "w-[120px]" }, + email: { width: "w-[160px]" }, + business_number: { width: "w-[120px]" }, + address: { minWidth: "min-w-[150px]" }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 엑셀 다운로드 const handleExcelDownload = async () => { diff --git a/frontend/app/(main)/COMPANY_30/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_30/quality/item-inspection/page.tsx index 5baf35d3..2c0e1338 100644 --- a/frontend/app/(main)/COMPANY_30/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_30/quality/item-inspection/page.tsx @@ -28,6 +28,7 @@ const GRID_COLUMNS = [ { key: "item_code", label: "품목코드" }, { key: "item_name", label: "품목명" }, { key: "inspection_type", label: "검사유형" }, + { key: "item_count", label: "항목수" }, { key: "is_active", label: "사용여부" }, ]; const ITEM_TABLE = "item_info"; @@ -420,18 +421,41 @@ export default function ItemInspectionInfoPage() { 0 && checkedIds.length === data.length} onCheckedChange={(v) => setCheckedIds(v ? data.map(r => r.id) : [])} /> - 품목코드 - 품목명 - 검사유형 - 항목수 - 사용여부 + {ts.visibleColumns.map((col) => ( + + {col.label} + + ))} - {groupedData.map((group) => { + {ts.groupData(groupedData).map((group) => { + if ((group as any)._isGroupSummary || (group as any)._isGroupHeader) return null; const isExpanded = expandedItems.has(group.item_code); - const groupIds = group.rows.map(r => r.id); - const allChecked = groupIds.every(id => checkedIds.includes(id)); + const groupIds = group.rows.map((r: any) => r.id); + const allChecked = groupIds.every((id: string) => checkedIds.includes(id)); + const renderCell = (key: string) => { + switch (key) { + case "item_code": return {group.item_code}; + case "item_name": return {group.item_name}; + case "inspection_type": return ( + +
+ {group.types.map((t: string) => {t})} +
+
+ ); + case "item_count": return {group.rows.filter((r: any) => r.inspection_standard_id).length}; + case "is_active": return ( + + + {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} + + + ); + default: return {(group as any)[key] ?? ""}; + } + }; return ( { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !groupIds.includes(id)) : [...new Set([...prev, ...groupIds])]); }}> {}} /> - {group.item_code} - {group.item_name} - -
- {group.types.map(t => {t})} -
-
- {group.rows.filter(r => r.inspection_standard_id).length} - - - {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} - - + {ts.visibleColumns.map((col) => renderCell(col.key))}
- {isExpanded && group.rows.filter(r => r.inspection_standard_id).map((row, i) => ( + {isExpanded && group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( diff --git a/frontend/app/(main)/COMPANY_30/sales/customer/page.tsx b/frontend/app/(main)/COMPANY_30/sales/customer/page.tsx index bddc7730..20b98727 100644 --- a/frontend/app/(main)/COMPANY_30/sales/customer/page.tsx +++ b/frontend/app/(main)/COMPANY_30/sales/customer/page.tsx @@ -12,7 +12,7 @@ * - 납품처 등록 (delivery_destination) */ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -820,12 +820,14 @@ export default function CustomerManagementPage() { const allItems = res.data?.data?.data || res.data?.data?.rows || []; setItemTotalCount(allItems.length); const existingItemIds = new Set(priceItems.map((p: any) => p.item_id || p.item_number)); - const SALES_CODES = ["CAT_ML8ZFVEL_1TOR"]; // 영업관리 카테고리 코드 - setItemSearchResults(allItems.filter((item: any) => { + const seenNumbers = new Set(); + const deduped = allItems.filter((item: any) => { if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false; - const divCodes = (item.division || "").split(",").map((c: string) => c.trim()); - return divCodes.some((code: string) => SALES_CODES.includes(code)); - })); + if (item.item_number && seenNumbers.has(item.item_number)) return false; + if (item.item_number) seenNumbers.add(item.item_number); + return true; + }); + setItemSearchResults(deduped); } catch { /* skip */ } finally { setItemSearchLoading(false); } }; @@ -1229,47 +1231,44 @@ export default function CustomerManagementPage() { } }; - // 컬럼 가시성 헬퍼 - const isColumnVisible = (key: string) => ts.isVisible(key); - - const customerColSpan = 1 + ["customer_code", "customer_name", "contact_person", "contact_phone", "division", "status"] - .filter((k) => isColumnVisible(k)).length; - - // EDataTable 컬럼 정의 (거래처 목록) - const customerColumns: EDataTableColumn[] = [ - ...(isColumnVisible("customer_code") ? [{ key: "customer_code", label: "거래처코드", width: "w-[120px]" }] : []), - ...(isColumnVisible("customer_name") ? [{ key: "customer_name", label: "거래처명", minWidth: "min-w-[140px]" }] : []), - ...(isColumnVisible("division") ? [{ - key: "division", - label: "거래유형", - width: "w-[80px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ...(isColumnVisible("contact_person") ? [{ key: "contact_person", label: "담당자", width: "w-[80px]" }] : []), - ...(isColumnVisible("contact_phone") ? [{ key: "contact_phone", label: "전화번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("email") ? [{ key: "email", label: "이메일", width: "w-[160px]" }] : []), - ...(isColumnVisible("business_number") ? [{ key: "business_number", label: "사업자번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("address") ? [{ key: "address", label: "주소", minWidth: "min-w-[150px]" }] : []), - ...(isColumnVisible("status") ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ]; + // EDataTable 컬럼 정의 (거래처 목록) — ts.visibleColumns 순서를 따름 + const customerColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + customer_code: { width: "w-[120px]" }, + customer_name: { minWidth: "min-w-[140px]" }, + division: { + width: "w-[80px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + contact_person: { width: "w-[80px]" }, + contact_phone: { width: "w-[120px]" }, + email: { width: "w-[160px]" }, + business_number: { width: "w-[120px]" }, + address: { minWidth: "min-w-[150px]" }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 엑셀 다운로드 const handleExcelDownload = async () => { diff --git a/frontend/app/(main)/COMPANY_30/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_30/sales/shipping-order/page.tsx index 4ab5a9ad..2ed29b40 100644 --- a/frontend/app/(main)/COMPANY_30/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_30/sales/shipping-order/page.tsx @@ -363,7 +363,7 @@ export default function ShippingOrderPage() { spec: item.spec, material: item.material, orderQty: item.orderQty, - planQty: item.planQty, + planQty: item.orderQty, shipQty: 0, sourceType: item.sourceType, shipmentPlanId: item.shipmentPlanId, diff --git a/frontend/app/(main)/COMPANY_7/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_7/equipment/info/page.tsx index eeb63844..cd53e9b1 100644 --- a/frontend/app/(main)/COMPANY_7/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_7/equipment/info/page.tsx @@ -142,15 +142,20 @@ export default function EquipmentInfoPage() { }; const mainTableColumns = useMemo(() => { - const cols: EDataTableColumn[] = []; - if (ts.isVisible("equipment_code")) cols.push({ key: "equipment_code", label: "설비코드", width: "w-[110px]" }); - if (ts.isVisible("equipment_name")) cols.push({ key: "equipment_name", label: "설비명", minWidth: "min-w-[130px]", truncate: true, render: (v) => v || "-" }); - if (ts.isVisible("equipment_type")) cols.push({ key: "equipment_type", label: "설비유형", width: "w-[90px]", render: (v) => v || "-" }); - if (ts.isVisible("manufacturer")) cols.push({ key: "manufacturer", label: "제조사", width: "w-[100px]", render: (v) => v || "-" }); - if (ts.isVisible("installation_location")) cols.push({ key: "installation_location", label: "설치장소", width: "w-[100px]", render: (v) => v || "-" }); - if (ts.isVisible("operation_status")) cols.push({ key: "operation_status", label: "가동상태", width: "w-[80px]", render: (v) => v || "-" }); - return cols; - }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + 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 || "-" }, + manufacturer: { width: "w-[100px]", render: (v) => v || "-" }, + installation_location: { width: "w-[100px]", render: (v) => v || "-" }, + operation_status: { width: "w-[80px]", render: (v) => v || "-" }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 설비 조회 const fetchEquipments = useCallback(async () => { @@ -272,8 +277,8 @@ export default function EquipmentInfoPage() { if (!inspectionForm.inspection_cycle) { toast.error("점검주기는 필수입니다."); return; } if (!inspectionForm.inspection_method) { toast.error("점검방법은 필수입니다."); return; } const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; - if (isNumeric && !inspectionForm.unit) { toast.error("숫자 점검방법은 측정단위가 필수입니다."); return; } + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); + if (isNumeric && !inspectionForm.unit) { toast.error("측정단위가 필수입니다."); return; } // 기준값/오차범위 → 하한치/상한치 자동 계산 const saveData = { ...inspectionForm }; if (isNumeric && saveData.standard_value) { @@ -739,7 +744,7 @@ export default function EquipmentInfoPage() {
{catSelect("inspection_method", inspectionForm.inspection_method, (v) => { const label = resolve("inspection_method", v); - const isNum = label === "숫자" || v === "숫자"; + const isNum = ["숫자", "치수검사"].includes(label) || ["숫자", "치수검사"].includes(v); if (!isNum) { setInspectionForm((p) => ({ ...p, inspection_method: v, unit: "", standard_value: "", tolerance: "", lower_limit: "", upper_limit: "" })); } else { @@ -748,7 +753,7 @@ export default function EquipmentInfoPage() { }, "점검방법")}
{(() => { const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); if (!isNumeric) return null; return (
@@ -758,7 +763,7 @@ export default function EquipmentInfoPage() {
{(() => { const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); if (!isNumeric) return null; return (
diff --git a/frontend/app/(main)/COMPANY_7/logistics/material-status/page.tsx b/frontend/app/(main)/COMPANY_7/logistics/material-status/page.tsx index 46de306c..eb87ba92 100644 --- a/frontend/app/(main)/COMPANY_7/logistics/material-status/page.tsx +++ b/frontend/app/(main)/COMPANY_7/logistics/material-status/page.tsx @@ -333,69 +333,90 @@ export default function MaterialStatusPage() {

) : ( - workOrders.map((wo) => ( -
handleSelectWo(wo.id)} - > + ts.groupData(workOrders).map((wo) => { + if ((wo as any)._isGroupSummary || (wo as any)._isGroupHeader) return null; + return (
e.stopPropagation()} + key={wo.id} + className={cn( + "flex gap-3 rounded-lg border p-3 transition-all cursor-pointer", + "hover:border-primary/50 hover:shadow-sm", + selectedWoId === wo.id + ? "border-primary bg-primary/5 shadow-sm" + : "border-border" + )} + onClick={() => handleSelectWo(wo.id)} > - - handleCheckWo(wo.id, c as boolean) - } - /> -
-
-
- - {wo.plan_no || wo.work_order_no || `WO-${wo.id}`} - - e.stopPropagation()} + > + + handleCheckWo(wo.id, c as boolean) + } + /> +
+
+
+ {ts.isVisible("plan_no") && ( + + {wo.plan_no || wo.work_order_no || `WO-${wo.id}`} + )} - > - {getStatusLabel(wo.status)} - -
-
- - {wo.item_name} - - - ({wo.item_code}) - -
-
- 수량: - - {Number(wo.plan_qty).toLocaleString()}개 - - | - 일자: - - {wo.plan_date - ? new Date(wo.plan_date) - .toISOString() - .slice(0, 10) - : "-"} - + {ts.isVisible("status") && ( + + {getStatusLabel(wo.status)} + + )} +
+
+ {ts.isVisible("item_name") && ( + + {wo.item_name} + + )} + {ts.isVisible("item_code") && ( + + ({wo.item_code}) + + )} +
+
+ {ts.isVisible("plan_qty") && ( + <> + 수량: + + {Number(wo.plan_qty).toLocaleString()}개 + + + )} + {ts.isVisible("plan_qty") && ts.isVisible("plan_date") && ( + | + )} + {ts.isVisible("plan_date") && ( + <> + 일자: + + {wo.plan_date + ? new Date(wo.plan_date) + .toISOString() + .slice(0, 10) + : "-"} + + + )} +
-
- )) + ); + }) )}
diff --git a/frontend/app/(main)/COMPANY_7/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_7/logistics/outbound/page.tsx index 5ab46a8a..c1ffbd40 100644 --- a/frontend/app/(main)/COMPANY_7/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_7/logistics/outbound/page.tsx @@ -140,8 +140,16 @@ const DETAIL_HEADER_COLS = [ // 마스터 필드 키 목록 (필터 분류용) const MASTER_KEYS = new Set(["outbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 출고번호(1) + 마스터필드(7) = 10 -const TOTAL_COLS = 10; +// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) +const DETAIL_KEY_MAP: Record = { + source_type: "source_type", + item_number: "item_code", + item_name: "item_name", + spec: "specification", + outbound_qty: "outbound_qty", + unit_price: "unit_price", + total_amount: "total_amount", +}; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -248,6 +256,31 @@ interface SelectedSourceItem { export default function OutboundPage() { const ts = useTableSettings("c16-outbound", "outbound_mng", GRID_COLUMNS); + + // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 + const visibleMasterLayout = useMemo(() => { + const ordered: typeof MASTER_BODY_LAYOUT = []; + for (const vc of ts.visibleColumns) { + const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); + if (m) ordered.push(m); + } + return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; + }, [ts.visibleColumns]); + + const visibleDetailCols = useMemo(() => { + const ordered: typeof DETAIL_HEADER_COLS = []; + for (const vc of ts.visibleColumns) { + const detailKey = DETAIL_KEY_MAP[vc.key]; + if (detailKey) { + const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); + if (d) ordered.push(d); + } + } + return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; + }, [ts.visibleColumns]); + + const TOTAL_COLS = 3 + visibleMasterLayout.length; + // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -900,8 +933,15 @@ export default function OutboundPage() {
-
- +
+ + + + + {visibleMasterLayout.map((col) => ( + + ))} + - {/* 마스터 필드 헤더 */} - {MASTER_BODY_LAYOUT.map((col) => ( + {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => (
handleSort(col.key)}> @@ -1039,38 +1079,51 @@ export default function OutboundPage() { {outboundNo} ({group.details.length}) - {/* 출고유형 */} - - - {master.outbound_type || "-"} - - - {/* 출고일 */} - - {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} - - {/* 참조번호 */} - - {master.reference_number || ""} - - {/* 거래처 */} - - {master.customer_name || ""} - - {/* 창고 */} - - {master.warehouse_name || master.warehouse_code || ""} - - {/* 출고상태 */} - - - {master.outbound_status || "-"} - - - {/* 비고 */} - - {master.memo || ""} - + {/* 마스터 필드 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => { + switch (col.key) { + case "outbound_type": return ( + + + {master.outbound_type || "-"} + + + ); + case "outbound_date": return ( + + {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} + + ); + case "reference_number": return ( + + {master.reference_number || ""} + + ); + case "customer_name": return ( + + {master.customer_name || ""} + + ); + case "warehouse_name": return ( + + {master.warehouse_name || master.warehouse_code || ""} + + ); + case "outbound_status": return ( + + + {master.outbound_status || "-"} + + + ); + case "memo": return ( + + {master.memo || ""} + + ); + default: return {(master as any)[col.key] ?? ""}; + } + })} {/* 디테일 서브 헤더 (펼쳤을 때만) */} @@ -1084,7 +1137,7 @@ export default function OutboundPage() { - {DETAIL_HEADER_COLS.map((col) => { + {visibleDetailCols.map((col) => { const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); const isSorted = sortState?.key === col.key; const uniqueVals = Array.from(new Set( @@ -1163,20 +1216,18 @@ export default function OutboundPage() {
- {/* 출처 */} - {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"} - {/* 품목코드 */} - {row.item_code || ""} - {/* 품목명 */} - {row.item_name || ""} - {/* 규격 */} - {row.specification || ""} - {/* 출고수량 */} - {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""} - {/* 단가 */} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {/* 금액 */} - {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {visibleDetailCols.map((col) => { + switch (col.key) { + case "source_type": return {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"}; + case "item_code": return {row.item_code || ""}; + case "item_name": return {row.item_name || ""}; + case "specification": return {row.specification || ""}; + case "outbound_qty": return {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""}; + case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; + case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; + default: return {(row as any)[col.key] ?? ""}; + } + })} ); })} diff --git a/frontend/app/(main)/COMPANY_7/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_7/logistics/packaging/page.tsx index 5d4d5787..6ae340aa 100644 --- a/frontend/app/(main)/COMPANY_7/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_7/logistics/packaging/page.tsx @@ -460,18 +460,20 @@ export default function PackagingPage() { {/* 포장재 목록 테이블 */}
PKG_TYPE_LABEL[v] || v || "-" }, - { key: "size", label: "크기(mm)", width: "w-[100px]", render: (_v, row) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, - { key: "max_load_kg", label: "최대중량", width: "w-[80px]", align: "right", render: (v) => Number(v || 0) > 0 ? `${v}kg` : "-" }, - { key: "status", label: "상태", width: "w-[60px]", align: "center", render: (v) => ( - - {STATUS_LABEL[v] || v} - - )}, - ] as EDataTableColumn[]} + columns={ts.visibleColumns.map((col): EDataTableColumn => { + const renderMap: Record>> = { + pkg_type: { width: "w-[80px]", render: (v: any) => PKG_TYPE_LABEL[v] || v || "-" }, + size: { width: "w-[100px]", render: (_v: any, row: any) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, + max_weight: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, + max_load_kg: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, + status: { width: "w-[60px]", align: "center", render: (v: any) => ( + + {STATUS_LABEL[v] || v} + + )}, + }; + return { key: col.key, label: col.label, ...renderMap[col.key] }; + })} data={ts.groupData(filteredPkgUnits)} rowKey={(row) => String(row.id)} loading={pkgLoading} diff --git a/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx index a8d5fc2c..85cdc23c 100644 --- a/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx @@ -117,12 +117,20 @@ const DETAIL_HEADER_COLS = [ { key: "total_amount", label: "금액" }, ]; -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 입고번호(1) + 디테일(7) = 10 -const TOTAL_COLS = 10; - // 마스터 필드 키 목록 (필터 분류용) const MASTER_KEYS = new Set(["inbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); +// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) +const DETAIL_KEY_MAP: Record = { + source_type: "source_table", + item_number: "item_number", + item_name: "item_name", + spec: "spec", + inbound_qty: "inbound_qty", + unit_price: "unit_price", + total_amount: "total_amount", +}; + // 헤더 필터 Popover function HeaderFilterPopover({ colKey, colLabel, uniqueValues, filterValues, onToggle, onClear, @@ -278,6 +286,31 @@ interface SelectedSourceItem { export default function ReceivingPage() { const ts = useTableSettings("c16-receiving", "inbound_mng", GRID_COLUMNS); + + // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 + const visibleMasterLayout = useMemo(() => { + const ordered: typeof MASTER_BODY_LAYOUT = []; + for (const vc of ts.visibleColumns) { + const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); + if (m) ordered.push(m); + } + return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; + }, [ts.visibleColumns]); + + const visibleDetailCols = useMemo(() => { + const ordered: typeof DETAIL_HEADER_COLS = []; + for (const vc of ts.visibleColumns) { + const detailKey = DETAIL_KEY_MAP[vc.key]; + if (detailKey) { + const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); + if (d) ordered.push(d); + } + } + return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; + }, [ts.visibleColumns]); + + const TOTAL_COLS = 3 + visibleMasterLayout.length; + // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -847,8 +880,15 @@ export default function ReceivingPage() {
-
- +
+ + + + + {visibleMasterLayout.map((col) => ( + + ))} + - {/* 마스터 필드 헤더 (colSpan으로 디테일 컬럼 위에 맵핑) */} - {MASTER_BODY_LAYOUT.map((col) => ( + {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => (
handleSort(col.key)}> @@ -985,38 +1025,51 @@ export default function ReceivingPage() { {inboundNo} ({group.details.length}) - {/* 입고유형 */} - - - {resolveInboundType(master.inbound_type)} - - - {/* 입고일 */} - - {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} - - {/* 참조번호 */} - - {master.reference_number || ""} - - {/* 공급처 */} - - {master.supplier_name || ""} - - {/* 창고 */} - - {master.warehouse_name || master.warehouse_code || ""} - - {/* 입고상태 */} - - - {master.inbound_status || "-"} - - - {/* 비고 */} - - {master.memo || ""} - + {/* 마스터 필드 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => { + switch (col.key) { + case "inbound_type": return ( + + + {resolveInboundType(master.inbound_type)} + + + ); + case "inbound_date": return ( + + {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} + + ); + case "reference_number": return ( + + {master.reference_number || ""} + + ); + case "supplier_name": return ( + + {master.supplier_name || ""} + + ); + case "warehouse_name": return ( + + {master.warehouse_name || master.warehouse_code || ""} + + ); + case "inbound_status": return ( + + + {master.inbound_status || "-"} + + + ); + case "memo": return ( + + {master.memo || ""} + + ); + default: return {(master as any)[col.key] ?? ""}; + } + })} {/* 디테일 서브 헤더 (펼쳤을 때만) */} @@ -1030,7 +1083,7 @@ export default function ReceivingPage() { - {DETAIL_HEADER_COLS.map((col) => { + {visibleDetailCols.map((col) => { const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); const isSorted = sortState?.key === col.key; const uniqueVals = Array.from(new Set( @@ -1108,20 +1161,18 @@ export default function ReceivingPage() {
- {/* 출처 */} - {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"} - {/* 품목코드 */} - {row.item_number || ""} - {/* 품목명 */} - {row.item_name || ""} - {/* 규격 */} - {row.spec || ""} - {/* 입고수량 */} - {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""} - {/* 단가 */} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {/* 금액 */} - {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {visibleDetailCols.map((col) => { + switch (col.key) { + case "source_table": return {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"}; + case "item_number": return {row.item_number || ""}; + case "item_name": return {row.item_name || ""}; + case "spec": return {row.spec || ""}; + case "inbound_qty": return {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""}; + case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; + case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; + default: return {(row as any)[col.key] ?? ""}; + } + })} ); })} 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 dfd1b666..9d7f2dea 100644 --- a/frontend/app/(main)/COMPANY_7/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_7/master-data/company/page.tsx @@ -491,12 +491,6 @@ export default function CompanyPage() { > 회사정보 - - 부서관리 -
@@ -635,89 +629,6 @@ export default function CompanyPage() {
- {/* ===================== Tab 2: 부서관리 ===================== */} - -
- - {/* 좌측: 부서 트리 */} - -
-
-
- - 부서 - {depts.length}건 -
-
- - - -
-
-
- {deptLoading ? ( -
- -
- ) : deptTree.length === 0 ? ( -
- - 등록된 부서가 없어요 -
- ) : ( - renderTree(deptTree) - )} -
-
-
- - - - {/* 우측: 사원 목록 */} - -
-
-
- - {selectedDept ? "부서 인원" : "부서를 선택해주세요"} - {selectedDept && {selectedDept.dept_name}} - {members.length > 0 && {members.length}명} -
- {selectedDeptCode && ( - - )} -
- {selectedDeptCode ? ( - row.user_id || row.id} - loading={memberLoading} - emptyMessage="소속 사원이 없어요" - emptyIcon={} - onRowDoubleClick={(row) => openUserModal(row)} - showPagination={false} - draggableColumns={false} - /> - ) : ( -
- - 좌측에서 부서를 선택해주세요 -
- )} -
-
-
-
-
{/* ── 부서 등록/수정 모달 ── */} diff --git a/frontend/app/(main)/COMPANY_7/master-data/department/page.tsx b/frontend/app/(main)/COMPANY_7/master-data/department/page.tsx index a2bbcba5..3245571e 100644 --- a/frontend/app/(main)/COMPANY_7/master-data/department/page.tsx +++ b/frontend/app/(main)/COMPANY_7/master-data/department/page.tsx @@ -9,7 +9,7 @@ * 모달: 부서 등록(dept_info), 사원 추가(user_info) */ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -279,6 +279,7 @@ export default function DepartmentPage() { dept_code: userForm.dept_code || undefined, dept_name: depts.find((d) => d.dept_code === userForm.dept_code)?.dept_name || undefined, status: userForm.status || "active", + end_date: userForm.end_date || null, }, mainDept: userForm.dept_code ? { dept_code: userForm.dept_code, @@ -312,37 +313,40 @@ export default function DepartmentPage() { const activeMembers = members.filter((m) => !m.end_date || m.end_date.substring(0, 10) >= today); const resignedMembers = members.filter((m) => m.end_date && m.end_date.substring(0, 10) < today); - const isColVisible = (key: string) => ts.isVisible(key); - - // EDataTable 컬럼 정의 (부서 목록) - const deptColumns: EDataTableColumn[] = [ - { key: "dept_code", label: "부서코드", width: "w-[120px]" }, - { key: "dept_name", label: "부서명", minWidth: "min-w-[140px]" }, - ...(isColVisible("parent_dept_code") - ? [{ - key: "parent_dept_code", - label: "상위부서", - width: "w-[110px]", - render: (val: any) => {val || "\u2014"}, - }] - : []), - ...(isColVisible("status") - ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val === "active" ? "활성" : (val || "\u2014")} - - ) : null, - }] - : []), - ]; + // EDataTable 컬럼 정의 (부서 목록) — ts.visibleColumns 순서를 따름 + const deptColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + dept_code: { width: "w-[120px]" }, + dept_name: { minWidth: "min-w-[140px]" }, + parent_dept_code: { + width: "w-[110px]", + render: (val: any) => {val || "\u2014"}, + }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val === "active" ? "활성" : (val || "\u2014")} + + ) : null, + }, + }; + // dept_code, dept_name은 항상 표시 (DEPT_COLUMNS에 포함되지 않으므로 visibleColumns에 없음) + const fixedCols: EDataTableColumn[] = [ + { key: "dept_code", label: "부서코드", ...colProps["dept_code"] }, + { key: "dept_name", label: "부서명", ...colProps["dept_name"] }, + ]; + const dynamicCols = ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + return [...fixedCols, ...dynamicCols]; + }, [ts.visibleColumns]); return (
diff --git a/frontend/app/(main)/COMPANY_7/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_7/master-data/item-info/page.tsx index 3f037275..375fd900 100644 --- a/frontend/app/(main)/COMPANY_7/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_7/master-data/item-info/page.tsx @@ -84,6 +84,56 @@ function CategoryCombobox({ options, value, onChange, placeholder }: { ); } +// 다중 선택 카테고리 콤보박스 +function MultiCategoryCombobox({ options, value, onChange, placeholder }: { + options: { code: string; label: string }[]; + value: string; + onChange: (v: string) => void; + placeholder: string; +}) { + const [open, setOpen] = useState(false); + const selectedCodes = value ? value.split(",").map((c) => c.trim()).filter(Boolean) : []; + const selectedLabels = selectedCodes.map((code) => options.find((o) => o.code === code)?.label || code).filter(Boolean); + + const toggle = (code: string) => { + const next = selectedCodes.includes(code) + ? selectedCodes.filter((c) => c !== code) + : [...selectedCodes, code]; + onChange(next.join(",")); + }; + + return ( + + + + + + + + + 검색 결과가 없어요 + + {options.map((opt) => ( + toggle(opt.code)}> + + {opt.label} + + ))} + + + + + + ); +} + const TABLE_NAME = "item_info"; const GRID_COLUMNS = [ @@ -108,7 +158,7 @@ const GRID_COLUMNS = [ const FORM_FIELDS = [ { key: "item_number", label: "품목코드", type: "text", required: true, disabled: true, placeholder: "자동 채번" }, { key: "item_name", label: "품명", type: "text", required: true }, - { key: "division", label: "관리품목", type: "category" }, + { key: "division", label: "관리품목", type: "multi-category" }, { key: "type", label: "품목구분", type: "category" }, { key: "size", label: "규격", type: "text" }, { key: "unit", label: "단위", type: "category" }, @@ -137,6 +187,7 @@ export default function ItemInfoPage() { const { user } = useAuth(); const ts = useTableSettings("c16-item-info", TABLE_NAME, GRID_COLUMNS); const [items, setItems] = useState([]); + const [rawItems, setRawItems] = useState([]); const [loading, setLoading] = useState(false); // 검색 필터 (DynamicSearchFilter) @@ -215,6 +266,7 @@ export default function ItemInfoPage() { } return categoryOptions[col]?.find((o) => o.code === code)?.label || code; }; + setRawItems(raw); const data = raw.map((r: any) => { const converted = { ...r }; for (const col of CATEGORY_COLUMNS) { @@ -261,7 +313,8 @@ export default function ItemInfoPage() { // 수정 모달 열기 const openEditModal = (item: any) => { - setFormData({ ...item }); + const raw = rawItems.find((r) => r.id === item.id) || item; + setFormData({ ...raw }); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); @@ -269,7 +322,8 @@ export default function ItemInfoPage() { // 복사 모달 열기 const openCopyModal = async (item: any) => { - const { id, item_number, created_date, updated_date, writer, ...rest } = item; + const raw = rawItems.find((r) => r.id === item.id) || item; + const { id, item_number, created_date, updated_date, writer, ...rest } = raw; setFormData(rest); setIsEditMode(false); setEditId(null); @@ -459,6 +513,13 @@ export default function ItemInfoPage() { columnName={field.key} height="h-32" /> + ) : field.type === "multi-category" ? ( + setFormData((prev) => ({ ...prev, [field.key]: v }))} + placeholder={`${field.label} 선택`} + /> ) : field.type === "category" ? ( (() => { - const cols: EDataTableColumn[] = []; - if (ts.isVisible("item_number")) cols.push({ key: "item_number", label: "품번", width: "w-[110px]" }); - if (ts.isVisible("item_name")) cols.push({ key: "item_name", label: "품명", minWidth: "min-w-[130px]", render: (v) => v || "-" }); - if (ts.isVisible("size")) cols.push({ key: "size", label: "규격", width: "w-[90px]", render: (v) => v || "-" }); - if (ts.isVisible("unit")) cols.push({ key: "unit", label: "단위", width: "w-[60px]", render: (v) => v || "-" }); - if (ts.isVisible("standard_price")) cols.push({ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }); - if (ts.isVisible("selling_price")) cols.push({ key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true }); - if (ts.isVisible("currency_code")) cols.push({ key: "currency_code", label: "통화", width: "w-[50px]", render: (v) => v || "-" }); - if (ts.isVisible("status")) cols.push({ key: "status", label: "상태", width: "w-[60px]", render: (v) => v || "-" }); - return cols; - }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + const colProps: Record> = { + item_number: { width: "w-[110px]" }, + item_name: { minWidth: "min-w-[130px]", render: (v) => v || "-" }, + size: { width: "w-[90px]", render: (v) => v || "-" }, + unit: { width: "w-[60px]", render: (v) => v || "-" }, + standard_price: { width: "w-[90px]", align: "right", formatNumber: true }, + selling_price: { width: "w-[90px]", align: "right", formatNumber: true }, + currency_code: { width: "w-[50px]", render: (v) => v || "-" }, + status: { width: "w-[60px]", render: (v) => v || "-" }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 좌측: 품목 조회 (division이 "외주관리"인 품목만 필터링) const outsourcingDivisionCode = categoryOptions["division"]?.find( diff --git a/frontend/app/(main)/COMPANY_7/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_7/production/plan-management/page.tsx index 4a0d341a..6bd63176 100644 --- a/frontend/app/(main)/COMPANY_7/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_7/production/plan-management/page.tsx @@ -919,9 +919,7 @@ export default function ProductionPlanManagementPage() { // 숫자 포맷 const formatNumber = (num: number | string) => Number(num).toLocaleString(); - // 컬럼 표시 여부 - const isColVisible = (key: string) => ts.isVisible(key); - const orderColSpan = 4 + ORDER_COLUMNS.filter((c) => isColVisible(c.key)).length; + // (컬럼 표시는 ts.visibleColumns 순서를 따름) return (
@@ -1019,6 +1017,38 @@ export default function ProductionPlanManagementPage() {
) : (
+ {(() => { + // 디테일 행에서 개별 값을 표시하는 컬럼 매핑 + const DETAIL_VALUE_MAP: Record = { + total_order_qty: "order_qty", + total_ship_qty: "ship_qty", + total_balance_qty: "balance_qty", + }; + + // 그룹 행에서 특수 렌더링이 필요한 컬럼 + const renderGroupCell = (col: { key: string }, item: any) => { + if (col.key === "required_plan_qty") { + return ( + 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> + {formatNumber(item.required_plan_qty)} + + ); + } + if (col.key === "lead_time") { + return ( + toggleItemExpand(item.item_code)}> + {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"} + + ); + } + return ( + toggleItemExpand(item.item_code)}> + {formatNumber(item[col.key])} + + ); + }; + + return (
@@ -1028,15 +1058,11 @@ export default function ProductionPlanManagementPage() { 품목코드 품목명 - {isColVisible("total_order_qty") && 총수주량} - {isColVisible("total_ship_qty") && 출고량} - {isColVisible("total_balance_qty") && 잔량} - {isColVisible("current_stock") && 현재고} - {isColVisible("safety_stock") && 안전재고} - {isColVisible("existing_plan_qty") && 기생산계획량} - {isColVisible("in_progress_qty") && 생산진행} - {isColVisible("required_plan_qty") && 필요생산계획} - {isColVisible("lead_time") && 리드타임(일)} + {ts.visibleColumns.map((col) => ( + + {col.label} + + ))} @@ -1046,6 +1072,7 @@ export default function ProductionPlanManagementPage() { + {ts.visibleColumns.map((col) => { const v = (item as any)[col.key]; return ( @@ -1068,25 +1095,14 @@ export default function ProductionPlanManagementPage() { toggleItemExpand(item.item_code)}>{item.item_code} toggleItemExpand(item.item_code)}>{item.item_name} - {isColVisible("total_order_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_order_qty)}} - {isColVisible("total_ship_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_ship_qty)}} - {isColVisible("total_balance_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_balance_qty)}} - {isColVisible("current_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.current_stock)}} - {isColVisible("safety_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.safety_stock)}} - {isColVisible("existing_plan_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.existing_plan_qty)}} - {isColVisible("in_progress_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.in_progress_qty)}} - {isColVisible("required_plan_qty") && ( - 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> - {formatNumber(item.required_plan_qty)} - - )} - {isColVisible("lead_time") && ( - toggleItemExpand(item.item_code)}> - {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"} - - )} + {ts.visibleColumns.map((col) => renderGroupCell(col, item))} - {expandedItems.has(item.item_code) && item.orders?.map((detail) => ( + {expandedItems.has(item.item_code) && item.orders?.map((detail: any) => { + let remainColSpan = 0; + for (const col of ts.visibleColumns) { + if (!DETAIL_VALUE_MAP[col.key]) remainColSpan++; + } + return ( @@ -1101,19 +1117,28 @@ export default function ProductionPlanManagementPage() { - {isColVisible("total_order_qty") && {formatNumber(detail.order_qty)}} - {isColVisible("total_ship_qty") && {formatNumber(detail.ship_qty)}} - {isColVisible("total_balance_qty") && {formatNumber(detail.balance_qty)}} - - 납기일: {detail.due_date || "-"} - + {ts.visibleColumns.map((col) => { + const detailKey = DETAIL_VALUE_MAP[col.key]; + if (detailKey) { + return {formatNumber(detail[detailKey])}; + } + return null; + })} + {remainColSpan > 0 && ( + + 납기일: {detail.due_date || "-"} + + )} - ))} + ); + })} ); })}
+ ); + })()} )} diff --git a/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx index fa0e08c5..1bc3bc88 100644 --- a/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx @@ -742,10 +742,24 @@ export default function PurchaseOrderPage() { ) : ( (() => { const MASTER_KEYS = new Set(["purchase_no", "order_date", "supplier_name", "status", "memo"]); - const detailCols = ts.visibleColumns.filter(c => !MASTER_KEYS.has(c.key)); - const masterCols = ts.visibleColumns.filter(c => MASTER_KEYS.has(c.key)); const numCols = new Set(["order_qty", "received_qty", "remain_qty", "unit_price", "amount"]); + // ts.visibleColumns 순서를 따르되, 마스터/디테일 컬럼을 분리 + // 고정 컬럼(품목수)은 마스터 선행 컬럼 뒤에 배치 + const leadingMaster: typeof ts.visibleColumns = []; + const detailCols: typeof ts.visibleColumns = []; + const trailingMaster: typeof ts.visibleColumns = []; + let passedFirstDetail = false; + for (const col of ts.visibleColumns) { + if (MASTER_KEYS.has(col.key)) { + if (passedFirstDetail) trailingMaster.push(col); + else leadingMaster.push(col); + } else { + passedFirstDetail = true; + detailCols.push(col); + } + } + const renderDetailCell = (row: any, key: string) => { const val = row[key]; if (key === "status") return val ? {val} : "-"; @@ -753,23 +767,35 @@ export default function PurchaseOrderPage() { return val || "-"; }; + const renderMasterHead = (col: { key: string; label: string }) => ( + + {col.label} + + ); + + const renderMasterCell = (col: { key: string }, m: any, purchaseNo: string) => { + if (col.key === "purchase_no") return {purchaseNo}; + if (col.key === "order_date") return {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}; + if (col.key === "supplier_name") return {m.supplier_name || "-"}; + if (col.key === "status") return {m.status && {m.status}}; + if (col.key === "memo") return {m.memo || ""}; + return ; + }; + return ( - {ts.isVisible("purchase_no") && 발주번호} - {ts.isVisible("order_date") && 발주일} - {ts.isVisible("supplier_name") && 공급업체} + {leadingMaster.map(renderMasterHead)} 품목수 {detailCols.map(col => ( {col.label}{col.key === "order_qty" || col.key === "amount" ? " 합계" : ""} ))} - {ts.isVisible("status") && 상태} - {ts.isVisible("memo") && 메모} + {trailingMaster.map(renderMasterHead)} @@ -795,9 +821,7 @@ export default function PurchaseOrderPage() { { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !detailIds.includes(id)) : [...new Set([...prev, ...detailIds])]); }}> {}} /> - {ts.isVisible("purchase_no") && {purchaseNo}} - {ts.isVisible("order_date") && {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}} - {ts.isVisible("supplier_name") && {m.supplier_name || "-"}} + {leadingMaster.map(col => renderMasterCell(col, m, purchaseNo))} {group.details.length}건 {detailCols.map(col => ( @@ -806,8 +830,7 @@ export default function PurchaseOrderPage() { : ""} ))} - {ts.isVisible("status") && {m.status && {m.status}}} - {ts.isVisible("memo") && {m.memo || ""}} + {trailingMaster.map(col => renderMasterCell(col, m, purchaseNo))} {isExpanded && group.details.map((row) => ( @@ -815,17 +838,14 @@ export default function PurchaseOrderPage() { { e.stopPropagation(); setCheckedIds(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id]); }}> {}} /> - {ts.isVisible("purchase_no") && } - {ts.isVisible("order_date") && } - {ts.isVisible("supplier_name") && } + {leadingMaster.map(col => )} {detailCols.map(col => ( {renderDetailCell(row, col.key)} ))} - {ts.isVisible("status") && } - {ts.isVisible("memo") && } + {trailingMaster.map(col => )} ))} diff --git a/frontend/app/(main)/COMPANY_7/purchase/purchase-item/page.tsx b/frontend/app/(main)/COMPANY_7/purchase/purchase-item/page.tsx index 1f7c4137..7f211a88 100644 --- a/frontend/app/(main)/COMPANY_7/purchase/purchase-item/page.tsx +++ b/frontend/app/(main)/COMPANY_7/purchase/purchase-item/page.tsx @@ -617,17 +617,21 @@ export default function PurchaseItemPage() { toast.success("다운로드 완료"); }; - // EDataTable 컬럼 정의 (구매품목) - const itemColumns: EDataTableColumn[] = [ - { key: "item_number", label: "품번", width: "w-[110px]" }, - { key: "item_name", label: "품명", minWidth: "min-w-[130px]" }, - { key: "size", label: "규격", width: "w-[80px]" }, - { key: "unit", label: "단위", width: "w-[60px]" }, - { key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }, - { key: "standard_price", label: "구매단가", width: "w-[90px]", align: "right", formatNumber: true }, - { key: "currency_code", label: "통화", width: "w-[50px]" }, - { key: "status", label: "상태", width: "w-[60px]" }, - ]; + // EDataTable 컬럼 정의 (구매품목) — ts.visibleColumns 기반 + const COLUMN_RENDER_MAP: Record> = { + item_number: { width: "w-[110px]" }, + item_name: { minWidth: "min-w-[130px]" }, + size: { width: "w-[80px]" }, + unit: { width: "w-[60px]" }, + standard_price: { width: "w-[90px]", align: "right", formatNumber: true }, + currency_code: { width: "w-[50px]" }, + status: { width: "w-[60px]" }, + }; + const itemColumns: EDataTableColumn[] = ts.visibleColumns.map((col): EDataTableColumn => ({ + key: col.key, + label: col.label, + ...COLUMN_RENDER_MAP[col.key], + })); return (
diff --git a/frontend/app/(main)/COMPANY_7/purchase/supplier/page.tsx b/frontend/app/(main)/COMPANY_7/purchase/supplier/page.tsx index 51c50aa5..521f770e 100644 --- a/frontend/app/(main)/COMPANY_7/purchase/supplier/page.tsx +++ b/frontend/app/(main)/COMPANY_7/purchase/supplier/page.tsx @@ -12,7 +12,7 @@ * - 납품처 등록 (delivery_destination) */ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -1229,47 +1229,44 @@ export default function SupplierManagementPage() { } }; - // 컬럼 가시성 헬퍼 - const isColumnVisible = (key: string) => ts.isVisible(key); - - const supplierColSpan = 1 + ["supplier_code", "supplier_name", "contact_person", "contact_phone", "division", "status"] - .filter((k) => isColumnVisible(k)).length; - - // EDataTable 컬럼 정의 (공급업체 목록) - const supplierColumns: EDataTableColumn[] = [ - ...(isColumnVisible("supplier_code") ? [{ key: "supplier_code", label: "공급업체코드", width: "w-[120px]" }] : []), - ...(isColumnVisible("supplier_name") ? [{ key: "supplier_name", label: "공급업체명", minWidth: "min-w-[140px]" }] : []), - ...(isColumnVisible("division") ? [{ - key: "division", - label: "공급업체유형", - width: "w-[80px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ...(isColumnVisible("contact_person") ? [{ key: "contact_person", label: "담당자", width: "w-[80px]" }] : []), - ...(isColumnVisible("contact_phone") ? [{ key: "contact_phone", label: "전화번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("email") ? [{ key: "email", label: "이메일", width: "w-[160px]" }] : []), - ...(isColumnVisible("business_number") ? [{ key: "business_number", label: "사업자번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("address") ? [{ key: "address", label: "주소", minWidth: "min-w-[150px]" }] : []), - ...(isColumnVisible("status") ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ]; + // EDataTable 컬럼 정의 (공급업체 목록) — ts.visibleColumns 순서를 따름 + const supplierColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + supplier_code: { width: "w-[120px]" }, + supplier_name: { minWidth: "min-w-[140px]" }, + division: { + width: "w-[80px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + contact_person: { width: "w-[80px]" }, + contact_phone: { width: "w-[120px]" }, + email: { width: "w-[160px]" }, + business_number: { width: "w-[120px]" }, + address: { minWidth: "min-w-[150px]" }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 엑셀 다운로드 const handleExcelDownload = async () => { diff --git a/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx index 5baf35d3..2c0e1338 100644 --- a/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx @@ -28,6 +28,7 @@ const GRID_COLUMNS = [ { key: "item_code", label: "품목코드" }, { key: "item_name", label: "품목명" }, { key: "inspection_type", label: "검사유형" }, + { key: "item_count", label: "항목수" }, { key: "is_active", label: "사용여부" }, ]; const ITEM_TABLE = "item_info"; @@ -420,18 +421,41 @@ export default function ItemInspectionInfoPage() { 0 && checkedIds.length === data.length} onCheckedChange={(v) => setCheckedIds(v ? data.map(r => r.id) : [])} /> - 품목코드 - 품목명 - 검사유형 - 항목수 - 사용여부 + {ts.visibleColumns.map((col) => ( + + {col.label} + + ))} - {groupedData.map((group) => { + {ts.groupData(groupedData).map((group) => { + if ((group as any)._isGroupSummary || (group as any)._isGroupHeader) return null; const isExpanded = expandedItems.has(group.item_code); - const groupIds = group.rows.map(r => r.id); - const allChecked = groupIds.every(id => checkedIds.includes(id)); + const groupIds = group.rows.map((r: any) => r.id); + const allChecked = groupIds.every((id: string) => checkedIds.includes(id)); + const renderCell = (key: string) => { + switch (key) { + case "item_code": return {group.item_code}; + case "item_name": return {group.item_name}; + case "inspection_type": return ( + +
+ {group.types.map((t: string) => {t})} +
+
+ ); + case "item_count": return {group.rows.filter((r: any) => r.inspection_standard_id).length}; + case "is_active": return ( + + + {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} + + + ); + default: return {(group as any)[key] ?? ""}; + } + }; return ( { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !groupIds.includes(id)) : [...new Set([...prev, ...groupIds])]); }}> {}} /> - {group.item_code} - {group.item_name} - -
- {group.types.map(t => {t})} -
-
- {group.rows.filter(r => r.inspection_standard_id).length} - - - {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} - - + {ts.visibleColumns.map((col) => renderCell(col.key))}
- {isExpanded && group.rows.filter(r => r.inspection_standard_id).map((row, i) => ( + {isExpanded && group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( diff --git a/frontend/app/(main)/COMPANY_7/sales/customer/page.tsx b/frontend/app/(main)/COMPANY_7/sales/customer/page.tsx index bddc7730..20b98727 100644 --- a/frontend/app/(main)/COMPANY_7/sales/customer/page.tsx +++ b/frontend/app/(main)/COMPANY_7/sales/customer/page.tsx @@ -12,7 +12,7 @@ * - 납품처 등록 (delivery_destination) */ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -820,12 +820,14 @@ export default function CustomerManagementPage() { const allItems = res.data?.data?.data || res.data?.data?.rows || []; setItemTotalCount(allItems.length); const existingItemIds = new Set(priceItems.map((p: any) => p.item_id || p.item_number)); - const SALES_CODES = ["CAT_ML8ZFVEL_1TOR"]; // 영업관리 카테고리 코드 - setItemSearchResults(allItems.filter((item: any) => { + const seenNumbers = new Set(); + const deduped = allItems.filter((item: any) => { if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false; - const divCodes = (item.division || "").split(",").map((c: string) => c.trim()); - return divCodes.some((code: string) => SALES_CODES.includes(code)); - })); + if (item.item_number && seenNumbers.has(item.item_number)) return false; + if (item.item_number) seenNumbers.add(item.item_number); + return true; + }); + setItemSearchResults(deduped); } catch { /* skip */ } finally { setItemSearchLoading(false); } }; @@ -1229,47 +1231,44 @@ export default function CustomerManagementPage() { } }; - // 컬럼 가시성 헬퍼 - const isColumnVisible = (key: string) => ts.isVisible(key); - - const customerColSpan = 1 + ["customer_code", "customer_name", "contact_person", "contact_phone", "division", "status"] - .filter((k) => isColumnVisible(k)).length; - - // EDataTable 컬럼 정의 (거래처 목록) - const customerColumns: EDataTableColumn[] = [ - ...(isColumnVisible("customer_code") ? [{ key: "customer_code", label: "거래처코드", width: "w-[120px]" }] : []), - ...(isColumnVisible("customer_name") ? [{ key: "customer_name", label: "거래처명", minWidth: "min-w-[140px]" }] : []), - ...(isColumnVisible("division") ? [{ - key: "division", - label: "거래유형", - width: "w-[80px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ...(isColumnVisible("contact_person") ? [{ key: "contact_person", label: "담당자", width: "w-[80px]" }] : []), - ...(isColumnVisible("contact_phone") ? [{ key: "contact_phone", label: "전화번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("email") ? [{ key: "email", label: "이메일", width: "w-[160px]" }] : []), - ...(isColumnVisible("business_number") ? [{ key: "business_number", label: "사업자번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("address") ? [{ key: "address", label: "주소", minWidth: "min-w-[150px]" }] : []), - ...(isColumnVisible("status") ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ]; + // EDataTable 컬럼 정의 (거래처 목록) — ts.visibleColumns 순서를 따름 + const customerColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + customer_code: { width: "w-[120px]" }, + customer_name: { minWidth: "min-w-[140px]" }, + division: { + width: "w-[80px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + contact_person: { width: "w-[80px]" }, + contact_phone: { width: "w-[120px]" }, + email: { width: "w-[160px]" }, + business_number: { width: "w-[120px]" }, + address: { minWidth: "min-w-[150px]" }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 엑셀 다운로드 const handleExcelDownload = async () => { diff --git a/frontend/app/(main)/COMPANY_7/sales/order/page.tsx b/frontend/app/(main)/COMPANY_7/sales/order/page.tsx index f896bbfd..f9a358d4 100644 --- a/frontend/app/(main)/COMPANY_7/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_7/sales/order/page.tsx @@ -13,7 +13,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, ClipboardList, Pencil, Search, X, Truck, Package, - ChevronLeft, ChevronRight, ChevronDown, ChevronsLeft, ChevronsRight, + ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, RotateCcw, Filter, Check, ArrowUp, ArrowDown, } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; @@ -42,41 +42,30 @@ const formatNumber = (val: string) => { }; const parseNumber = (val: string) => val.replace(/,/g, ""); -// 마스터 헤더 레이아웃 (수주번호 뒤, 디테일 11컬럼 위에 colSpan으로 맵핑) -// 순서: 거래처 | 단가방식 | 납품처 | 납품장소 | 수주일 | 담당자 → 합계 colSpan = 11 -const MASTER_BODY_LAYOUT = [ - { key: "partner_id", label: "거래처", colSpan: 2 }, - { key: "price_mode", label: "단가방식", colSpan: 1 }, - { key: "delivery_partner_id", label: "납품처", colSpan: 2 }, - { key: "delivery_address", label: "납품장소", colSpan: 2 }, - { key: "order_date", label: "수주일", colSpan: 2 }, - { key: "manager_id", label: "담당자", colSpan: 2 }, +// 플랫 테이블 컬럼 정의 (마스터+디테일 통합) +const FLAT_COLUMNS = [ + { key: "order_no", label: "수주번호", source: "master" }, + { key: "partner_id", label: "거래처", source: "master" }, + { key: "order_date", label: "수주일", source: "master" }, + { key: "part_code", label: "품번", source: "detail" }, + { key: "part_name", label: "품명", source: "detail" }, + { key: "spec", label: "규격", source: "detail" }, + { key: "unit", label: "단위", source: "detail" }, + { key: "qty", label: "수량", source: "detail" }, + { key: "ship_qty", label: "출하수량", source: "detail" }, + { key: "balance_qty", label: "잔량", source: "detail" }, + { key: "unit_price", label: "단가", source: "detail" }, + { key: "amount", label: "금액", source: "detail" }, + { key: "due_date", label: "납기일", source: "detail" }, + { key: "memo", label: "메모", source: "master" }, ]; -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "part_code", label: "품번" }, - { key: "part_name", label: "품명" }, - { key: "spec", label: "규격" }, - { key: "unit", label: "단위" }, - { key: "qty", label: "수량" }, - { key: "ship_qty", label: "출하수량" }, - { key: "balance_qty", label: "잔량" }, - { key: "unit_price", label: "단가" }, - { key: "amount", label: "금액" }, - { key: "currency_code", label: "통화" }, - { key: "due_date", label: "납기일" }, -]; +const DETAIL_HEADER_COLS = FLAT_COLUMNS.filter((c) => c.source === "detail"); // 필터용 전체 키 -const GRID_COLUMNS_CONFIG = [ - { key: "order_no", label: "수주번호" }, - ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label })), - ...DETAIL_HEADER_COLS, - { key: "memo", label: "메모" }, -]; +const GRID_COLUMNS_CONFIG = FLAT_COLUMNS.map(({ key, label }) => ({ key, label })); -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 수주번호(1) + 디테일(11) + 메모(1) = 15 +// 총 컬럼 수: 체크박스(1) + 플랫 컬럼(14) = 15 const TOTAL_COLS = 15; // 헤더 필터 Popover @@ -180,8 +169,6 @@ export default function SalesOrderPage() { const [masterForm, setMasterForm] = useState>({}); const [detailRows, setDetailRows] = useState([]); const [allowPriceEdit, setAllowPriceEdit] = useState(true); - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); // 품목 선택 모달 const [itemSelectOpen, setItemSelectOpen] = useState(false); @@ -376,25 +363,8 @@ export default function SalesOrderPage() { useEffect(() => { fetchOrders(); }, [fetchOrders]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) - const columnUniqueValues = useMemo(() => { - const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { - const values = new Set(); - orders.forEach((row) => { - const val = row[col.key]; - if (val !== null && val !== undefined && val !== "") values.add(String(val)); - }); - result[col.key] = Array.from(values).sort(); - } - return result; - }, [orders]); - - // 마스터 필드 키 목록 (필터 분류용) - const MASTER_KEYS = new Set(["order_no", ...MASTER_BODY_LAYOUT.map((c) => c.key), "memo"]); - - // 카테고리 코드→라벨 변환 (마스터 필터용) - const resolveMasterLabel = useCallback((key: string, code: string) => { + // 카테고리 코드→라벨 변환 + const resolveLabel = useCallback((key: string, code: string) => { if (!code) return ""; if (key === "partner_id" || key === "manager_id" || key === "price_mode") { return categoryOptions[key]?.find((o) => o.code === code)?.label || code; @@ -402,106 +372,60 @@ export default function SalesOrderPage() { return code; }, [categoryOptions]); - // 필터 + 정렬 적용된 데이터 → 그룹핑 - const filteredOrderGroups = useMemo(() => { - // 1차: order_no 기준 그룹핑 (필터 전) - const allGroups: Record = {}; - for (const row of orders) { - const key = row.order_no || "_no_order"; - if (!allGroups[key]) { - allGroups[key] = { master: row._master || {}, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위 필터링) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - const raw = group.master?.[colKey] ?? ""; - const label = resolveMasterLabel(colKey, String(raw)); - return values.has(label) || values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위 필터링) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([orderNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - const cellVal = row[colKey] != null ? String(row[colKey]) : ""; - return values.has(cellVal); - }) - ); - return [orderNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - // 마스터 필드 정렬 → 그룹 단위 - entries.sort(([, a], [, b]) => { - const av = a.master?.[key] ?? ""; - const bv = b.master?.[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - // 디테일 필드 정렬 → 각 그룹 내 디테일 정렬 - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - const av = a[key] ?? ""; - const bv = b[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [orders, headerFilters, sortState, resolveMasterLabel]); - - // 마스터 컬럼별 고유값 (마스터 헤더 필터용) - const masterUniqueValues = useMemo(() => { - const result: Record = {}; - // 필터 전 전체 마스터에서 고유값 추출 - const seenMasters = new Map(); - orders.forEach((row) => { - if (row.order_no && row._master && !seenMasters.has(row.order_no)) { - seenMasters.set(row.order_no, row._master); - } + // 플랫 행 생성 (마스터 필드를 각 디테일 행에 병합) + const flatRows = useMemo(() => { + return orders.map((row) => { + const master = row._master || {}; + return { + ...row, + partner_id: resolveLabel("partner_id", master.partner_id || row.partner_id || ""), + order_date: master.order_date || row.order_date || "", + memo: row.memo || master.memo || "", + }; }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "order_no", label: "수주번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label })), { key: "memo", label: "메모" }]) { + }, [orders, resolveLabel]); + + // 컬럼별 고유값 (헤더 필터용) + const columnUniqueValues = useMemo(() => { + const result: Record = {}; + for (const col of FLAT_COLUMNS) { const values = new Set(); - masters.forEach((m) => { - const val = m?.[col.key]; - if (val !== null && val !== undefined && val !== "") { - values.add(resolveMasterLabel(col.key, String(val))); - } + flatRows.forEach((row) => { + const val = row[col.key]; + if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [orders, resolveMasterLabel]); + }, [flatRows]); + + // 필터 + 정렬 적용된 플랫 데이터 + const filteredFlatRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = row[colKey] != null ? String(row[colKey]) : ""; + return values.has(cellVal); + }); + } + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = a[key] ?? ""; + const bv = b[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -965,111 +889,70 @@ export default function SalesOrderPage() {
- {/* 데이터 테이블 (트리 구조) */} + {/* 데이터 테이블 (플랫 리스트) */}
- {/* 체크박스 */} - {/* 펼침 화살표 */} - {/* 수주번호 */} - {/* 품번 / 거래처 */} - {/* 품명 / 거래처(cont) */} - {/* 규격 / 단가방식 */} - {/* 단위 / 납품처 */} - {/* 수량 / 납품처(cont) */} - {/* 출하수량 / 납품장소 */} - {/* 잔량 / 납품장소(cont) */} - {/* 단가 / 수주일 */} - {/* 금액 / 수주일(cont) */} - {/* 통화 / 담당자 */} - {/* 납기일 / 담당자(cont) */} - {/* 메모 */} + + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredOrderGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredFlatRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredOrderGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredFlatRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 수주번호 (별도 컬럼) */} - -
-
handleSort("order_no")}> - 수주번호 - {sortState?.key === "order_no" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["order_no"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (colSpan으로 디테일 컬럼 위에 맵핑) */} - {MASTER_BODY_LAYOUT.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {FLAT_COLUMNS.map((col) => { + const isRight = ["qty", "ship_qty", "balance_qty", "unit_price", "amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} - {/* 메모 (마스터) */} - -
-
handleSort("memo")}> - 메모 - {sortState?.key === "memo" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["memo"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
+ + ); + })} @@ -1079,7 +962,7 @@ export default function SalesOrderPage() { - ) : Object.keys(filteredOrderGroups).length === 0 ? ( + ) : filteredFlatRows.length === 0 ? (
@@ -1089,200 +972,48 @@ export default function SalesOrderPage() { ) : ( - Object.entries(filteredOrderGroups).map(([orderNo, group]) => { - const isExpanded = expandedOrders.has(orderNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredFlatRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 — 마스터 테이블 필드만 표시 */} - { - if (expandedOrders.has(orderNo)) { - setClosingOrders((prev) => new Set(prev).add(orderNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(orderNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(orderNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(orderNo)); - } - }} - onDoubleClick={() => openEditModal(orderNo)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 수주번호 */} - - {orderNo} - ({group.details.length}) - - {/* 거래처 (colSpan=2) */} - - - {master.partner_id ? (categoryOptions["partner_id"]?.find((o) => o.code === master.partner_id)?.label || master.partner_id) : ""} - - - {/* 단가방식 (colSpan=1) */} - - - {master.price_mode ? (categoryOptions["price_mode"]?.find((o) => o.code === master.price_mode)?.label || master.price_mode) : ""} - - - {/* 납품처 (colSpan=2) */} - - {master.delivery_partner_id || ""} - - {/* 납품장소 (colSpan=2) */} - - {master.delivery_address || ""} - - {/* 수주일 (colSpan=2) */} - - {master.order_date || ""} - - {/* 담당자 (colSpan=2) */} - - - {master.manager_id ? (categoryOptions["manager_id"]?.find((o) => o.code === master.manager_id)?.label || master.manager_id) : ""} - - - {/* 메모 */} - - {master.memo || ""} - - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - {/* 수주번호 컬럼 빈 셀 */} - {DETAIL_HEADER_COLS.map((col) => { - const isRight = ["qty", "ship_qty", "balance_qty", "unit_price", "amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => d[col.key]).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} - -
+ { - const isClosing = closingOrders.has(orderNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - onDoubleClick={() => openEditModal(row.order_no)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - {/* 수주번호 컬럼 빈 셀 */} - {row.part_code} - {row.part_name} - {row.spec} - {row.unit} - {row.qty ? Number(row.qty).toLocaleString() : ""} - {row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""} - {row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {row.amount ? Number(row.amount).toLocaleString() : ""} - {row.currency_code || ""} - {row.due_date || ""} - - + onClick={() => { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] ); - })} - + }} + onDoubleClick={() => openEditModal(row.order_no)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + > + {}} /> + + {row.order_no} + {row.partner_id || ""} + {row.order_date || ""} + {row.part_code} + {row.part_name} + {row.spec} + {row.unit} + {row.qty ? Number(row.qty).toLocaleString() : ""} + {row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""} + {row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.amount ? Number(row.amount).toLocaleString() : ""} + {row.due_date || ""} + {row.memo || ""} + ); }) )} diff --git a/frontend/app/(main)/COMPANY_7/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_7/sales/shipping-order/page.tsx index 4ab5a9ad..2ed29b40 100644 --- a/frontend/app/(main)/COMPANY_7/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_7/sales/shipping-order/page.tsx @@ -363,7 +363,7 @@ export default function ShippingOrderPage() { spec: item.spec, material: item.material, orderQty: item.orderQty, - planQty: item.planQty, + planQty: item.orderQty, shipQty: 0, sourceType: item.sourceType, shipmentPlanId: item.shipmentPlanId, diff --git a/frontend/app/(main)/COMPANY_8/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_8/equipment/info/page.tsx index eeb63844..cd53e9b1 100644 --- a/frontend/app/(main)/COMPANY_8/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_8/equipment/info/page.tsx @@ -142,15 +142,20 @@ export default function EquipmentInfoPage() { }; const mainTableColumns = useMemo(() => { - const cols: EDataTableColumn[] = []; - if (ts.isVisible("equipment_code")) cols.push({ key: "equipment_code", label: "설비코드", width: "w-[110px]" }); - if (ts.isVisible("equipment_name")) cols.push({ key: "equipment_name", label: "설비명", minWidth: "min-w-[130px]", truncate: true, render: (v) => v || "-" }); - if (ts.isVisible("equipment_type")) cols.push({ key: "equipment_type", label: "설비유형", width: "w-[90px]", render: (v) => v || "-" }); - if (ts.isVisible("manufacturer")) cols.push({ key: "manufacturer", label: "제조사", width: "w-[100px]", render: (v) => v || "-" }); - if (ts.isVisible("installation_location")) cols.push({ key: "installation_location", label: "설치장소", width: "w-[100px]", render: (v) => v || "-" }); - if (ts.isVisible("operation_status")) cols.push({ key: "operation_status", label: "가동상태", width: "w-[80px]", render: (v) => v || "-" }); - return cols; - }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + 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 || "-" }, + manufacturer: { width: "w-[100px]", render: (v) => v || "-" }, + installation_location: { width: "w-[100px]", render: (v) => v || "-" }, + operation_status: { width: "w-[80px]", render: (v) => v || "-" }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 설비 조회 const fetchEquipments = useCallback(async () => { @@ -272,8 +277,8 @@ export default function EquipmentInfoPage() { if (!inspectionForm.inspection_cycle) { toast.error("점검주기는 필수입니다."); return; } if (!inspectionForm.inspection_method) { toast.error("점검방법은 필수입니다."); return; } const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; - if (isNumeric && !inspectionForm.unit) { toast.error("숫자 점검방법은 측정단위가 필수입니다."); return; } + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); + if (isNumeric && !inspectionForm.unit) { toast.error("측정단위가 필수입니다."); return; } // 기준값/오차범위 → 하한치/상한치 자동 계산 const saveData = { ...inspectionForm }; if (isNumeric && saveData.standard_value) { @@ -739,7 +744,7 @@ export default function EquipmentInfoPage() {
{catSelect("inspection_method", inspectionForm.inspection_method, (v) => { const label = resolve("inspection_method", v); - const isNum = label === "숫자" || v === "숫자"; + const isNum = ["숫자", "치수검사"].includes(label) || ["숫자", "치수검사"].includes(v); if (!isNum) { setInspectionForm((p) => ({ ...p, inspection_method: v, unit: "", standard_value: "", tolerance: "", lower_limit: "", upper_limit: "" })); } else { @@ -748,7 +753,7 @@ export default function EquipmentInfoPage() { }, "점검방법")}
{(() => { const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); if (!isNumeric) return null; return (
@@ -758,7 +763,7 @@ export default function EquipmentInfoPage() {
{(() => { const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); if (!isNumeric) return null; return (
diff --git a/frontend/app/(main)/COMPANY_8/logistics/material-status/page.tsx b/frontend/app/(main)/COMPANY_8/logistics/material-status/page.tsx index 46de306c..eb87ba92 100644 --- a/frontend/app/(main)/COMPANY_8/logistics/material-status/page.tsx +++ b/frontend/app/(main)/COMPANY_8/logistics/material-status/page.tsx @@ -333,69 +333,90 @@ export default function MaterialStatusPage() {

) : ( - workOrders.map((wo) => ( -
handleSelectWo(wo.id)} - > + ts.groupData(workOrders).map((wo) => { + if ((wo as any)._isGroupSummary || (wo as any)._isGroupHeader) return null; + return (
e.stopPropagation()} + key={wo.id} + className={cn( + "flex gap-3 rounded-lg border p-3 transition-all cursor-pointer", + "hover:border-primary/50 hover:shadow-sm", + selectedWoId === wo.id + ? "border-primary bg-primary/5 shadow-sm" + : "border-border" + )} + onClick={() => handleSelectWo(wo.id)} > - - handleCheckWo(wo.id, c as boolean) - } - /> -
-
-
- - {wo.plan_no || wo.work_order_no || `WO-${wo.id}`} - - e.stopPropagation()} + > + + handleCheckWo(wo.id, c as boolean) + } + /> +
+
+
+ {ts.isVisible("plan_no") && ( + + {wo.plan_no || wo.work_order_no || `WO-${wo.id}`} + )} - > - {getStatusLabel(wo.status)} - -
-
- - {wo.item_name} - - - ({wo.item_code}) - -
-
- 수량: - - {Number(wo.plan_qty).toLocaleString()}개 - - | - 일자: - - {wo.plan_date - ? new Date(wo.plan_date) - .toISOString() - .slice(0, 10) - : "-"} - + {ts.isVisible("status") && ( + + {getStatusLabel(wo.status)} + + )} +
+
+ {ts.isVisible("item_name") && ( + + {wo.item_name} + + )} + {ts.isVisible("item_code") && ( + + ({wo.item_code}) + + )} +
+
+ {ts.isVisible("plan_qty") && ( + <> + 수량: + + {Number(wo.plan_qty).toLocaleString()}개 + + + )} + {ts.isVisible("plan_qty") && ts.isVisible("plan_date") && ( + | + )} + {ts.isVisible("plan_date") && ( + <> + 일자: + + {wo.plan_date + ? new Date(wo.plan_date) + .toISOString() + .slice(0, 10) + : "-"} + + + )} +
-
- )) + ); + }) )}
diff --git a/frontend/app/(main)/COMPANY_8/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_8/logistics/outbound/page.tsx index 5ab46a8a..c1ffbd40 100644 --- a/frontend/app/(main)/COMPANY_8/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_8/logistics/outbound/page.tsx @@ -140,8 +140,16 @@ const DETAIL_HEADER_COLS = [ // 마스터 필드 키 목록 (필터 분류용) const MASTER_KEYS = new Set(["outbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 출고번호(1) + 마스터필드(7) = 10 -const TOTAL_COLS = 10; +// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) +const DETAIL_KEY_MAP: Record = { + source_type: "source_type", + item_number: "item_code", + item_name: "item_name", + spec: "specification", + outbound_qty: "outbound_qty", + unit_price: "unit_price", + total_amount: "total_amount", +}; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -248,6 +256,31 @@ interface SelectedSourceItem { export default function OutboundPage() { const ts = useTableSettings("c16-outbound", "outbound_mng", GRID_COLUMNS); + + // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 + const visibleMasterLayout = useMemo(() => { + const ordered: typeof MASTER_BODY_LAYOUT = []; + for (const vc of ts.visibleColumns) { + const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); + if (m) ordered.push(m); + } + return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; + }, [ts.visibleColumns]); + + const visibleDetailCols = useMemo(() => { + const ordered: typeof DETAIL_HEADER_COLS = []; + for (const vc of ts.visibleColumns) { + const detailKey = DETAIL_KEY_MAP[vc.key]; + if (detailKey) { + const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); + if (d) ordered.push(d); + } + } + return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; + }, [ts.visibleColumns]); + + const TOTAL_COLS = 3 + visibleMasterLayout.length; + // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -900,8 +933,15 @@ export default function OutboundPage() {
-
- +
+ + + + + {visibleMasterLayout.map((col) => ( + + ))} + - {/* 마스터 필드 헤더 */} - {MASTER_BODY_LAYOUT.map((col) => ( + {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => (
handleSort(col.key)}> @@ -1039,38 +1079,51 @@ export default function OutboundPage() { {outboundNo} ({group.details.length}) - {/* 출고유형 */} - - - {master.outbound_type || "-"} - - - {/* 출고일 */} - - {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} - - {/* 참조번호 */} - - {master.reference_number || ""} - - {/* 거래처 */} - - {master.customer_name || ""} - - {/* 창고 */} - - {master.warehouse_name || master.warehouse_code || ""} - - {/* 출고상태 */} - - - {master.outbound_status || "-"} - - - {/* 비고 */} - - {master.memo || ""} - + {/* 마스터 필드 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => { + switch (col.key) { + case "outbound_type": return ( + + + {master.outbound_type || "-"} + + + ); + case "outbound_date": return ( + + {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} + + ); + case "reference_number": return ( + + {master.reference_number || ""} + + ); + case "customer_name": return ( + + {master.customer_name || ""} + + ); + case "warehouse_name": return ( + + {master.warehouse_name || master.warehouse_code || ""} + + ); + case "outbound_status": return ( + + + {master.outbound_status || "-"} + + + ); + case "memo": return ( + + {master.memo || ""} + + ); + default: return {(master as any)[col.key] ?? ""}; + } + })} {/* 디테일 서브 헤더 (펼쳤을 때만) */} @@ -1084,7 +1137,7 @@ export default function OutboundPage() { - {DETAIL_HEADER_COLS.map((col) => { + {visibleDetailCols.map((col) => { const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); const isSorted = sortState?.key === col.key; const uniqueVals = Array.from(new Set( @@ -1163,20 +1216,18 @@ export default function OutboundPage() {
- {/* 출처 */} - {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"} - {/* 품목코드 */} - {row.item_code || ""} - {/* 품목명 */} - {row.item_name || ""} - {/* 규격 */} - {row.specification || ""} - {/* 출고수량 */} - {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""} - {/* 단가 */} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {/* 금액 */} - {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {visibleDetailCols.map((col) => { + switch (col.key) { + case "source_type": return {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"}; + case "item_code": return {row.item_code || ""}; + case "item_name": return {row.item_name || ""}; + case "specification": return {row.specification || ""}; + case "outbound_qty": return {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""}; + case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; + case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; + default: return {(row as any)[col.key] ?? ""}; + } + })} ); })} diff --git a/frontend/app/(main)/COMPANY_8/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_8/logistics/packaging/page.tsx index 5d4d5787..6ae340aa 100644 --- a/frontend/app/(main)/COMPANY_8/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_8/logistics/packaging/page.tsx @@ -460,18 +460,20 @@ export default function PackagingPage() { {/* 포장재 목록 테이블 */}
PKG_TYPE_LABEL[v] || v || "-" }, - { key: "size", label: "크기(mm)", width: "w-[100px]", render: (_v, row) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, - { key: "max_load_kg", label: "최대중량", width: "w-[80px]", align: "right", render: (v) => Number(v || 0) > 0 ? `${v}kg` : "-" }, - { key: "status", label: "상태", width: "w-[60px]", align: "center", render: (v) => ( - - {STATUS_LABEL[v] || v} - - )}, - ] as EDataTableColumn[]} + columns={ts.visibleColumns.map((col): EDataTableColumn => { + const renderMap: Record>> = { + pkg_type: { width: "w-[80px]", render: (v: any) => PKG_TYPE_LABEL[v] || v || "-" }, + size: { width: "w-[100px]", render: (_v: any, row: any) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, + max_weight: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, + max_load_kg: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, + status: { width: "w-[60px]", align: "center", render: (v: any) => ( + + {STATUS_LABEL[v] || v} + + )}, + }; + return { key: col.key, label: col.label, ...renderMap[col.key] }; + })} data={ts.groupData(filteredPkgUnits)} rowKey={(row) => String(row.id)} loading={pkgLoading} diff --git a/frontend/app/(main)/COMPANY_8/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_8/logistics/receiving/page.tsx index a8d5fc2c..85cdc23c 100644 --- a/frontend/app/(main)/COMPANY_8/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_8/logistics/receiving/page.tsx @@ -117,12 +117,20 @@ const DETAIL_HEADER_COLS = [ { key: "total_amount", label: "금액" }, ]; -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 입고번호(1) + 디테일(7) = 10 -const TOTAL_COLS = 10; - // 마스터 필드 키 목록 (필터 분류용) const MASTER_KEYS = new Set(["inbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); +// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) +const DETAIL_KEY_MAP: Record = { + source_type: "source_table", + item_number: "item_number", + item_name: "item_name", + spec: "spec", + inbound_qty: "inbound_qty", + unit_price: "unit_price", + total_amount: "total_amount", +}; + // 헤더 필터 Popover function HeaderFilterPopover({ colKey, colLabel, uniqueValues, filterValues, onToggle, onClear, @@ -278,6 +286,31 @@ interface SelectedSourceItem { export default function ReceivingPage() { const ts = useTableSettings("c16-receiving", "inbound_mng", GRID_COLUMNS); + + // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 + const visibleMasterLayout = useMemo(() => { + const ordered: typeof MASTER_BODY_LAYOUT = []; + for (const vc of ts.visibleColumns) { + const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); + if (m) ordered.push(m); + } + return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; + }, [ts.visibleColumns]); + + const visibleDetailCols = useMemo(() => { + const ordered: typeof DETAIL_HEADER_COLS = []; + for (const vc of ts.visibleColumns) { + const detailKey = DETAIL_KEY_MAP[vc.key]; + if (detailKey) { + const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); + if (d) ordered.push(d); + } + } + return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; + }, [ts.visibleColumns]); + + const TOTAL_COLS = 3 + visibleMasterLayout.length; + // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -847,8 +880,15 @@ export default function ReceivingPage() {
-
- +
+ + + + + {visibleMasterLayout.map((col) => ( + + ))} + - {/* 마스터 필드 헤더 (colSpan으로 디테일 컬럼 위에 맵핑) */} - {MASTER_BODY_LAYOUT.map((col) => ( + {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => (
handleSort(col.key)}> @@ -985,38 +1025,51 @@ export default function ReceivingPage() { {inboundNo} ({group.details.length}) - {/* 입고유형 */} - - - {resolveInboundType(master.inbound_type)} - - - {/* 입고일 */} - - {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} - - {/* 참조번호 */} - - {master.reference_number || ""} - - {/* 공급처 */} - - {master.supplier_name || ""} - - {/* 창고 */} - - {master.warehouse_name || master.warehouse_code || ""} - - {/* 입고상태 */} - - - {master.inbound_status || "-"} - - - {/* 비고 */} - - {master.memo || ""} - + {/* 마스터 필드 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => { + switch (col.key) { + case "inbound_type": return ( + + + {resolveInboundType(master.inbound_type)} + + + ); + case "inbound_date": return ( + + {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} + + ); + case "reference_number": return ( + + {master.reference_number || ""} + + ); + case "supplier_name": return ( + + {master.supplier_name || ""} + + ); + case "warehouse_name": return ( + + {master.warehouse_name || master.warehouse_code || ""} + + ); + case "inbound_status": return ( + + + {master.inbound_status || "-"} + + + ); + case "memo": return ( + + {master.memo || ""} + + ); + default: return {(master as any)[col.key] ?? ""}; + } + })} {/* 디테일 서브 헤더 (펼쳤을 때만) */} @@ -1030,7 +1083,7 @@ export default function ReceivingPage() { - {DETAIL_HEADER_COLS.map((col) => { + {visibleDetailCols.map((col) => { const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); const isSorted = sortState?.key === col.key; const uniqueVals = Array.from(new Set( @@ -1108,20 +1161,18 @@ export default function ReceivingPage() {
- {/* 출처 */} - {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"} - {/* 품목코드 */} - {row.item_number || ""} - {/* 품목명 */} - {row.item_name || ""} - {/* 규격 */} - {row.spec || ""} - {/* 입고수량 */} - {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""} - {/* 단가 */} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {/* 금액 */} - {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {visibleDetailCols.map((col) => { + switch (col.key) { + case "source_table": return {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"}; + case "item_number": return {row.item_number || ""}; + case "item_name": return {row.item_name || ""}; + case "spec": return {row.spec || ""}; + case "inbound_qty": return {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""}; + case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; + case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; + default: return {(row as any)[col.key] ?? ""}; + } + })} ); })} 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 dfd1b666..9d7f2dea 100644 --- a/frontend/app/(main)/COMPANY_8/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_8/master-data/company/page.tsx @@ -491,12 +491,6 @@ export default function CompanyPage() { > 회사정보 - - 부서관리 -
@@ -635,89 +629,6 @@ export default function CompanyPage() {
- {/* ===================== Tab 2: 부서관리 ===================== */} - -
- - {/* 좌측: 부서 트리 */} - -
-
-
- - 부서 - {depts.length}건 -
-
- - - -
-
-
- {deptLoading ? ( -
- -
- ) : deptTree.length === 0 ? ( -
- - 등록된 부서가 없어요 -
- ) : ( - renderTree(deptTree) - )} -
-
-
- - - - {/* 우측: 사원 목록 */} - -
-
-
- - {selectedDept ? "부서 인원" : "부서를 선택해주세요"} - {selectedDept && {selectedDept.dept_name}} - {members.length > 0 && {members.length}명} -
- {selectedDeptCode && ( - - )} -
- {selectedDeptCode ? ( - row.user_id || row.id} - loading={memberLoading} - emptyMessage="소속 사원이 없어요" - emptyIcon={} - onRowDoubleClick={(row) => openUserModal(row)} - showPagination={false} - draggableColumns={false} - /> - ) : ( -
- - 좌측에서 부서를 선택해주세요 -
- )} -
-
-
-
-
{/* ── 부서 등록/수정 모달 ── */} diff --git a/frontend/app/(main)/COMPANY_8/master-data/department/page.tsx b/frontend/app/(main)/COMPANY_8/master-data/department/page.tsx index a2bbcba5..3245571e 100644 --- a/frontend/app/(main)/COMPANY_8/master-data/department/page.tsx +++ b/frontend/app/(main)/COMPANY_8/master-data/department/page.tsx @@ -9,7 +9,7 @@ * 모달: 부서 등록(dept_info), 사원 추가(user_info) */ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -279,6 +279,7 @@ export default function DepartmentPage() { dept_code: userForm.dept_code || undefined, dept_name: depts.find((d) => d.dept_code === userForm.dept_code)?.dept_name || undefined, status: userForm.status || "active", + end_date: userForm.end_date || null, }, mainDept: userForm.dept_code ? { dept_code: userForm.dept_code, @@ -312,37 +313,40 @@ export default function DepartmentPage() { const activeMembers = members.filter((m) => !m.end_date || m.end_date.substring(0, 10) >= today); const resignedMembers = members.filter((m) => m.end_date && m.end_date.substring(0, 10) < today); - const isColVisible = (key: string) => ts.isVisible(key); - - // EDataTable 컬럼 정의 (부서 목록) - const deptColumns: EDataTableColumn[] = [ - { key: "dept_code", label: "부서코드", width: "w-[120px]" }, - { key: "dept_name", label: "부서명", minWidth: "min-w-[140px]" }, - ...(isColVisible("parent_dept_code") - ? [{ - key: "parent_dept_code", - label: "상위부서", - width: "w-[110px]", - render: (val: any) => {val || "\u2014"}, - }] - : []), - ...(isColVisible("status") - ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val === "active" ? "활성" : (val || "\u2014")} - - ) : null, - }] - : []), - ]; + // EDataTable 컬럼 정의 (부서 목록) — ts.visibleColumns 순서를 따름 + const deptColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + dept_code: { width: "w-[120px]" }, + dept_name: { minWidth: "min-w-[140px]" }, + parent_dept_code: { + width: "w-[110px]", + render: (val: any) => {val || "\u2014"}, + }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val === "active" ? "활성" : (val || "\u2014")} + + ) : null, + }, + }; + // dept_code, dept_name은 항상 표시 (DEPT_COLUMNS에 포함되지 않으므로 visibleColumns에 없음) + const fixedCols: EDataTableColumn[] = [ + { key: "dept_code", label: "부서코드", ...colProps["dept_code"] }, + { key: "dept_name", label: "부서명", ...colProps["dept_name"] }, + ]; + const dynamicCols = ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + return [...fixedCols, ...dynamicCols]; + }, [ts.visibleColumns]); return (
diff --git a/frontend/app/(main)/COMPANY_8/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_8/master-data/item-info/page.tsx index 3f037275..375fd900 100644 --- a/frontend/app/(main)/COMPANY_8/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_8/master-data/item-info/page.tsx @@ -84,6 +84,56 @@ function CategoryCombobox({ options, value, onChange, placeholder }: { ); } +// 다중 선택 카테고리 콤보박스 +function MultiCategoryCombobox({ options, value, onChange, placeholder }: { + options: { code: string; label: string }[]; + value: string; + onChange: (v: string) => void; + placeholder: string; +}) { + const [open, setOpen] = useState(false); + const selectedCodes = value ? value.split(",").map((c) => c.trim()).filter(Boolean) : []; + const selectedLabels = selectedCodes.map((code) => options.find((o) => o.code === code)?.label || code).filter(Boolean); + + const toggle = (code: string) => { + const next = selectedCodes.includes(code) + ? selectedCodes.filter((c) => c !== code) + : [...selectedCodes, code]; + onChange(next.join(",")); + }; + + return ( + + + + + + + + + 검색 결과가 없어요 + + {options.map((opt) => ( + toggle(opt.code)}> + + {opt.label} + + ))} + + + + + + ); +} + const TABLE_NAME = "item_info"; const GRID_COLUMNS = [ @@ -108,7 +158,7 @@ const GRID_COLUMNS = [ const FORM_FIELDS = [ { key: "item_number", label: "품목코드", type: "text", required: true, disabled: true, placeholder: "자동 채번" }, { key: "item_name", label: "품명", type: "text", required: true }, - { key: "division", label: "관리품목", type: "category" }, + { key: "division", label: "관리품목", type: "multi-category" }, { key: "type", label: "품목구분", type: "category" }, { key: "size", label: "규격", type: "text" }, { key: "unit", label: "단위", type: "category" }, @@ -137,6 +187,7 @@ export default function ItemInfoPage() { const { user } = useAuth(); const ts = useTableSettings("c16-item-info", TABLE_NAME, GRID_COLUMNS); const [items, setItems] = useState([]); + const [rawItems, setRawItems] = useState([]); const [loading, setLoading] = useState(false); // 검색 필터 (DynamicSearchFilter) @@ -215,6 +266,7 @@ export default function ItemInfoPage() { } return categoryOptions[col]?.find((o) => o.code === code)?.label || code; }; + setRawItems(raw); const data = raw.map((r: any) => { const converted = { ...r }; for (const col of CATEGORY_COLUMNS) { @@ -261,7 +313,8 @@ export default function ItemInfoPage() { // 수정 모달 열기 const openEditModal = (item: any) => { - setFormData({ ...item }); + const raw = rawItems.find((r) => r.id === item.id) || item; + setFormData({ ...raw }); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); @@ -269,7 +322,8 @@ export default function ItemInfoPage() { // 복사 모달 열기 const openCopyModal = async (item: any) => { - const { id, item_number, created_date, updated_date, writer, ...rest } = item; + const raw = rawItems.find((r) => r.id === item.id) || item; + const { id, item_number, created_date, updated_date, writer, ...rest } = raw; setFormData(rest); setIsEditMode(false); setEditId(null); @@ -459,6 +513,13 @@ export default function ItemInfoPage() { columnName={field.key} height="h-32" /> + ) : field.type === "multi-category" ? ( + setFormData((prev) => ({ ...prev, [field.key]: v }))} + placeholder={`${field.label} 선택`} + /> ) : field.type === "category" ? ( (() => { - const cols: EDataTableColumn[] = []; - if (ts.isVisible("item_number")) cols.push({ key: "item_number", label: "품번", width: "w-[110px]" }); - if (ts.isVisible("item_name")) cols.push({ key: "item_name", label: "품명", minWidth: "min-w-[130px]", render: (v) => v || "-" }); - if (ts.isVisible("size")) cols.push({ key: "size", label: "규격", width: "w-[90px]", render: (v) => v || "-" }); - if (ts.isVisible("unit")) cols.push({ key: "unit", label: "단위", width: "w-[60px]", render: (v) => v || "-" }); - if (ts.isVisible("standard_price")) cols.push({ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }); - if (ts.isVisible("selling_price")) cols.push({ key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true }); - if (ts.isVisible("currency_code")) cols.push({ key: "currency_code", label: "통화", width: "w-[50px]", render: (v) => v || "-" }); - if (ts.isVisible("status")) cols.push({ key: "status", label: "상태", width: "w-[60px]", render: (v) => v || "-" }); - return cols; - }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + const colProps: Record> = { + item_number: { width: "w-[110px]" }, + item_name: { minWidth: "min-w-[130px]", render: (v) => v || "-" }, + size: { width: "w-[90px]", render: (v) => v || "-" }, + unit: { width: "w-[60px]", render: (v) => v || "-" }, + standard_price: { width: "w-[90px]", align: "right", formatNumber: true }, + selling_price: { width: "w-[90px]", align: "right", formatNumber: true }, + currency_code: { width: "w-[50px]", render: (v) => v || "-" }, + status: { width: "w-[60px]", render: (v) => v || "-" }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 좌측: 품목 조회 (division이 "외주관리"인 품목만 필터링) const outsourcingDivisionCode = categoryOptions["division"]?.find( diff --git a/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx index 4a0d341a..6bd63176 100644 --- a/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx @@ -919,9 +919,7 @@ export default function ProductionPlanManagementPage() { // 숫자 포맷 const formatNumber = (num: number | string) => Number(num).toLocaleString(); - // 컬럼 표시 여부 - const isColVisible = (key: string) => ts.isVisible(key); - const orderColSpan = 4 + ORDER_COLUMNS.filter((c) => isColVisible(c.key)).length; + // (컬럼 표시는 ts.visibleColumns 순서를 따름) return (
@@ -1019,6 +1017,38 @@ export default function ProductionPlanManagementPage() {
) : (
+ {(() => { + // 디테일 행에서 개별 값을 표시하는 컬럼 매핑 + const DETAIL_VALUE_MAP: Record = { + total_order_qty: "order_qty", + total_ship_qty: "ship_qty", + total_balance_qty: "balance_qty", + }; + + // 그룹 행에서 특수 렌더링이 필요한 컬럼 + const renderGroupCell = (col: { key: string }, item: any) => { + if (col.key === "required_plan_qty") { + return ( + 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> + {formatNumber(item.required_plan_qty)} + + ); + } + if (col.key === "lead_time") { + return ( + toggleItemExpand(item.item_code)}> + {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"} + + ); + } + return ( + toggleItemExpand(item.item_code)}> + {formatNumber(item[col.key])} + + ); + }; + + return (
@@ -1028,15 +1058,11 @@ export default function ProductionPlanManagementPage() { 품목코드 품목명 - {isColVisible("total_order_qty") && 총수주량} - {isColVisible("total_ship_qty") && 출고량} - {isColVisible("total_balance_qty") && 잔량} - {isColVisible("current_stock") && 현재고} - {isColVisible("safety_stock") && 안전재고} - {isColVisible("existing_plan_qty") && 기생산계획량} - {isColVisible("in_progress_qty") && 생산진행} - {isColVisible("required_plan_qty") && 필요생산계획} - {isColVisible("lead_time") && 리드타임(일)} + {ts.visibleColumns.map((col) => ( + + {col.label} + + ))} @@ -1046,6 +1072,7 @@ export default function ProductionPlanManagementPage() { + {ts.visibleColumns.map((col) => { const v = (item as any)[col.key]; return ( @@ -1068,25 +1095,14 @@ export default function ProductionPlanManagementPage() { toggleItemExpand(item.item_code)}>{item.item_code} toggleItemExpand(item.item_code)}>{item.item_name} - {isColVisible("total_order_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_order_qty)}} - {isColVisible("total_ship_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_ship_qty)}} - {isColVisible("total_balance_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_balance_qty)}} - {isColVisible("current_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.current_stock)}} - {isColVisible("safety_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.safety_stock)}} - {isColVisible("existing_plan_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.existing_plan_qty)}} - {isColVisible("in_progress_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.in_progress_qty)}} - {isColVisible("required_plan_qty") && ( - 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> - {formatNumber(item.required_plan_qty)} - - )} - {isColVisible("lead_time") && ( - toggleItemExpand(item.item_code)}> - {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"} - - )} + {ts.visibleColumns.map((col) => renderGroupCell(col, item))} - {expandedItems.has(item.item_code) && item.orders?.map((detail) => ( + {expandedItems.has(item.item_code) && item.orders?.map((detail: any) => { + let remainColSpan = 0; + for (const col of ts.visibleColumns) { + if (!DETAIL_VALUE_MAP[col.key]) remainColSpan++; + } + return ( @@ -1101,19 +1117,28 @@ export default function ProductionPlanManagementPage() { - {isColVisible("total_order_qty") && {formatNumber(detail.order_qty)}} - {isColVisible("total_ship_qty") && {formatNumber(detail.ship_qty)}} - {isColVisible("total_balance_qty") && {formatNumber(detail.balance_qty)}} - - 납기일: {detail.due_date || "-"} - + {ts.visibleColumns.map((col) => { + const detailKey = DETAIL_VALUE_MAP[col.key]; + if (detailKey) { + return {formatNumber(detail[detailKey])}; + } + return null; + })} + {remainColSpan > 0 && ( + + 납기일: {detail.due_date || "-"} + + )} - ))} + ); + })} ); })}
+ ); + })()} )} diff --git a/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx index fa0e08c5..1bc3bc88 100644 --- a/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx @@ -742,10 +742,24 @@ export default function PurchaseOrderPage() { ) : ( (() => { const MASTER_KEYS = new Set(["purchase_no", "order_date", "supplier_name", "status", "memo"]); - const detailCols = ts.visibleColumns.filter(c => !MASTER_KEYS.has(c.key)); - const masterCols = ts.visibleColumns.filter(c => MASTER_KEYS.has(c.key)); const numCols = new Set(["order_qty", "received_qty", "remain_qty", "unit_price", "amount"]); + // ts.visibleColumns 순서를 따르되, 마스터/디테일 컬럼을 분리 + // 고정 컬럼(품목수)은 마스터 선행 컬럼 뒤에 배치 + const leadingMaster: typeof ts.visibleColumns = []; + const detailCols: typeof ts.visibleColumns = []; + const trailingMaster: typeof ts.visibleColumns = []; + let passedFirstDetail = false; + for (const col of ts.visibleColumns) { + if (MASTER_KEYS.has(col.key)) { + if (passedFirstDetail) trailingMaster.push(col); + else leadingMaster.push(col); + } else { + passedFirstDetail = true; + detailCols.push(col); + } + } + const renderDetailCell = (row: any, key: string) => { const val = row[key]; if (key === "status") return val ? {val} : "-"; @@ -753,23 +767,35 @@ export default function PurchaseOrderPage() { return val || "-"; }; + const renderMasterHead = (col: { key: string; label: string }) => ( + + {col.label} + + ); + + const renderMasterCell = (col: { key: string }, m: any, purchaseNo: string) => { + if (col.key === "purchase_no") return {purchaseNo}; + if (col.key === "order_date") return {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}; + if (col.key === "supplier_name") return {m.supplier_name || "-"}; + if (col.key === "status") return {m.status && {m.status}}; + if (col.key === "memo") return {m.memo || ""}; + return ; + }; + return ( - {ts.isVisible("purchase_no") && 발주번호} - {ts.isVisible("order_date") && 발주일} - {ts.isVisible("supplier_name") && 공급업체} + {leadingMaster.map(renderMasterHead)} 품목수 {detailCols.map(col => ( {col.label}{col.key === "order_qty" || col.key === "amount" ? " 합계" : ""} ))} - {ts.isVisible("status") && 상태} - {ts.isVisible("memo") && 메모} + {trailingMaster.map(renderMasterHead)} @@ -795,9 +821,7 @@ export default function PurchaseOrderPage() { { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !detailIds.includes(id)) : [...new Set([...prev, ...detailIds])]); }}> {}} /> - {ts.isVisible("purchase_no") && {purchaseNo}} - {ts.isVisible("order_date") && {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}} - {ts.isVisible("supplier_name") && {m.supplier_name || "-"}} + {leadingMaster.map(col => renderMasterCell(col, m, purchaseNo))} {group.details.length}건 {detailCols.map(col => ( @@ -806,8 +830,7 @@ export default function PurchaseOrderPage() { : ""} ))} - {ts.isVisible("status") && {m.status && {m.status}}} - {ts.isVisible("memo") && {m.memo || ""}} + {trailingMaster.map(col => renderMasterCell(col, m, purchaseNo))} {isExpanded && group.details.map((row) => ( @@ -815,17 +838,14 @@ export default function PurchaseOrderPage() { { e.stopPropagation(); setCheckedIds(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id]); }}> {}} /> - {ts.isVisible("purchase_no") && } - {ts.isVisible("order_date") && } - {ts.isVisible("supplier_name") && } + {leadingMaster.map(col => )} {detailCols.map(col => ( {renderDetailCell(row, col.key)} ))} - {ts.isVisible("status") && } - {ts.isVisible("memo") && } + {trailingMaster.map(col => )} ))} diff --git a/frontend/app/(main)/COMPANY_8/purchase/purchase-item/page.tsx b/frontend/app/(main)/COMPANY_8/purchase/purchase-item/page.tsx index 1f7c4137..7f211a88 100644 --- a/frontend/app/(main)/COMPANY_8/purchase/purchase-item/page.tsx +++ b/frontend/app/(main)/COMPANY_8/purchase/purchase-item/page.tsx @@ -617,17 +617,21 @@ export default function PurchaseItemPage() { toast.success("다운로드 완료"); }; - // EDataTable 컬럼 정의 (구매품목) - const itemColumns: EDataTableColumn[] = [ - { key: "item_number", label: "품번", width: "w-[110px]" }, - { key: "item_name", label: "품명", minWidth: "min-w-[130px]" }, - { key: "size", label: "규격", width: "w-[80px]" }, - { key: "unit", label: "단위", width: "w-[60px]" }, - { key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }, - { key: "standard_price", label: "구매단가", width: "w-[90px]", align: "right", formatNumber: true }, - { key: "currency_code", label: "통화", width: "w-[50px]" }, - { key: "status", label: "상태", width: "w-[60px]" }, - ]; + // EDataTable 컬럼 정의 (구매품목) — ts.visibleColumns 기반 + const COLUMN_RENDER_MAP: Record> = { + item_number: { width: "w-[110px]" }, + item_name: { minWidth: "min-w-[130px]" }, + size: { width: "w-[80px]" }, + unit: { width: "w-[60px]" }, + standard_price: { width: "w-[90px]", align: "right", formatNumber: true }, + currency_code: { width: "w-[50px]" }, + status: { width: "w-[60px]" }, + }; + const itemColumns: EDataTableColumn[] = ts.visibleColumns.map((col): EDataTableColumn => ({ + key: col.key, + label: col.label, + ...COLUMN_RENDER_MAP[col.key], + })); return (
diff --git a/frontend/app/(main)/COMPANY_8/purchase/supplier/page.tsx b/frontend/app/(main)/COMPANY_8/purchase/supplier/page.tsx index 51c50aa5..521f770e 100644 --- a/frontend/app/(main)/COMPANY_8/purchase/supplier/page.tsx +++ b/frontend/app/(main)/COMPANY_8/purchase/supplier/page.tsx @@ -12,7 +12,7 @@ * - 납품처 등록 (delivery_destination) */ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -1229,47 +1229,44 @@ export default function SupplierManagementPage() { } }; - // 컬럼 가시성 헬퍼 - const isColumnVisible = (key: string) => ts.isVisible(key); - - const supplierColSpan = 1 + ["supplier_code", "supplier_name", "contact_person", "contact_phone", "division", "status"] - .filter((k) => isColumnVisible(k)).length; - - // EDataTable 컬럼 정의 (공급업체 목록) - const supplierColumns: EDataTableColumn[] = [ - ...(isColumnVisible("supplier_code") ? [{ key: "supplier_code", label: "공급업체코드", width: "w-[120px]" }] : []), - ...(isColumnVisible("supplier_name") ? [{ key: "supplier_name", label: "공급업체명", minWidth: "min-w-[140px]" }] : []), - ...(isColumnVisible("division") ? [{ - key: "division", - label: "공급업체유형", - width: "w-[80px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ...(isColumnVisible("contact_person") ? [{ key: "contact_person", label: "담당자", width: "w-[80px]" }] : []), - ...(isColumnVisible("contact_phone") ? [{ key: "contact_phone", label: "전화번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("email") ? [{ key: "email", label: "이메일", width: "w-[160px]" }] : []), - ...(isColumnVisible("business_number") ? [{ key: "business_number", label: "사업자번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("address") ? [{ key: "address", label: "주소", minWidth: "min-w-[150px]" }] : []), - ...(isColumnVisible("status") ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ]; + // EDataTable 컬럼 정의 (공급업체 목록) — ts.visibleColumns 순서를 따름 + const supplierColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + supplier_code: { width: "w-[120px]" }, + supplier_name: { minWidth: "min-w-[140px]" }, + division: { + width: "w-[80px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + contact_person: { width: "w-[80px]" }, + contact_phone: { width: "w-[120px]" }, + email: { width: "w-[160px]" }, + business_number: { width: "w-[120px]" }, + address: { minWidth: "min-w-[150px]" }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 엑셀 다운로드 const handleExcelDownload = async () => { diff --git a/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx index 5baf35d3..2c0e1338 100644 --- a/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx @@ -28,6 +28,7 @@ const GRID_COLUMNS = [ { key: "item_code", label: "품목코드" }, { key: "item_name", label: "품목명" }, { key: "inspection_type", label: "검사유형" }, + { key: "item_count", label: "항목수" }, { key: "is_active", label: "사용여부" }, ]; const ITEM_TABLE = "item_info"; @@ -420,18 +421,41 @@ export default function ItemInspectionInfoPage() { 0 && checkedIds.length === data.length} onCheckedChange={(v) => setCheckedIds(v ? data.map(r => r.id) : [])} /> - 품목코드 - 품목명 - 검사유형 - 항목수 - 사용여부 + {ts.visibleColumns.map((col) => ( + + {col.label} + + ))} - {groupedData.map((group) => { + {ts.groupData(groupedData).map((group) => { + if ((group as any)._isGroupSummary || (group as any)._isGroupHeader) return null; const isExpanded = expandedItems.has(group.item_code); - const groupIds = group.rows.map(r => r.id); - const allChecked = groupIds.every(id => checkedIds.includes(id)); + const groupIds = group.rows.map((r: any) => r.id); + const allChecked = groupIds.every((id: string) => checkedIds.includes(id)); + const renderCell = (key: string) => { + switch (key) { + case "item_code": return {group.item_code}; + case "item_name": return {group.item_name}; + case "inspection_type": return ( + +
+ {group.types.map((t: string) => {t})} +
+
+ ); + case "item_count": return {group.rows.filter((r: any) => r.inspection_standard_id).length}; + case "is_active": return ( + + + {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} + + + ); + default: return {(group as any)[key] ?? ""}; + } + }; return ( { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !groupIds.includes(id)) : [...new Set([...prev, ...groupIds])]); }}> {}} /> - {group.item_code} - {group.item_name} - -
- {group.types.map(t => {t})} -
-
- {group.rows.filter(r => r.inspection_standard_id).length} - - - {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} - - + {ts.visibleColumns.map((col) => renderCell(col.key))}
- {isExpanded && group.rows.filter(r => r.inspection_standard_id).map((row, i) => ( + {isExpanded && group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( diff --git a/frontend/app/(main)/COMPANY_8/sales/customer/page.tsx b/frontend/app/(main)/COMPANY_8/sales/customer/page.tsx index bddc7730..20b98727 100644 --- a/frontend/app/(main)/COMPANY_8/sales/customer/page.tsx +++ b/frontend/app/(main)/COMPANY_8/sales/customer/page.tsx @@ -12,7 +12,7 @@ * - 납품처 등록 (delivery_destination) */ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -820,12 +820,14 @@ export default function CustomerManagementPage() { const allItems = res.data?.data?.data || res.data?.data?.rows || []; setItemTotalCount(allItems.length); const existingItemIds = new Set(priceItems.map((p: any) => p.item_id || p.item_number)); - const SALES_CODES = ["CAT_ML8ZFVEL_1TOR"]; // 영업관리 카테고리 코드 - setItemSearchResults(allItems.filter((item: any) => { + const seenNumbers = new Set(); + const deduped = allItems.filter((item: any) => { if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false; - const divCodes = (item.division || "").split(",").map((c: string) => c.trim()); - return divCodes.some((code: string) => SALES_CODES.includes(code)); - })); + if (item.item_number && seenNumbers.has(item.item_number)) return false; + if (item.item_number) seenNumbers.add(item.item_number); + return true; + }); + setItemSearchResults(deduped); } catch { /* skip */ } finally { setItemSearchLoading(false); } }; @@ -1229,47 +1231,44 @@ export default function CustomerManagementPage() { } }; - // 컬럼 가시성 헬퍼 - const isColumnVisible = (key: string) => ts.isVisible(key); - - const customerColSpan = 1 + ["customer_code", "customer_name", "contact_person", "contact_phone", "division", "status"] - .filter((k) => isColumnVisible(k)).length; - - // EDataTable 컬럼 정의 (거래처 목록) - const customerColumns: EDataTableColumn[] = [ - ...(isColumnVisible("customer_code") ? [{ key: "customer_code", label: "거래처코드", width: "w-[120px]" }] : []), - ...(isColumnVisible("customer_name") ? [{ key: "customer_name", label: "거래처명", minWidth: "min-w-[140px]" }] : []), - ...(isColumnVisible("division") ? [{ - key: "division", - label: "거래유형", - width: "w-[80px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ...(isColumnVisible("contact_person") ? [{ key: "contact_person", label: "담당자", width: "w-[80px]" }] : []), - ...(isColumnVisible("contact_phone") ? [{ key: "contact_phone", label: "전화번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("email") ? [{ key: "email", label: "이메일", width: "w-[160px]" }] : []), - ...(isColumnVisible("business_number") ? [{ key: "business_number", label: "사업자번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("address") ? [{ key: "address", label: "주소", minWidth: "min-w-[150px]" }] : []), - ...(isColumnVisible("status") ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ]; + // EDataTable 컬럼 정의 (거래처 목록) — ts.visibleColumns 순서를 따름 + const customerColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + customer_code: { width: "w-[120px]" }, + customer_name: { minWidth: "min-w-[140px]" }, + division: { + width: "w-[80px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + contact_person: { width: "w-[80px]" }, + contact_phone: { width: "w-[120px]" }, + email: { width: "w-[160px]" }, + business_number: { width: "w-[120px]" }, + address: { minWidth: "min-w-[150px]" }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 엑셀 다운로드 const handleExcelDownload = async () => { diff --git a/frontend/app/(main)/COMPANY_8/sales/order/page.tsx b/frontend/app/(main)/COMPANY_8/sales/order/page.tsx index f896bbfd..f9a358d4 100644 --- a/frontend/app/(main)/COMPANY_8/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_8/sales/order/page.tsx @@ -13,7 +13,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, ClipboardList, Pencil, Search, X, Truck, Package, - ChevronLeft, ChevronRight, ChevronDown, ChevronsLeft, ChevronsRight, + ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, RotateCcw, Filter, Check, ArrowUp, ArrowDown, } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; @@ -42,41 +42,30 @@ const formatNumber = (val: string) => { }; const parseNumber = (val: string) => val.replace(/,/g, ""); -// 마스터 헤더 레이아웃 (수주번호 뒤, 디테일 11컬럼 위에 colSpan으로 맵핑) -// 순서: 거래처 | 단가방식 | 납품처 | 납품장소 | 수주일 | 담당자 → 합계 colSpan = 11 -const MASTER_BODY_LAYOUT = [ - { key: "partner_id", label: "거래처", colSpan: 2 }, - { key: "price_mode", label: "단가방식", colSpan: 1 }, - { key: "delivery_partner_id", label: "납품처", colSpan: 2 }, - { key: "delivery_address", label: "납품장소", colSpan: 2 }, - { key: "order_date", label: "수주일", colSpan: 2 }, - { key: "manager_id", label: "담당자", colSpan: 2 }, +// 플랫 테이블 컬럼 정의 (마스터+디테일 통합) +const FLAT_COLUMNS = [ + { key: "order_no", label: "수주번호", source: "master" }, + { key: "partner_id", label: "거래처", source: "master" }, + { key: "order_date", label: "수주일", source: "master" }, + { key: "part_code", label: "품번", source: "detail" }, + { key: "part_name", label: "품명", source: "detail" }, + { key: "spec", label: "규격", source: "detail" }, + { key: "unit", label: "단위", source: "detail" }, + { key: "qty", label: "수량", source: "detail" }, + { key: "ship_qty", label: "출하수량", source: "detail" }, + { key: "balance_qty", label: "잔량", source: "detail" }, + { key: "unit_price", label: "단가", source: "detail" }, + { key: "amount", label: "금액", source: "detail" }, + { key: "due_date", label: "납기일", source: "detail" }, + { key: "memo", label: "메모", source: "master" }, ]; -// 디테일 헤더 컬럼 -const DETAIL_HEADER_COLS = [ - { key: "part_code", label: "품번" }, - { key: "part_name", label: "품명" }, - { key: "spec", label: "규격" }, - { key: "unit", label: "단위" }, - { key: "qty", label: "수량" }, - { key: "ship_qty", label: "출하수량" }, - { key: "balance_qty", label: "잔량" }, - { key: "unit_price", label: "단가" }, - { key: "amount", label: "금액" }, - { key: "currency_code", label: "통화" }, - { key: "due_date", label: "납기일" }, -]; +const DETAIL_HEADER_COLS = FLAT_COLUMNS.filter((c) => c.source === "detail"); // 필터용 전체 키 -const GRID_COLUMNS_CONFIG = [ - { key: "order_no", label: "수주번호" }, - ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label })), - ...DETAIL_HEADER_COLS, - { key: "memo", label: "메모" }, -]; +const GRID_COLUMNS_CONFIG = FLAT_COLUMNS.map(({ key, label }) => ({ key, label })); -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 수주번호(1) + 디테일(11) + 메모(1) = 15 +// 총 컬럼 수: 체크박스(1) + 플랫 컬럼(14) = 15 const TOTAL_COLS = 15; // 헤더 필터 Popover @@ -180,8 +169,6 @@ export default function SalesOrderPage() { const [masterForm, setMasterForm] = useState>({}); const [detailRows, setDetailRows] = useState([]); const [allowPriceEdit, setAllowPriceEdit] = useState(true); - const [expandedOrders, setExpandedOrders] = useState>(new Set()); - const [closingOrders, setClosingOrders] = useState>(new Set()); // 품목 선택 모달 const [itemSelectOpen, setItemSelectOpen] = useState(false); @@ -376,25 +363,8 @@ export default function SalesOrderPage() { useEffect(() => { fetchOrders(); }, [fetchOrders]); - // 디테일 컬럼별 고유값 (디테일 서브헤더 필터용) - const columnUniqueValues = useMemo(() => { - const result: Record = {}; - for (const col of DETAIL_HEADER_COLS) { - const values = new Set(); - orders.forEach((row) => { - const val = row[col.key]; - if (val !== null && val !== undefined && val !== "") values.add(String(val)); - }); - result[col.key] = Array.from(values).sort(); - } - return result; - }, [orders]); - - // 마스터 필드 키 목록 (필터 분류용) - const MASTER_KEYS = new Set(["order_no", ...MASTER_BODY_LAYOUT.map((c) => c.key), "memo"]); - - // 카테고리 코드→라벨 변환 (마스터 필터용) - const resolveMasterLabel = useCallback((key: string, code: string) => { + // 카테고리 코드→라벨 변환 + const resolveLabel = useCallback((key: string, code: string) => { if (!code) return ""; if (key === "partner_id" || key === "manager_id" || key === "price_mode") { return categoryOptions[key]?.find((o) => o.code === code)?.label || code; @@ -402,106 +372,60 @@ export default function SalesOrderPage() { return code; }, [categoryOptions]); - // 필터 + 정렬 적용된 데이터 → 그룹핑 - const filteredOrderGroups = useMemo(() => { - // 1차: order_no 기준 그룹핑 (필터 전) - const allGroups: Record = {}; - for (const row of orders) { - const key = row.order_no || "_no_order"; - if (!allGroups[key]) { - allGroups[key] = { master: row._master || {}, details: [] }; - } - allGroups[key].details.push(row); - } - - // 마스터 필터 / 디테일 필터 분리 - const masterFilters: Record> = {}; - const detailFilters: Record> = {}; - for (const [colKey, values] of Object.entries(headerFilters)) { - if (values.size === 0) continue; - if (MASTER_KEYS.has(colKey)) masterFilters[colKey] = values; - else detailFilters[colKey] = values; - } - - // 2차: 마스터 필터 적용 (그룹 단위 필터링) - let entries = Object.entries(allGroups); - if (Object.keys(masterFilters).length > 0) { - entries = entries.filter(([, group]) => - Object.entries(masterFilters).every(([colKey, values]) => { - const raw = group.master?.[colKey] ?? ""; - const label = resolveMasterLabel(colKey, String(raw)); - return values.has(label) || values.has(String(raw)); - }) - ); - } - - // 3차: 디테일 필터 적용 (행 단위 필터링) - if (Object.keys(detailFilters).length > 0) { - entries = entries - .map(([orderNo, group]) => { - const filtered = group.details.filter((row) => - Object.entries(detailFilters).every(([colKey, values]) => { - const cellVal = row[colKey] != null ? String(row[colKey]) : ""; - return values.has(cellVal); - }) - ); - return [orderNo, { ...group, details: filtered }] as [string, typeof group]; - }) - .filter(([, group]) => group.details.length > 0); - } - - // 4차: 정렬 - if (sortState) { - const { key, direction } = sortState; - if (MASTER_KEYS.has(key)) { - // 마스터 필드 정렬 → 그룹 단위 - entries.sort(([, a], [, b]) => { - const av = a.master?.[key] ?? ""; - const bv = b.master?.[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - } else { - // 디테일 필드 정렬 → 각 그룹 내 디테일 정렬 - entries.forEach(([, group]) => { - group.details.sort((a, b) => { - const av = a[key] ?? ""; - const bv = b[key] ?? ""; - const na = Number(av); const nb = Number(bv); - if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; - return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); - }); - }); - } - } - - return Object.fromEntries(entries); - }, [orders, headerFilters, sortState, resolveMasterLabel]); - - // 마스터 컬럼별 고유값 (마스터 헤더 필터용) - const masterUniqueValues = useMemo(() => { - const result: Record = {}; - // 필터 전 전체 마스터에서 고유값 추출 - const seenMasters = new Map(); - orders.forEach((row) => { - if (row.order_no && row._master && !seenMasters.has(row.order_no)) { - seenMasters.set(row.order_no, row._master); - } + // 플랫 행 생성 (마스터 필드를 각 디테일 행에 병합) + const flatRows = useMemo(() => { + return orders.map((row) => { + const master = row._master || {}; + return { + ...row, + partner_id: resolveLabel("partner_id", master.partner_id || row.partner_id || ""), + order_date: master.order_date || row.order_date || "", + memo: row.memo || master.memo || "", + }; }); - const masters = Array.from(seenMasters.values()); - for (const col of [{ key: "order_no", label: "수주번호" }, ...MASTER_BODY_LAYOUT.map(({ key, label }) => ({ key, label })), { key: "memo", label: "메모" }]) { + }, [orders, resolveLabel]); + + // 컬럼별 고유값 (헤더 필터용) + const columnUniqueValues = useMemo(() => { + const result: Record = {}; + for (const col of FLAT_COLUMNS) { const values = new Set(); - masters.forEach((m) => { - const val = m?.[col.key]; - if (val !== null && val !== undefined && val !== "") { - values.add(resolveMasterLabel(col.key, String(val))); - } + flatRows.forEach((row) => { + const val = row[col.key]; + if (val !== null && val !== undefined && val !== "") values.add(String(val)); }); result[col.key] = Array.from(values).sort(); } return result; - }, [orders, resolveMasterLabel]); + }, [flatRows]); + + // 필터 + 정렬 적용된 플랫 데이터 + const filteredFlatRows = useMemo(() => { + let rows = [...flatRows]; + + // 1차: 헤더 필터 적용 + for (const [colKey, values] of Object.entries(headerFilters)) { + if (values.size === 0) continue; + rows = rows.filter((row) => { + const cellVal = row[colKey] != null ? String(row[colKey]) : ""; + return values.has(cellVal); + }); + } + + // 2차: 정렬 + if (sortState) { + const { key, direction } = sortState; + rows.sort((a, b) => { + const av = a[key] ?? ""; + const bv = b[key] ?? ""; + const na = Number(av); const nb = Number(bv); + if (!isNaN(na) && !isNaN(nb)) return direction === "asc" ? na - nb : nb - na; + return direction === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); + }); + } + + return rows; + }, [flatRows, headerFilters, sortState]); // 헤더 필터 토글/초기화 const toggleHeaderFilter = (colKey: string, value: string) => { @@ -965,111 +889,70 @@ export default function SalesOrderPage() {
- {/* 데이터 테이블 (트리 구조) */} + {/* 데이터 테이블 (플랫 리스트) */}
- {/* 체크박스 */} - {/* 펼침 화살표 */} - {/* 수주번호 */} - {/* 품번 / 거래처 */} - {/* 품명 / 거래처(cont) */} - {/* 규격 / 단가방식 */} - {/* 단위 / 납품처 */} - {/* 수량 / 납품처(cont) */} - {/* 출하수량 / 납품장소 */} - {/* 잔량 / 납품장소(cont) */} - {/* 단가 / 수주일 */} - {/* 금액 / 수주일(cont) */} - {/* 통화 / 담당자 */} - {/* 납기일 / 담당자(cont) */} - {/* 메모 */} + + + + + + + + + + + + + + + { - const allFilteredIds = Object.values(filteredOrderGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredFlatRows.map((r) => r.id); const allChecked = allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); setCheckedIds(allChecked ? [] : allFilteredIds); }} > { - const allFilteredIds = Object.values(filteredOrderGroups).flatMap((g) => g.details.map((d) => d.id)); + const allFilteredIds = filteredFlatRows.map((r) => r.id); return allFilteredIds.length > 0 && allFilteredIds.every((id) => checkedIds.includes(id)); })()} onCheckedChange={() => {}} /> - - {/* 수주번호 (별도 컬럼) */} - -
-
handleSort("order_no")}> - 수주번호 - {sortState?.key === "order_no" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["order_no"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
- {/* 마스터 필드 헤더 (colSpan으로 디테일 컬럼 위에 맵핑) */} - {MASTER_BODY_LAYOUT.map((col) => ( - -
-
handleSort(col.key)}> - {col.label} - {sortState?.key === col.key && ( - sortState.direction === "asc" - ? - : + {FLAT_COLUMNS.map((col) => { + const isRight = ["qty", "ship_qty", "balance_qty", "unit_price", "amount"].includes(col.key); + return ( + +
+
handleSort(col.key)}> + {col.label} + {sortState?.key === col.key && ( + sortState.direction === "asc" + ? + : + )} +
+ {(columnUniqueValues[col.key] || []).length > 0 && ( + ()} + onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} + /> )}
- {(masterUniqueValues[col.key] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
- - ))} - {/* 메모 (마스터) */} - -
-
handleSort("memo")}> - 메모 - {sortState?.key === "memo" && ( - sortState.direction === "asc" - ? - : - )} -
- {(masterUniqueValues["memo"] || []).length > 0 && ( - ()} - onToggle={toggleHeaderFilter} onClear={clearHeaderFilter} - /> - )} -
-
+ + ); + })} @@ -1079,7 +962,7 @@ export default function SalesOrderPage() { - ) : Object.keys(filteredOrderGroups).length === 0 ? ( + ) : filteredFlatRows.length === 0 ? (
@@ -1089,200 +972,48 @@ export default function SalesOrderPage() { ) : ( - Object.entries(filteredOrderGroups).map(([orderNo, group]) => { - const isExpanded = expandedOrders.has(orderNo); - const detailIds = group.details.map((d) => d.id); - const allDetailChecked = detailIds.length > 0 && detailIds.every((id) => checkedIds.includes(id)); - const someDetailChecked = detailIds.some((id) => checkedIds.includes(id)); - const master = group.master; + filteredFlatRows.map((row) => { + const isChecked = checkedIds.includes(row.id); return ( - - {/* 마스터 행 — 마스터 테이블 필드만 표시 */} - { - if (expandedOrders.has(orderNo)) { - setClosingOrders((prev) => new Set(prev).add(orderNo)); - setTimeout(() => { - setExpandedOrders((prev) => { const next = new Set(prev); next.delete(orderNo); return next; }); - setClosingOrders((prev) => { const next = new Set(prev); next.delete(orderNo); return next; }); - }, 200); - } else { - setExpandedOrders((prev) => new Set(prev).add(orderNo)); - } - }} - onDoubleClick={() => openEditModal(orderNo)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => { - if (allDetailChecked) return prev.filter((id) => !detailIds.includes(id)); - return [...new Set([...prev, ...detailIds])]; - }); - }} - > - {}} - /> - - - {isExpanded - ? - : - } - - {/* 수주번호 */} - - {orderNo} - ({group.details.length}) - - {/* 거래처 (colSpan=2) */} - - - {master.partner_id ? (categoryOptions["partner_id"]?.find((o) => o.code === master.partner_id)?.label || master.partner_id) : ""} - - - {/* 단가방식 (colSpan=1) */} - - - {master.price_mode ? (categoryOptions["price_mode"]?.find((o) => o.code === master.price_mode)?.label || master.price_mode) : ""} - - - {/* 납품처 (colSpan=2) */} - - {master.delivery_partner_id || ""} - - {/* 납품장소 (colSpan=2) */} - - {master.delivery_address || ""} - - {/* 수주일 (colSpan=2) */} - - {master.order_date || ""} - - {/* 담당자 (colSpan=2) */} - - - {master.manager_id ? (categoryOptions["manager_id"]?.find((o) => o.code === master.manager_id)?.label || master.manager_id) : ""} - - - {/* 메모 */} - - {master.memo || ""} - - - - {/* 디테일 서브 헤더 (펼쳤을 때만) */} - {isExpanded && ( - - - - {/* 수주번호 컬럼 빈 셀 */} - {DETAIL_HEADER_COLS.map((col) => { - const isRight = ["qty", "ship_qty", "balance_qty", "unit_price", "amount"].includes(col.key); - const isSorted = sortState?.key === col.key; - const uniqueVals = Array.from(new Set( - group.details.map((d) => d[col.key]).filter((v: any) => v != null && v !== "").map(String) - )).sort(); - const filterVals = headerFilters[col.key] || new Set(); - return ( - -
-
handleSort(col.key)} - > - {col.label} - {isSorted && ( - sortState!.direction === "asc" - ? - : - )} -
- {uniqueVals.length > 0 && ( - - )} -
-
- ); - })} - -
+ { - const isClosing = closingOrders.has(orderNo); - const isChecked = checkedIds.includes(row.id); - return ( - { - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - onDoubleClick={() => openEditModal(row.order_no)} - > - { - e.stopPropagation(); - setCheckedIds((prev) => - prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] - ); - }} - > - {}} /> - - -
- - {/* 수주번호 컬럼 빈 셀 */} - {row.part_code} - {row.part_name} - {row.spec} - {row.unit} - {row.qty ? Number(row.qty).toLocaleString() : ""} - {row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""} - {row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {row.amount ? Number(row.amount).toLocaleString() : ""} - {row.currency_code || ""} - {row.due_date || ""} - - + onClick={() => { + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] ); - })} - + }} + onDoubleClick={() => openEditModal(row.order_no)} + > + { + e.stopPropagation(); + setCheckedIds((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id] + ); + }} + > + {}} /> + + {row.order_no} + {row.partner_id || ""} + {row.order_date || ""} + {row.part_code} + {row.part_name} + {row.spec} + {row.unit} + {row.qty ? Number(row.qty).toLocaleString() : ""} + {row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""} + {row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""} + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + {row.amount ? Number(row.amount).toLocaleString() : ""} + {row.due_date || ""} + {row.memo || ""} + ); }) )} diff --git a/frontend/app/(main)/COMPANY_8/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_8/sales/shipping-order/page.tsx index 4ab5a9ad..2ed29b40 100644 --- a/frontend/app/(main)/COMPANY_8/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_8/sales/shipping-order/page.tsx @@ -363,7 +363,7 @@ export default function ShippingOrderPage() { spec: item.spec, material: item.material, orderQty: item.orderQty, - planQty: item.planQty, + planQty: item.orderQty, shipQty: 0, sourceType: item.sourceType, shipmentPlanId: item.shipmentPlanId, diff --git a/frontend/app/(main)/COMPANY_9/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_9/equipment/info/page.tsx index eeb63844..cd53e9b1 100644 --- a/frontend/app/(main)/COMPANY_9/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_9/equipment/info/page.tsx @@ -142,15 +142,20 @@ export default function EquipmentInfoPage() { }; const mainTableColumns = useMemo(() => { - const cols: EDataTableColumn[] = []; - if (ts.isVisible("equipment_code")) cols.push({ key: "equipment_code", label: "설비코드", width: "w-[110px]" }); - if (ts.isVisible("equipment_name")) cols.push({ key: "equipment_name", label: "설비명", minWidth: "min-w-[130px]", truncate: true, render: (v) => v || "-" }); - if (ts.isVisible("equipment_type")) cols.push({ key: "equipment_type", label: "설비유형", width: "w-[90px]", render: (v) => v || "-" }); - if (ts.isVisible("manufacturer")) cols.push({ key: "manufacturer", label: "제조사", width: "w-[100px]", render: (v) => v || "-" }); - if (ts.isVisible("installation_location")) cols.push({ key: "installation_location", label: "설치장소", width: "w-[100px]", render: (v) => v || "-" }); - if (ts.isVisible("operation_status")) cols.push({ key: "operation_status", label: "가동상태", width: "w-[80px]", render: (v) => v || "-" }); - return cols; - }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + 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 || "-" }, + manufacturer: { width: "w-[100px]", render: (v) => v || "-" }, + installation_location: { width: "w-[100px]", render: (v) => v || "-" }, + operation_status: { width: "w-[80px]", render: (v) => v || "-" }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 설비 조회 const fetchEquipments = useCallback(async () => { @@ -272,8 +277,8 @@ export default function EquipmentInfoPage() { if (!inspectionForm.inspection_cycle) { toast.error("점검주기는 필수입니다."); return; } if (!inspectionForm.inspection_method) { toast.error("점검방법은 필수입니다."); return; } const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; - if (isNumeric && !inspectionForm.unit) { toast.error("숫자 점검방법은 측정단위가 필수입니다."); return; } + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); + if (isNumeric && !inspectionForm.unit) { toast.error("측정단위가 필수입니다."); return; } // 기준값/오차범위 → 하한치/상한치 자동 계산 const saveData = { ...inspectionForm }; if (isNumeric && saveData.standard_value) { @@ -739,7 +744,7 @@ export default function EquipmentInfoPage() {
{catSelect("inspection_method", inspectionForm.inspection_method, (v) => { const label = resolve("inspection_method", v); - const isNum = label === "숫자" || v === "숫자"; + const isNum = ["숫자", "치수검사"].includes(label) || ["숫자", "치수검사"].includes(v); if (!isNum) { setInspectionForm((p) => ({ ...p, inspection_method: v, unit: "", standard_value: "", tolerance: "", lower_limit: "", upper_limit: "" })); } else { @@ -748,7 +753,7 @@ export default function EquipmentInfoPage() { }, "점검방법")}
{(() => { const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); if (!isNumeric) return null; return (
@@ -758,7 +763,7 @@ export default function EquipmentInfoPage() {
{(() => { const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); - const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + const isNumeric = ["숫자", "치수검사"].includes(methodLabel) || ["숫자", "치수검사"].includes(inspectionForm.inspection_method); if (!isNumeric) return null; return (
diff --git a/frontend/app/(main)/COMPANY_9/logistics/material-status/page.tsx b/frontend/app/(main)/COMPANY_9/logistics/material-status/page.tsx index 46de306c..eb87ba92 100644 --- a/frontend/app/(main)/COMPANY_9/logistics/material-status/page.tsx +++ b/frontend/app/(main)/COMPANY_9/logistics/material-status/page.tsx @@ -333,69 +333,90 @@ export default function MaterialStatusPage() {

) : ( - workOrders.map((wo) => ( -
handleSelectWo(wo.id)} - > + ts.groupData(workOrders).map((wo) => { + if ((wo as any)._isGroupSummary || (wo as any)._isGroupHeader) return null; + return (
e.stopPropagation()} + key={wo.id} + className={cn( + "flex gap-3 rounded-lg border p-3 transition-all cursor-pointer", + "hover:border-primary/50 hover:shadow-sm", + selectedWoId === wo.id + ? "border-primary bg-primary/5 shadow-sm" + : "border-border" + )} + onClick={() => handleSelectWo(wo.id)} > - - handleCheckWo(wo.id, c as boolean) - } - /> -
-
-
- - {wo.plan_no || wo.work_order_no || `WO-${wo.id}`} - - e.stopPropagation()} + > + + handleCheckWo(wo.id, c as boolean) + } + /> +
+
+
+ {ts.isVisible("plan_no") && ( + + {wo.plan_no || wo.work_order_no || `WO-${wo.id}`} + )} - > - {getStatusLabel(wo.status)} - -
-
- - {wo.item_name} - - - ({wo.item_code}) - -
-
- 수량: - - {Number(wo.plan_qty).toLocaleString()}개 - - | - 일자: - - {wo.plan_date - ? new Date(wo.plan_date) - .toISOString() - .slice(0, 10) - : "-"} - + {ts.isVisible("status") && ( + + {getStatusLabel(wo.status)} + + )} +
+
+ {ts.isVisible("item_name") && ( + + {wo.item_name} + + )} + {ts.isVisible("item_code") && ( + + ({wo.item_code}) + + )} +
+
+ {ts.isVisible("plan_qty") && ( + <> + 수량: + + {Number(wo.plan_qty).toLocaleString()}개 + + + )} + {ts.isVisible("plan_qty") && ts.isVisible("plan_date") && ( + | + )} + {ts.isVisible("plan_date") && ( + <> + 일자: + + {wo.plan_date + ? new Date(wo.plan_date) + .toISOString() + .slice(0, 10) + : "-"} + + + )} +
-
- )) + ); + }) )}
diff --git a/frontend/app/(main)/COMPANY_9/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_9/logistics/outbound/page.tsx index 5ab46a8a..c1ffbd40 100644 --- a/frontend/app/(main)/COMPANY_9/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_9/logistics/outbound/page.tsx @@ -140,8 +140,16 @@ const DETAIL_HEADER_COLS = [ // 마스터 필드 키 목록 (필터 분류용) const MASTER_KEYS = new Set(["outbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 출고번호(1) + 마스터필드(7) = 10 -const TOTAL_COLS = 10; +// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) +const DETAIL_KEY_MAP: Record = { + source_type: "source_type", + item_number: "item_code", + item_name: "item_name", + spec: "specification", + outbound_qty: "outbound_qty", + unit_price: "unit_price", + total_amount: "total_amount", +}; // 헤더 필터 Popover function HeaderFilterPopover({ @@ -248,6 +256,31 @@ interface SelectedSourceItem { export default function OutboundPage() { const ts = useTableSettings("c16-outbound", "outbound_mng", GRID_COLUMNS); + + // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 + const visibleMasterLayout = useMemo(() => { + const ordered: typeof MASTER_BODY_LAYOUT = []; + for (const vc of ts.visibleColumns) { + const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); + if (m) ordered.push(m); + } + return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; + }, [ts.visibleColumns]); + + const visibleDetailCols = useMemo(() => { + const ordered: typeof DETAIL_HEADER_COLS = []; + for (const vc of ts.visibleColumns) { + const detailKey = DETAIL_KEY_MAP[vc.key]; + if (detailKey) { + const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); + if (d) ordered.push(d); + } + } + return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; + }, [ts.visibleColumns]); + + const TOTAL_COLS = 3 + visibleMasterLayout.length; + // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -900,8 +933,15 @@ export default function OutboundPage() {
-
- +
+ + + + + {visibleMasterLayout.map((col) => ( + + ))} + - {/* 마스터 필드 헤더 */} - {MASTER_BODY_LAYOUT.map((col) => ( + {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => (
handleSort(col.key)}> @@ -1039,38 +1079,51 @@ export default function OutboundPage() { {outboundNo} ({group.details.length}) - {/* 출고유형 */} - - - {master.outbound_type || "-"} - - - {/* 출고일 */} - - {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} - - {/* 참조번호 */} - - {master.reference_number || ""} - - {/* 거래처 */} - - {master.customer_name || ""} - - {/* 창고 */} - - {master.warehouse_name || master.warehouse_code || ""} - - {/* 출고상태 */} - - - {master.outbound_status || "-"} - - - {/* 비고 */} - - {master.memo || ""} - + {/* 마스터 필드 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => { + switch (col.key) { + case "outbound_type": return ( + + + {master.outbound_type || "-"} + + + ); + case "outbound_date": return ( + + {master.outbound_date ? new Date(master.outbound_date).toLocaleDateString("ko-KR") : "-"} + + ); + case "reference_number": return ( + + {master.reference_number || ""} + + ); + case "customer_name": return ( + + {master.customer_name || ""} + + ); + case "warehouse_name": return ( + + {master.warehouse_name || master.warehouse_code || ""} + + ); + case "outbound_status": return ( + + + {master.outbound_status || "-"} + + + ); + case "memo": return ( + + {master.memo || ""} + + ); + default: return {(master as any)[col.key] ?? ""}; + } + })} {/* 디테일 서브 헤더 (펼쳤을 때만) */} @@ -1084,7 +1137,7 @@ export default function OutboundPage() { - {DETAIL_HEADER_COLS.map((col) => { + {visibleDetailCols.map((col) => { const isRight = ["outbound_qty", "unit_price", "total_amount"].includes(col.key); const isSorted = sortState?.key === col.key; const uniqueVals = Array.from(new Set( @@ -1163,20 +1216,18 @@ export default function OutboundPage() {
- {/* 출처 */} - {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"} - {/* 품목코드 */} - {row.item_code || ""} - {/* 품목명 */} - {row.item_name || ""} - {/* 규격 */} - {row.specification || ""} - {/* 출고수량 */} - {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""} - {/* 단가 */} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {/* 금액 */} - {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {visibleDetailCols.map((col) => { + switch (col.key) { + case "source_type": return {row.source_type ? (SOURCE_TYPE_LABEL[row.source_type] || row.source_type) : "-"}; + case "item_code": return {row.item_code || ""}; + case "item_name": return {row.item_name || ""}; + case "specification": return {row.specification || ""}; + case "outbound_qty": return {row.outbound_qty ? Number(row.outbound_qty).toLocaleString() : ""}; + case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; + case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; + default: return {(row as any)[col.key] ?? ""}; + } + })} ); })} diff --git a/frontend/app/(main)/COMPANY_9/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_9/logistics/packaging/page.tsx index 5d4d5787..6ae340aa 100644 --- a/frontend/app/(main)/COMPANY_9/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_9/logistics/packaging/page.tsx @@ -460,18 +460,20 @@ export default function PackagingPage() { {/* 포장재 목록 테이블 */}
PKG_TYPE_LABEL[v] || v || "-" }, - { key: "size", label: "크기(mm)", width: "w-[100px]", render: (_v, row) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, - { key: "max_load_kg", label: "최대중량", width: "w-[80px]", align: "right", render: (v) => Number(v || 0) > 0 ? `${v}kg` : "-" }, - { key: "status", label: "상태", width: "w-[60px]", align: "center", render: (v) => ( - - {STATUS_LABEL[v] || v} - - )}, - ] as EDataTableColumn[]} + columns={ts.visibleColumns.map((col): EDataTableColumn => { + const renderMap: Record>> = { + pkg_type: { width: "w-[80px]", render: (v: any) => PKG_TYPE_LABEL[v] || v || "-" }, + size: { width: "w-[100px]", render: (_v: any, row: any) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, + max_weight: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, + max_load_kg: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, + status: { width: "w-[60px]", align: "center", render: (v: any) => ( + + {STATUS_LABEL[v] || v} + + )}, + }; + return { key: col.key, label: col.label, ...renderMap[col.key] }; + })} data={ts.groupData(filteredPkgUnits)} rowKey={(row) => String(row.id)} loading={pkgLoading} diff --git a/frontend/app/(main)/COMPANY_9/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_9/logistics/receiving/page.tsx index a8d5fc2c..85cdc23c 100644 --- a/frontend/app/(main)/COMPANY_9/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_9/logistics/receiving/page.tsx @@ -117,12 +117,20 @@ const DETAIL_HEADER_COLS = [ { key: "total_amount", label: "금액" }, ]; -// 총 컬럼 수: 체크박스(1) + 화살표(1) + 입고번호(1) + 디테일(7) = 10 -const TOTAL_COLS = 10; - // 마스터 필드 키 목록 (필터 분류용) const MASTER_KEYS = new Set(["inbound_number", ...MASTER_BODY_LAYOUT.map((c) => c.key)]); +// 디테일 필드 키 매핑 (GRID_COLUMNS key → DETAIL_HEADER_COLS key) +const DETAIL_KEY_MAP: Record = { + source_type: "source_table", + item_number: "item_number", + item_name: "item_name", + spec: "spec", + inbound_qty: "inbound_qty", + unit_price: "unit_price", + total_amount: "total_amount", +}; + // 헤더 필터 Popover function HeaderFilterPopover({ colKey, colLabel, uniqueValues, filterValues, onToggle, onClear, @@ -278,6 +286,31 @@ interface SelectedSourceItem { export default function ReceivingPage() { const ts = useTableSettings("c16-receiving", "inbound_mng", GRID_COLUMNS); + + // ts.visibleColumns 기반 마스터/디테일 컬럼 계산 + const visibleMasterLayout = useMemo(() => { + const ordered: typeof MASTER_BODY_LAYOUT = []; + for (const vc of ts.visibleColumns) { + const m = MASTER_BODY_LAYOUT.find((ml) => ml.key === vc.key); + if (m) ordered.push(m); + } + return ordered.length > 0 ? ordered : MASTER_BODY_LAYOUT; + }, [ts.visibleColumns]); + + const visibleDetailCols = useMemo(() => { + const ordered: typeof DETAIL_HEADER_COLS = []; + for (const vc of ts.visibleColumns) { + const detailKey = DETAIL_KEY_MAP[vc.key]; + if (detailKey) { + const d = DETAIL_HEADER_COLS.find((dc) => dc.key === detailKey); + if (d) ordered.push(d); + } + } + return ordered.length > 0 ? ordered : DETAIL_HEADER_COLS; + }, [ts.visibleColumns]); + + const TOTAL_COLS = 3 + visibleMasterLayout.length; + // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -847,8 +880,15 @@ export default function ReceivingPage() {
-
- +
+ + + + + {visibleMasterLayout.map((col) => ( + + ))} + - {/* 마스터 필드 헤더 (colSpan으로 디테일 컬럼 위에 맵핑) */} - {MASTER_BODY_LAYOUT.map((col) => ( + {/* 마스터 필드 헤더 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => (
handleSort(col.key)}> @@ -985,38 +1025,51 @@ export default function ReceivingPage() { {inboundNo} ({group.details.length}) - {/* 입고유형 */} - - - {resolveInboundType(master.inbound_type)} - - - {/* 입고일 */} - - {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} - - {/* 참조번호 */} - - {master.reference_number || ""} - - {/* 공급처 */} - - {master.supplier_name || ""} - - {/* 창고 */} - - {master.warehouse_name || master.warehouse_code || ""} - - {/* 입고상태 */} - - - {master.inbound_status || "-"} - - - {/* 비고 */} - - {master.memo || ""} - + {/* 마스터 필드 (ts.visibleColumns 순서) */} + {visibleMasterLayout.map((col) => { + switch (col.key) { + case "inbound_type": return ( + + + {resolveInboundType(master.inbound_type)} + + + ); + case "inbound_date": return ( + + {master.inbound_date ? new Date(master.inbound_date).toLocaleDateString("ko-KR") : "-"} + + ); + case "reference_number": return ( + + {master.reference_number || ""} + + ); + case "supplier_name": return ( + + {master.supplier_name || ""} + + ); + case "warehouse_name": return ( + + {master.warehouse_name || master.warehouse_code || ""} + + ); + case "inbound_status": return ( + + + {master.inbound_status || "-"} + + + ); + case "memo": return ( + + {master.memo || ""} + + ); + default: return {(master as any)[col.key] ?? ""}; + } + })} {/* 디테일 서브 헤더 (펼쳤을 때만) */} @@ -1030,7 +1083,7 @@ export default function ReceivingPage() { - {DETAIL_HEADER_COLS.map((col) => { + {visibleDetailCols.map((col) => { const isRight = ["inbound_qty", "unit_price", "total_amount"].includes(col.key); const isSorted = sortState?.key === col.key; const uniqueVals = Array.from(new Set( @@ -1108,20 +1161,18 @@ export default function ReceivingPage() {
- {/* 출처 */} - {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"} - {/* 품목코드 */} - {row.item_number || ""} - {/* 품목명 */} - {row.item_name || ""} - {/* 규격 */} - {row.spec || ""} - {/* 입고수량 */} - {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""} - {/* 단가 */} - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - {/* 금액 */} - {row.total_amount ? Number(row.total_amount).toLocaleString() : ""} + {visibleDetailCols.map((col) => { + switch (col.key) { + case "source_table": return {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"}; + case "item_number": return {row.item_number || ""}; + case "item_name": return {row.item_name || ""}; + case "spec": return {row.spec || ""}; + case "inbound_qty": return {row.inbound_qty ? Number(row.inbound_qty).toLocaleString() : ""}; + case "unit_price": return {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}; + case "total_amount": return {row.total_amount ? Number(row.total_amount).toLocaleString() : ""}; + default: return {(row as any)[col.key] ?? ""}; + } + })} ); })} 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 dfd1b666..9d7f2dea 100644 --- a/frontend/app/(main)/COMPANY_9/master-data/company/page.tsx +++ b/frontend/app/(main)/COMPANY_9/master-data/company/page.tsx @@ -491,12 +491,6 @@ export default function CompanyPage() { > 회사정보 - - 부서관리 -
@@ -635,89 +629,6 @@ export default function CompanyPage() {
- {/* ===================== Tab 2: 부서관리 ===================== */} - -
- - {/* 좌측: 부서 트리 */} - -
-
-
- - 부서 - {depts.length}건 -
-
- - - -
-
-
- {deptLoading ? ( -
- -
- ) : deptTree.length === 0 ? ( -
- - 등록된 부서가 없어요 -
- ) : ( - renderTree(deptTree) - )} -
-
-
- - - - {/* 우측: 사원 목록 */} - -
-
-
- - {selectedDept ? "부서 인원" : "부서를 선택해주세요"} - {selectedDept && {selectedDept.dept_name}} - {members.length > 0 && {members.length}명} -
- {selectedDeptCode && ( - - )} -
- {selectedDeptCode ? ( - row.user_id || row.id} - loading={memberLoading} - emptyMessage="소속 사원이 없어요" - emptyIcon={} - onRowDoubleClick={(row) => openUserModal(row)} - showPagination={false} - draggableColumns={false} - /> - ) : ( -
- - 좌측에서 부서를 선택해주세요 -
- )} -
-
-
-
-
{/* ── 부서 등록/수정 모달 ── */} diff --git a/frontend/app/(main)/COMPANY_9/master-data/department/page.tsx b/frontend/app/(main)/COMPANY_9/master-data/department/page.tsx index a2bbcba5..3245571e 100644 --- a/frontend/app/(main)/COMPANY_9/master-data/department/page.tsx +++ b/frontend/app/(main)/COMPANY_9/master-data/department/page.tsx @@ -9,7 +9,7 @@ * 모달: 부서 등록(dept_info), 사원 추가(user_info) */ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -279,6 +279,7 @@ export default function DepartmentPage() { dept_code: userForm.dept_code || undefined, dept_name: depts.find((d) => d.dept_code === userForm.dept_code)?.dept_name || undefined, status: userForm.status || "active", + end_date: userForm.end_date || null, }, mainDept: userForm.dept_code ? { dept_code: userForm.dept_code, @@ -312,37 +313,40 @@ export default function DepartmentPage() { const activeMembers = members.filter((m) => !m.end_date || m.end_date.substring(0, 10) >= today); const resignedMembers = members.filter((m) => m.end_date && m.end_date.substring(0, 10) < today); - const isColVisible = (key: string) => ts.isVisible(key); - - // EDataTable 컬럼 정의 (부서 목록) - const deptColumns: EDataTableColumn[] = [ - { key: "dept_code", label: "부서코드", width: "w-[120px]" }, - { key: "dept_name", label: "부서명", minWidth: "min-w-[140px]" }, - ...(isColVisible("parent_dept_code") - ? [{ - key: "parent_dept_code", - label: "상위부서", - width: "w-[110px]", - render: (val: any) => {val || "\u2014"}, - }] - : []), - ...(isColVisible("status") - ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val === "active" ? "활성" : (val || "\u2014")} - - ) : null, - }] - : []), - ]; + // EDataTable 컬럼 정의 (부서 목록) — ts.visibleColumns 순서를 따름 + const deptColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + dept_code: { width: "w-[120px]" }, + dept_name: { minWidth: "min-w-[140px]" }, + parent_dept_code: { + width: "w-[110px]", + render: (val: any) => {val || "\u2014"}, + }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val === "active" ? "활성" : (val || "\u2014")} + + ) : null, + }, + }; + // dept_code, dept_name은 항상 표시 (DEPT_COLUMNS에 포함되지 않으므로 visibleColumns에 없음) + const fixedCols: EDataTableColumn[] = [ + { key: "dept_code", label: "부서코드", ...colProps["dept_code"] }, + { key: "dept_name", label: "부서명", ...colProps["dept_name"] }, + ]; + const dynamicCols = ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + return [...fixedCols, ...dynamicCols]; + }, [ts.visibleColumns]); return (
diff --git a/frontend/app/(main)/COMPANY_9/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_9/master-data/item-info/page.tsx index 3f037275..375fd900 100644 --- a/frontend/app/(main)/COMPANY_9/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_9/master-data/item-info/page.tsx @@ -84,6 +84,56 @@ function CategoryCombobox({ options, value, onChange, placeholder }: { ); } +// 다중 선택 카테고리 콤보박스 +function MultiCategoryCombobox({ options, value, onChange, placeholder }: { + options: { code: string; label: string }[]; + value: string; + onChange: (v: string) => void; + placeholder: string; +}) { + const [open, setOpen] = useState(false); + const selectedCodes = value ? value.split(",").map((c) => c.trim()).filter(Boolean) : []; + const selectedLabels = selectedCodes.map((code) => options.find((o) => o.code === code)?.label || code).filter(Boolean); + + const toggle = (code: string) => { + const next = selectedCodes.includes(code) + ? selectedCodes.filter((c) => c !== code) + : [...selectedCodes, code]; + onChange(next.join(",")); + }; + + return ( + + + + + + + + + 검색 결과가 없어요 + + {options.map((opt) => ( + toggle(opt.code)}> + + {opt.label} + + ))} + + + + + + ); +} + const TABLE_NAME = "item_info"; const GRID_COLUMNS = [ @@ -108,7 +158,7 @@ const GRID_COLUMNS = [ const FORM_FIELDS = [ { key: "item_number", label: "품목코드", type: "text", required: true, disabled: true, placeholder: "자동 채번" }, { key: "item_name", label: "품명", type: "text", required: true }, - { key: "division", label: "관리품목", type: "category" }, + { key: "division", label: "관리품목", type: "multi-category" }, { key: "type", label: "품목구분", type: "category" }, { key: "size", label: "규격", type: "text" }, { key: "unit", label: "단위", type: "category" }, @@ -137,6 +187,7 @@ export default function ItemInfoPage() { const { user } = useAuth(); const ts = useTableSettings("c16-item-info", TABLE_NAME, GRID_COLUMNS); const [items, setItems] = useState([]); + const [rawItems, setRawItems] = useState([]); const [loading, setLoading] = useState(false); // 검색 필터 (DynamicSearchFilter) @@ -215,6 +266,7 @@ export default function ItemInfoPage() { } return categoryOptions[col]?.find((o) => o.code === code)?.label || code; }; + setRawItems(raw); const data = raw.map((r: any) => { const converted = { ...r }; for (const col of CATEGORY_COLUMNS) { @@ -261,7 +313,8 @@ export default function ItemInfoPage() { // 수정 모달 열기 const openEditModal = (item: any) => { - setFormData({ ...item }); + const raw = rawItems.find((r) => r.id === item.id) || item; + setFormData({ ...raw }); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); @@ -269,7 +322,8 @@ export default function ItemInfoPage() { // 복사 모달 열기 const openCopyModal = async (item: any) => { - const { id, item_number, created_date, updated_date, writer, ...rest } = item; + const raw = rawItems.find((r) => r.id === item.id) || item; + const { id, item_number, created_date, updated_date, writer, ...rest } = raw; setFormData(rest); setIsEditMode(false); setEditId(null); @@ -459,6 +513,13 @@ export default function ItemInfoPage() { columnName={field.key} height="h-32" /> + ) : field.type === "multi-category" ? ( + setFormData((prev) => ({ ...prev, [field.key]: v }))} + placeholder={`${field.label} 선택`} + /> ) : field.type === "category" ? ( (() => { - const cols: EDataTableColumn[] = []; - if (ts.isVisible("item_number")) cols.push({ key: "item_number", label: "품번", width: "w-[110px]" }); - if (ts.isVisible("item_name")) cols.push({ key: "item_name", label: "품명", minWidth: "min-w-[130px]", render: (v) => v || "-" }); - if (ts.isVisible("size")) cols.push({ key: "size", label: "규격", width: "w-[90px]", render: (v) => v || "-" }); - if (ts.isVisible("unit")) cols.push({ key: "unit", label: "단위", width: "w-[60px]", render: (v) => v || "-" }); - if (ts.isVisible("standard_price")) cols.push({ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }); - if (ts.isVisible("selling_price")) cols.push({ key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true }); - if (ts.isVisible("currency_code")) cols.push({ key: "currency_code", label: "통화", width: "w-[50px]", render: (v) => v || "-" }); - if (ts.isVisible("status")) cols.push({ key: "status", label: "상태", width: "w-[60px]", render: (v) => v || "-" }); - return cols; - }, [ts.visibleColumns]); // eslint-disable-line react-hooks/exhaustive-deps + const colProps: Record> = { + item_number: { width: "w-[110px]" }, + item_name: { minWidth: "min-w-[130px]", render: (v) => v || "-" }, + size: { width: "w-[90px]", render: (v) => v || "-" }, + unit: { width: "w-[60px]", render: (v) => v || "-" }, + standard_price: { width: "w-[90px]", align: "right", formatNumber: true }, + selling_price: { width: "w-[90px]", align: "right", formatNumber: true }, + currency_code: { width: "w-[50px]", render: (v) => v || "-" }, + status: { width: "w-[60px]", render: (v) => v || "-" }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 좌측: 품목 조회 (division이 "외주관리"인 품목만 필터링) const outsourcingDivisionCode = categoryOptions["division"]?.find( diff --git a/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx index 4a0d341a..6bd63176 100644 --- a/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx @@ -919,9 +919,7 @@ export default function ProductionPlanManagementPage() { // 숫자 포맷 const formatNumber = (num: number | string) => Number(num).toLocaleString(); - // 컬럼 표시 여부 - const isColVisible = (key: string) => ts.isVisible(key); - const orderColSpan = 4 + ORDER_COLUMNS.filter((c) => isColVisible(c.key)).length; + // (컬럼 표시는 ts.visibleColumns 순서를 따름) return (
@@ -1019,6 +1017,38 @@ export default function ProductionPlanManagementPage() {
) : (
+ {(() => { + // 디테일 행에서 개별 값을 표시하는 컬럼 매핑 + const DETAIL_VALUE_MAP: Record = { + total_order_qty: "order_qty", + total_ship_qty: "ship_qty", + total_balance_qty: "balance_qty", + }; + + // 그룹 행에서 특수 렌더링이 필요한 컬럼 + const renderGroupCell = (col: { key: string }, item: any) => { + if (col.key === "required_plan_qty") { + return ( + 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> + {formatNumber(item.required_plan_qty)} + + ); + } + if (col.key === "lead_time") { + return ( + toggleItemExpand(item.item_code)}> + {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"} + + ); + } + return ( + toggleItemExpand(item.item_code)}> + {formatNumber(item[col.key])} + + ); + }; + + return (
@@ -1028,15 +1058,11 @@ export default function ProductionPlanManagementPage() { 품목코드 품목명 - {isColVisible("total_order_qty") && 총수주량} - {isColVisible("total_ship_qty") && 출고량} - {isColVisible("total_balance_qty") && 잔량} - {isColVisible("current_stock") && 현재고} - {isColVisible("safety_stock") && 안전재고} - {isColVisible("existing_plan_qty") && 기생산계획량} - {isColVisible("in_progress_qty") && 생산진행} - {isColVisible("required_plan_qty") && 필요생산계획} - {isColVisible("lead_time") && 리드타임(일)} + {ts.visibleColumns.map((col) => ( + + {col.label} + + ))} @@ -1046,6 +1072,7 @@ export default function ProductionPlanManagementPage() { + {ts.visibleColumns.map((col) => { const v = (item as any)[col.key]; return ( @@ -1068,25 +1095,14 @@ export default function ProductionPlanManagementPage() { toggleItemExpand(item.item_code)}>{item.item_code} toggleItemExpand(item.item_code)}>{item.item_name} - {isColVisible("total_order_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_order_qty)}} - {isColVisible("total_ship_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_ship_qty)}} - {isColVisible("total_balance_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_balance_qty)}} - {isColVisible("current_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.current_stock)}} - {isColVisible("safety_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.safety_stock)}} - {isColVisible("existing_plan_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.existing_plan_qty)}} - {isColVisible("in_progress_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.in_progress_qty)}} - {isColVisible("required_plan_qty") && ( - 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> - {formatNumber(item.required_plan_qty)} - - )} - {isColVisible("lead_time") && ( - toggleItemExpand(item.item_code)}> - {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"} - - )} + {ts.visibleColumns.map((col) => renderGroupCell(col, item))} - {expandedItems.has(item.item_code) && item.orders?.map((detail) => ( + {expandedItems.has(item.item_code) && item.orders?.map((detail: any) => { + let remainColSpan = 0; + for (const col of ts.visibleColumns) { + if (!DETAIL_VALUE_MAP[col.key]) remainColSpan++; + } + return ( @@ -1101,19 +1117,28 @@ export default function ProductionPlanManagementPage() { - {isColVisible("total_order_qty") && {formatNumber(detail.order_qty)}} - {isColVisible("total_ship_qty") && {formatNumber(detail.ship_qty)}} - {isColVisible("total_balance_qty") && {formatNumber(detail.balance_qty)}} - - 납기일: {detail.due_date || "-"} - + {ts.visibleColumns.map((col) => { + const detailKey = DETAIL_VALUE_MAP[col.key]; + if (detailKey) { + return {formatNumber(detail[detailKey])}; + } + return null; + })} + {remainColSpan > 0 && ( + + 납기일: {detail.due_date || "-"} + + )} - ))} + ); + })} ); })}
+ ); + })()} )} diff --git a/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx index fa0e08c5..1bc3bc88 100644 --- a/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx @@ -742,10 +742,24 @@ export default function PurchaseOrderPage() { ) : ( (() => { const MASTER_KEYS = new Set(["purchase_no", "order_date", "supplier_name", "status", "memo"]); - const detailCols = ts.visibleColumns.filter(c => !MASTER_KEYS.has(c.key)); - const masterCols = ts.visibleColumns.filter(c => MASTER_KEYS.has(c.key)); const numCols = new Set(["order_qty", "received_qty", "remain_qty", "unit_price", "amount"]); + // ts.visibleColumns 순서를 따르되, 마스터/디테일 컬럼을 분리 + // 고정 컬럼(품목수)은 마스터 선행 컬럼 뒤에 배치 + const leadingMaster: typeof ts.visibleColumns = []; + const detailCols: typeof ts.visibleColumns = []; + const trailingMaster: typeof ts.visibleColumns = []; + let passedFirstDetail = false; + for (const col of ts.visibleColumns) { + if (MASTER_KEYS.has(col.key)) { + if (passedFirstDetail) trailingMaster.push(col); + else leadingMaster.push(col); + } else { + passedFirstDetail = true; + detailCols.push(col); + } + } + const renderDetailCell = (row: any, key: string) => { const val = row[key]; if (key === "status") return val ? {val} : "-"; @@ -753,23 +767,35 @@ export default function PurchaseOrderPage() { return val || "-"; }; + const renderMasterHead = (col: { key: string; label: string }) => ( + + {col.label} + + ); + + const renderMasterCell = (col: { key: string }, m: any, purchaseNo: string) => { + if (col.key === "purchase_no") return {purchaseNo}; + if (col.key === "order_date") return {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}; + if (col.key === "supplier_name") return {m.supplier_name || "-"}; + if (col.key === "status") return {m.status && {m.status}}; + if (col.key === "memo") return {m.memo || ""}; + return ; + }; + return ( - {ts.isVisible("purchase_no") && 발주번호} - {ts.isVisible("order_date") && 발주일} - {ts.isVisible("supplier_name") && 공급업체} + {leadingMaster.map(renderMasterHead)} 품목수 {detailCols.map(col => ( {col.label}{col.key === "order_qty" || col.key === "amount" ? " 합계" : ""} ))} - {ts.isVisible("status") && 상태} - {ts.isVisible("memo") && 메모} + {trailingMaster.map(renderMasterHead)} @@ -795,9 +821,7 @@ export default function PurchaseOrderPage() { { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !detailIds.includes(id)) : [...new Set([...prev, ...detailIds])]); }}> {}} /> - {ts.isVisible("purchase_no") && {purchaseNo}} - {ts.isVisible("order_date") && {m.order_date ? new Date(m.order_date).toLocaleDateString("ko-KR") : "-"}} - {ts.isVisible("supplier_name") && {m.supplier_name || "-"}} + {leadingMaster.map(col => renderMasterCell(col, m, purchaseNo))} {group.details.length}건 {detailCols.map(col => ( @@ -806,8 +830,7 @@ export default function PurchaseOrderPage() { : ""} ))} - {ts.isVisible("status") && {m.status && {m.status}}} - {ts.isVisible("memo") && {m.memo || ""}} + {trailingMaster.map(col => renderMasterCell(col, m, purchaseNo))} {isExpanded && group.details.map((row) => ( @@ -815,17 +838,14 @@ export default function PurchaseOrderPage() { { e.stopPropagation(); setCheckedIds(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id]); }}> {}} /> - {ts.isVisible("purchase_no") && } - {ts.isVisible("order_date") && } - {ts.isVisible("supplier_name") && } + {leadingMaster.map(col => )} {detailCols.map(col => ( {renderDetailCell(row, col.key)} ))} - {ts.isVisible("status") && } - {ts.isVisible("memo") && } + {trailingMaster.map(col => )} ))} diff --git a/frontend/app/(main)/COMPANY_9/purchase/purchase-item/page.tsx b/frontend/app/(main)/COMPANY_9/purchase/purchase-item/page.tsx index 1f7c4137..7f211a88 100644 --- a/frontend/app/(main)/COMPANY_9/purchase/purchase-item/page.tsx +++ b/frontend/app/(main)/COMPANY_9/purchase/purchase-item/page.tsx @@ -617,17 +617,21 @@ export default function PurchaseItemPage() { toast.success("다운로드 완료"); }; - // EDataTable 컬럼 정의 (구매품목) - const itemColumns: EDataTableColumn[] = [ - { key: "item_number", label: "품번", width: "w-[110px]" }, - { key: "item_name", label: "품명", minWidth: "min-w-[130px]" }, - { key: "size", label: "규격", width: "w-[80px]" }, - { key: "unit", label: "단위", width: "w-[60px]" }, - { key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }, - { key: "standard_price", label: "구매단가", width: "w-[90px]", align: "right", formatNumber: true }, - { key: "currency_code", label: "통화", width: "w-[50px]" }, - { key: "status", label: "상태", width: "w-[60px]" }, - ]; + // EDataTable 컬럼 정의 (구매품목) — ts.visibleColumns 기반 + const COLUMN_RENDER_MAP: Record> = { + item_number: { width: "w-[110px]" }, + item_name: { minWidth: "min-w-[130px]" }, + size: { width: "w-[80px]" }, + unit: { width: "w-[60px]" }, + standard_price: { width: "w-[90px]", align: "right", formatNumber: true }, + currency_code: { width: "w-[50px]" }, + status: { width: "w-[60px]" }, + }; + const itemColumns: EDataTableColumn[] = ts.visibleColumns.map((col): EDataTableColumn => ({ + key: col.key, + label: col.label, + ...COLUMN_RENDER_MAP[col.key], + })); return (
diff --git a/frontend/app/(main)/COMPANY_9/purchase/supplier/page.tsx b/frontend/app/(main)/COMPANY_9/purchase/supplier/page.tsx index 51c50aa5..521f770e 100644 --- a/frontend/app/(main)/COMPANY_9/purchase/supplier/page.tsx +++ b/frontend/app/(main)/COMPANY_9/purchase/supplier/page.tsx @@ -12,7 +12,7 @@ * - 납품처 등록 (delivery_destination) */ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -1229,47 +1229,44 @@ export default function SupplierManagementPage() { } }; - // 컬럼 가시성 헬퍼 - const isColumnVisible = (key: string) => ts.isVisible(key); - - const supplierColSpan = 1 + ["supplier_code", "supplier_name", "contact_person", "contact_phone", "division", "status"] - .filter((k) => isColumnVisible(k)).length; - - // EDataTable 컬럼 정의 (공급업체 목록) - const supplierColumns: EDataTableColumn[] = [ - ...(isColumnVisible("supplier_code") ? [{ key: "supplier_code", label: "공급업체코드", width: "w-[120px]" }] : []), - ...(isColumnVisible("supplier_name") ? [{ key: "supplier_name", label: "공급업체명", minWidth: "min-w-[140px]" }] : []), - ...(isColumnVisible("division") ? [{ - key: "division", - label: "공급업체유형", - width: "w-[80px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ...(isColumnVisible("contact_person") ? [{ key: "contact_person", label: "담당자", width: "w-[80px]" }] : []), - ...(isColumnVisible("contact_phone") ? [{ key: "contact_phone", label: "전화번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("email") ? [{ key: "email", label: "이메일", width: "w-[160px]" }] : []), - ...(isColumnVisible("business_number") ? [{ key: "business_number", label: "사업자번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("address") ? [{ key: "address", label: "주소", minWidth: "min-w-[150px]" }] : []), - ...(isColumnVisible("status") ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ]; + // EDataTable 컬럼 정의 (공급업체 목록) — ts.visibleColumns 순서를 따름 + const supplierColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + supplier_code: { width: "w-[120px]" }, + supplier_name: { minWidth: "min-w-[140px]" }, + division: { + width: "w-[80px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + contact_person: { width: "w-[80px]" }, + contact_phone: { width: "w-[120px]" }, + email: { width: "w-[160px]" }, + business_number: { width: "w-[120px]" }, + address: { minWidth: "min-w-[150px]" }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 엑셀 다운로드 const handleExcelDownload = async () => { diff --git a/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx index 5baf35d3..2c0e1338 100644 --- a/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx @@ -28,6 +28,7 @@ const GRID_COLUMNS = [ { key: "item_code", label: "품목코드" }, { key: "item_name", label: "품목명" }, { key: "inspection_type", label: "검사유형" }, + { key: "item_count", label: "항목수" }, { key: "is_active", label: "사용여부" }, ]; const ITEM_TABLE = "item_info"; @@ -420,18 +421,41 @@ export default function ItemInspectionInfoPage() { 0 && checkedIds.length === data.length} onCheckedChange={(v) => setCheckedIds(v ? data.map(r => r.id) : [])} /> - 품목코드 - 품목명 - 검사유형 - 항목수 - 사용여부 + {ts.visibleColumns.map((col) => ( + + {col.label} + + ))} - {groupedData.map((group) => { + {ts.groupData(groupedData).map((group) => { + if ((group as any)._isGroupSummary || (group as any)._isGroupHeader) return null; const isExpanded = expandedItems.has(group.item_code); - const groupIds = group.rows.map(r => r.id); - const allChecked = groupIds.every(id => checkedIds.includes(id)); + const groupIds = group.rows.map((r: any) => r.id); + const allChecked = groupIds.every((id: string) => checkedIds.includes(id)); + const renderCell = (key: string) => { + switch (key) { + case "item_code": return {group.item_code}; + case "item_name": return {group.item_name}; + case "inspection_type": return ( + +
+ {group.types.map((t: string) => {t})} +
+
+ ); + case "item_count": return {group.rows.filter((r: any) => r.inspection_standard_id).length}; + case "is_active": return ( + + + {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} + + + ); + default: return {(group as any)[key] ?? ""}; + } + }; return ( { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !groupIds.includes(id)) : [...new Set([...prev, ...groupIds])]); }}> {}} /> - {group.item_code} - {group.item_name} - -
- {group.types.map(t => {t})} -
-
- {group.rows.filter(r => r.inspection_standard_id).length} - - - {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} - - + {ts.visibleColumns.map((col) => renderCell(col.key))}
- {isExpanded && group.rows.filter(r => r.inspection_standard_id).map((row, i) => ( + {isExpanded && group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( diff --git a/frontend/app/(main)/COMPANY_9/sales/customer/page.tsx b/frontend/app/(main)/COMPANY_9/sales/customer/page.tsx index bddc7730..20b98727 100644 --- a/frontend/app/(main)/COMPANY_9/sales/customer/page.tsx +++ b/frontend/app/(main)/COMPANY_9/sales/customer/page.tsx @@ -12,7 +12,7 @@ * - 납품처 등록 (delivery_destination) */ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -820,12 +820,14 @@ export default function CustomerManagementPage() { const allItems = res.data?.data?.data || res.data?.data?.rows || []; setItemTotalCount(allItems.length); const existingItemIds = new Set(priceItems.map((p: any) => p.item_id || p.item_number)); - const SALES_CODES = ["CAT_ML8ZFVEL_1TOR"]; // 영업관리 카테고리 코드 - setItemSearchResults(allItems.filter((item: any) => { + const seenNumbers = new Set(); + const deduped = allItems.filter((item: any) => { if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false; - const divCodes = (item.division || "").split(",").map((c: string) => c.trim()); - return divCodes.some((code: string) => SALES_CODES.includes(code)); - })); + if (item.item_number && seenNumbers.has(item.item_number)) return false; + if (item.item_number) seenNumbers.add(item.item_number); + return true; + }); + setItemSearchResults(deduped); } catch { /* skip */ } finally { setItemSearchLoading(false); } }; @@ -1229,47 +1231,44 @@ export default function CustomerManagementPage() { } }; - // 컬럼 가시성 헬퍼 - const isColumnVisible = (key: string) => ts.isVisible(key); - - const customerColSpan = 1 + ["customer_code", "customer_name", "contact_person", "contact_phone", "division", "status"] - .filter((k) => isColumnVisible(k)).length; - - // EDataTable 컬럼 정의 (거래처 목록) - const customerColumns: EDataTableColumn[] = [ - ...(isColumnVisible("customer_code") ? [{ key: "customer_code", label: "거래처코드", width: "w-[120px]" }] : []), - ...(isColumnVisible("customer_name") ? [{ key: "customer_name", label: "거래처명", minWidth: "min-w-[140px]" }] : []), - ...(isColumnVisible("division") ? [{ - key: "division", - label: "거래유형", - width: "w-[80px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ...(isColumnVisible("contact_person") ? [{ key: "contact_person", label: "담당자", width: "w-[80px]" }] : []), - ...(isColumnVisible("contact_phone") ? [{ key: "contact_phone", label: "전화번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("email") ? [{ key: "email", label: "이메일", width: "w-[160px]" }] : []), - ...(isColumnVisible("business_number") ? [{ key: "business_number", label: "사업자번호", width: "w-[120px]" }] : []), - ...(isColumnVisible("address") ? [{ key: "address", label: "주소", minWidth: "min-w-[150px]" }] : []), - ...(isColumnVisible("status") ? [{ - key: "status", - label: "상태", - width: "w-[70px]", - render: (val: any) => - val ? ( - - {val} - - ) : null, - }] : []), - ]; + // EDataTable 컬럼 정의 (거래처 목록) — ts.visibleColumns 순서를 따름 + const customerColumns: EDataTableColumn[] = useMemo(() => { + const colProps: Record> = { + customer_code: { width: "w-[120px]" }, + customer_name: { minWidth: "min-w-[140px]" }, + division: { + width: "w-[80px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + contact_person: { width: "w-[80px]" }, + contact_phone: { width: "w-[120px]" }, + email: { width: "w-[160px]" }, + business_number: { width: "w-[120px]" }, + address: { minWidth: "min-w-[150px]" }, + status: { + width: "w-[70px]", + render: (val: any) => + val ? ( + + {val} + + ) : null, + }, + }; + return ts.visibleColumns.map((col) => ({ + key: col.key, + label: col.label, + ...colProps[col.key], + })); + }, [ts.visibleColumns]); // 엑셀 다운로드 const handleExcelDownload = async () => { diff --git a/frontend/app/(main)/COMPANY_9/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_9/sales/shipping-order/page.tsx index 4ab5a9ad..2ed29b40 100644 --- a/frontend/app/(main)/COMPANY_9/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_9/sales/shipping-order/page.tsx @@ -363,7 +363,7 @@ export default function ShippingOrderPage() { spec: item.spec, material: item.material, orderQty: item.orderQty, - planQty: item.planQty, + planQty: item.orderQty, shipQty: 0, sourceType: item.sourceType, shipmentPlanId: item.shipmentPlanId, diff --git a/frontend/components/common/ImageUpload.tsx b/frontend/components/common/ImageUpload.tsx index afd68a19..8fc52729 100644 --- a/frontend/components/common/ImageUpload.tsx +++ b/frontend/components/common/ImageUpload.tsx @@ -53,11 +53,14 @@ export function ImageUpload({ const [dragOver, setDragOver] = useState(false); const fileRef = useRef(null); - // 이미지 URL 결정 + // 이미지 URL 결정 (Next.js 프록시 우회 — 바이너리 스트림 500 에러 방지) + const apiBase = typeof window !== "undefined" + ? (process.env.NEXT_PUBLIC_API_URL || "").replace(/\/api\/?$/, "") + : ""; const imageUrl = value ? (value.startsWith("http") || value.startsWith("/")) ? value - : `/api/files/preview/${value}` + : `${apiBase}/api/files/preview/${value}` : null; const handleUpload = useCallback(async (file: File) => {