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) => {