diff --git a/backend-node/src/controllers/outboundController.ts b/backend-node/src/controllers/outboundController.ts index 268b8274..86156ca5 100644 --- a/backend-node/src/controllers/outboundController.ts +++ b/backend-node/src/controllers/outboundController.ts @@ -201,13 +201,32 @@ export async function create(req: AuthenticatedRequest, res: Response) { // 재고 레코드가 없으면 0으로 생성 (마이너스 방지) await client.query( `INSERT INTO inventory_stock ( - company_code, item_code, warehouse_code, location_code, + id, company_code, item_code, warehouse_code, location_code, current_qty, safety_qty, last_out_date, created_date, updated_date, writer - ) VALUES ($1, $2, $3, $4, '0', '0', NOW(), NOW(), NOW(), $5)`, + ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '0', '0', NOW(), NOW(), NOW(), $5)`, [companyCode, itemCode, whCode, locCode, userId] ); } + + // 재고 이력 기록 (inventory_history) + const afterStockRes = await client.query( + `SELECT current_qty FROM inventory_stock + WHERE company_code = $1 AND item_code = $2 + AND COALESCE(warehouse_code, '') = COALESCE($3, '') + AND COALESCE(location_code, '') = COALESCE($4, '') + LIMIT 1`, + [companyCode, itemCode, whCode || '', locCode || ''] + ); + const afterQty = afterStockRes.rows[0]?.current_qty || '0'; + await client.query( + `INSERT INTO inventory_history ( + id, company_code, item_number, warehouse_code, location_code, + history_type, history_date, change_qty, after_qty, reason, + created_by, created_date + ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '출고', NOW()::date, $5, $6, $7, $8, NOW())`, + [companyCode, itemCode, whCode, locCode, String(-outQty), afterQty, item.outbound_type || '출고', userId] + ); } // 판매출고인 경우 출하지시의 ship_qty 업데이트 diff --git a/backend-node/src/controllers/receivingController.ts b/backend-node/src/controllers/receivingController.ts index 51986e3c..ff892318 100644 --- a/backend-node/src/controllers/receivingController.ts +++ b/backend-node/src/controllers/receivingController.ts @@ -253,6 +253,25 @@ export async function create(req: AuthenticatedRequest, res: Response) { [companyCode, itemCode, whCode, locCode, String(inQty), userId] ); } + + // 2b-2. 재고 이력 기록 (inventory_history) + const afterStockRes = await client.query( + `SELECT current_qty FROM inventory_stock + WHERE company_code = $1 AND item_code = $2 + AND COALESCE(warehouse_code, '') = COALESCE($3, '') + AND COALESCE(location_code, '') = COALESCE($4, '') + LIMIT 1`, + [companyCode, itemCode, whCode || '', locCode || ''] + ); + const afterQty = afterStockRes.rows[0]?.current_qty || String(inQty); + await client.query( + `INSERT INTO inventory_history ( + id, company_code, item_number, warehouse_code, location_code, + history_type, history_date, change_qty, after_qty, reason, + created_by, created_date + ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '입고', NOW()::date, $5, $6, $7, $8, NOW())`, + [companyCode, itemCode, whCode, locCode, String(inQty), afterQty, item.inbound_type || '입고', userId] + ); } // 2c. 구매입고인 경우 발주의 received_qty 업데이트 — 기존 로직 유지 @@ -516,6 +535,25 @@ export async function deleteReceiving(req: AuthenticatedRequest, res: Response) AND COALESCE(location_code, '') = COALESCE($5, '')`, [inQty, companyCode, itemCode, whCode || '', locCode || ''] ); + + // 입고취소 이력 기록 + const afterStockRes = await client.query( + `SELECT current_qty FROM inventory_stock + WHERE company_code = $1 AND item_code = $2 + AND COALESCE(warehouse_code, '') = COALESCE($3, '') + AND COALESCE(location_code, '') = COALESCE($4, '') + LIMIT 1`, + [companyCode, itemCode, whCode || '', locCode || ''] + ); + const afterQty = afterStockRes.rows[0]?.current_qty || '0'; + await client.query( + `INSERT INTO inventory_history ( + id, company_code, item_number, warehouse_code, location_code, + history_type, history_date, change_qty, after_qty, reason, + created_by, created_date + ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '입고취소', NOW()::date, $5, $6, '입고 삭제에 의한 롤백', $7, NOW())`, + [companyCode, itemCode, whCode, locCode, String(-inQty), afterQty, userId] + ); } // 구매입고 발주 롤백: purchase_order_mng 기반 diff --git a/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx index ec0c401d..29be86d7 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx @@ -96,7 +96,7 @@ const TAB_CONFIGS: TabConfig[] = [ columns: [ { key: "carrier_code", label: "업체코드", width: "120px" }, { key: "carrier_name", label: "업체명", width: "160px" }, - { key: "carrier_type", label: "유형", width: "100px" }, + { key: "carrier_type", label: "업체유형", width: "100px" }, { key: "contact_person", label: "담당자", width: "100px" }, { key: "contact_phone", label: "연락처", width: "130px" }, { key: "email", label: "이메일", width: "180px" }, @@ -107,12 +107,12 @@ const TAB_CONFIGS: TabConfig[] = [ formFields: [ { key: "carrier_code", label: "업체코드", type: "text", required: true, placeholder: "업체코드를 입력해주세요" }, { key: "carrier_name", label: "업체명", type: "text", required: true, placeholder: "업체명을 입력해주세요" }, - { key: "carrier_type", label: "유형", type: "select", categoryKey: "carrier_mng:carrier_type", placeholder: "유형을 선택해주세요" }, + { key: "carrier_type", label: "업체유형", type: "select", required: true, categoryKey: "carrier_mng:carrier_type", placeholder: "업체유형을 선택해주세요" }, { key: "contact_person", label: "담당자", type: "text", placeholder: "담당자명" }, - { key: "contact_phone", label: "연락처", type: "text", placeholder: "010-0000-0000" }, + { key: "contact_phone", label: "연락처", type: "text", placeholder: "예: 02-1234-5678" }, { key: "email", label: "이메일", type: "text", placeholder: "email@example.com" }, { key: "address", label: "주소", type: "text", placeholder: "주소를 입력해주세요" }, - { key: "rating", label: "등급", type: "select", options: [1, 2, 3, 4, 5].map((v) => ({ value: String(v), label: `${v}등급` })) }, + { key: "rating", label: "등급", type: "select", categoryKey: "carrier_mng:rating", placeholder: "등급을 선택해주세요" }, { key: "status", label: "상태", type: "select", categoryKey: "carrier_mng:status", placeholder: "상태를 선택해주세요" }, ], }, @@ -155,6 +155,7 @@ const TAB_CONFIGS: TabConfig[] = [ { key: "contract_start_date", label: "시작일", width: "110px" }, { key: "contract_end_date", label: "종료일", width: "110px" }, { key: "contract_amount", label: "계약금액", width: "130px", align: "right", formatNumber: true }, + { key: "contact_person", label: "담당자", width: "100px" }, { key: "status", label: "상태", width: "80px", align: "center" }, ], formFields: [ @@ -163,6 +164,7 @@ const TAB_CONFIGS: TabConfig[] = [ { key: "contract_start_date", label: "시작일", type: "date", required: true }, { key: "contract_end_date", label: "종료일", type: "date", required: true }, { key: "contract_amount", label: "계약금액", type: "number", placeholder: "0" }, + { key: "contact_person", label: "담당자", type: "text", placeholder: "담당자명을 입력해주세요" }, { key: "status", label: "상태", type: "select", categoryKey: "carrier_contract_mng:status", placeholder: "상태를 선택해주세요" }, ], }, @@ -184,9 +186,8 @@ const TAB_CONFIGS: TabConfig[] = [ ], formFields: [ { key: "route_code", label: "구간코드", type: "text", required: true, placeholder: "구간코드를 입력해주세요" }, - { key: "route_name", label: "구간명", type: "text", required: true, placeholder: "구간명을 입력해주세요" }, - { key: "departure", label: "출발지", type: "text", placeholder: "출발지" }, - { key: "destination", label: "도착지", type: "text", placeholder: "도착지" }, + { key: "departure", label: "출발지", type: "text", required: true, placeholder: "출발지" }, + { key: "destination", label: "도착지", type: "text", required: true, placeholder: "도착지" }, { key: "distance_km", label: "거리(km)", type: "number", placeholder: "0" }, { key: "avg_time_hours", label: "평균시간(h)", type: "number", placeholder: "0" }, { key: "route_type", label: "구간유형", type: "select", categoryKey: "delivery_route_mng:route_type", placeholder: "유형을 선택해주세요" }, @@ -202,19 +203,21 @@ const TAB_CONFIGS: TabConfig[] = [ columns: [ { key: "vehicle_code", label: "차량코드", width: "120px" }, { key: "vehicle_number", label: "차량번호", width: "120px" }, - { key: "vehicle_type", label: "차종", width: "100px" }, + { key: "vehicle_type", label: "차량유형", width: "100px" }, { key: "carrier_code", label: "운송업체", width: "120px" }, { key: "load_capacity_kg", label: "적재용량(kg)", width: "120px", align: "right", formatNumber: true }, { key: "driver_name", label: "운전자", width: "100px" }, + { key: "last_maintenance_date", label: "최종정비일", width: "110px" }, { key: "status", label: "상태", width: "80px", align: "center" }, ], formFields: [ { key: "vehicle_code", label: "차량코드", type: "text", required: true, placeholder: "차량코드를 입력해주세요" }, { key: "vehicle_number", label: "차량번호", type: "text", required: true, placeholder: "12가 3456" }, - { key: "vehicle_type", label: "차종", type: "select", categoryKey: "carrier_vehicle_mng:vehicle_type", placeholder: "차종을 선택해주세요" }, + { key: "vehicle_type", label: "차량유형", type: "select", required: true, categoryKey: "carrier_vehicle_mng:vehicle_type", placeholder: "차량유형을 선택해주세요" }, { key: "carrier_code", label: "운송업체", type: "smartselect", required: true, referenceKey: "carrier" }, { key: "load_capacity_kg", label: "적재용량(kg)", type: "number", placeholder: "0" }, { key: "driver_name", label: "운전자", type: "text", placeholder: "운전자명" }, + { key: "last_maintenance_date", label: "최종정비일", type: "date" }, { key: "status", label: "상태", type: "select", categoryKey: "carrier_vehicle_mng:status", placeholder: "상태를 선택해주세요" }, ], }, @@ -433,16 +436,22 @@ export default function LogisticsInfoPage() { } try { + // 배송구간: 출발지→도착지 로 구간명 자동 생성 + const saveData = { ...formData }; + if (activeTab === "route" && saveData.departure && saveData.destination) { + saveData.route_name = `${saveData.departure}→${saveData.destination}`; + } + if (editMode && editId) { await apiClient.put(`/table-management/tables/${config.tableName}/edit`, { originalData: { id: editId }, - updatedData: formData, + updatedData: saveData, }); toast.success("수정이 완료되었어요."); } else { await apiClient.post( `/table-management/tables/${config.tableName}/add`, - { id: crypto.randomUUID(), ...formData } + { id: crypto.randomUUID(), ...saveData } ); toast.success("등록이 완료되었어요."); } diff --git a/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx index f9422f27..6743e03e 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx @@ -18,7 +18,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; @@ -43,6 +43,7 @@ import { Settings2, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { toast } from "sonner"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; @@ -50,6 +51,7 @@ import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSea import { getOutboundList, createOutbound, + updateOutbound, deleteOutbound, generateOutboundNumber, getOutboundWarehouses, @@ -146,6 +148,10 @@ export default function OutboundPage() { const [selectedItems, setSelectedItems] = useState([]); const [saving, setSaving] = useState(false); + // 수정 모드 (등록 모달을 재활용) + const [editMode, setEditMode] = useState(false); + const [editItemIds, setEditItemIds] = useState([]); + // 소스 데이터 const [sourceKeyword, setSourceKeyword] = useState(""); const [sourceLoading, setSourceLoading] = useState(false); @@ -249,6 +255,8 @@ export default function OutboundPage() { const openRegisterModal = async () => { const defaultType = "판매출고"; + setEditMode(false); + setEditItemIds([]); setModalOutboundType(defaultType); setModalOutboundDate(new Date().toISOString().split("T")[0]); setModalWarehouse(""); @@ -273,6 +281,43 @@ export default function OutboundPage() { } }; + // 수정 모달 열기 (같은 출고번호 묶어서) + const openEditModal = (row: OutboundItem) => { + const outNo = row.outbound_number; + const grouped = data.filter((d) => d.outbound_number === outNo); + const first = grouped[0] || row; + + setEditMode(true); + setEditItemIds(grouped.map((g) => g.id)); + setModalOutboundNo(outNo); + setModalOutboundType(first.outbound_type || "판매출고"); + setModalOutboundDate(first.outbound_date ? first.outbound_date.slice(0, 10) : ""); + setModalWarehouse(first.warehouse_code || ""); + setModalLocation(first.location_code || ""); + setModalManager(first.manager_id || ""); + setModalMemo(first.memo || ""); + setSelectedItems( + grouped.map((g) => ({ + key: g.id, + outbound_type: g.outbound_type || "", + reference_number: g.reference_number || "", + customer_code: g.customer_code || "", + customer_name: g.customer_name || "", + item_number: g.item_code || "", + item_name: g.item_name || "", + spec: g.specification || "", + material: g.material || "", + unit: g.unit || "", + outbound_qty: Number(g.outbound_qty) || 0, + unit_price: Number(g.unit_price) || 0, + total_amount: Number(g.total_amount) || 0, + source_type: g.source_type || "", + source_id: g.source_id || "", + })) + ); + setIsModalOpen(true); + }; + const searchSourceData = useCallback(async () => { setSourcePage(1); await loadSourceData(modalOutboundType, sourceKeyword || undefined); @@ -445,44 +490,67 @@ export default function OutboundPage() { setSaving(true); try { - const res = await createOutbound({ - outbound_number: modalOutboundNo, - outbound_date: modalOutboundDate, - warehouse_code: modalWarehouse || undefined, - location_code: modalLocation || undefined, - manager_id: modalManager || undefined, - memo: modalMemo || undefined, - items: selectedItems.map((item) => ({ - outbound_type: item.outbound_type, - reference_number: item.reference_number, - customer_code: item.customer_code, - customer_name: item.customer_name, - item_code: item.item_number, - item_name: item.item_name, - spec: item.spec, - material: item.material, - unit: item.unit, - outbound_qty: item.outbound_qty, - unit_price: item.unit_price, - total_amount: item.total_amount, - source_type: item.source_type, - source_id: item.source_id, - outbound_status: "출고완료", - })), - }); - - if (res.success) { - alert(res.message || "출고 등록 완료"); + if (editMode) { + // 수정 모드: 각 아이템별 update + await Promise.all( + selectedItems.map((item) => + updateOutbound(item.key, { + outbound_date: modalOutboundDate, + outbound_qty: item.outbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + warehouse_code: modalWarehouse || undefined, + location_code: modalLocation || undefined, + manager_id: modalManager || undefined, + memo: modalMemo || undefined, + } as any) + ) + ); + toast.success("출고 정보를 수정했어요"); setIsModalOpen(false); fetchList(); + } else { + const res = await createOutbound({ + outbound_number: modalOutboundNo, + outbound_date: modalOutboundDate, + warehouse_code: modalWarehouse || undefined, + location_code: modalLocation || undefined, + manager_id: modalManager || undefined, + memo: modalMemo || undefined, + items: selectedItems.map((item) => ({ + outbound_type: item.outbound_type, + reference_number: item.reference_number, + customer_code: item.customer_code, + customer_name: item.customer_name, + item_code: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + outbound_qty: item.outbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_type: item.source_type, + source_id: item.source_id, + outbound_status: "출고완료", + })), + }); + + if (res.success) { + toast.success(res.message || "출고 등록 완료"); + setIsModalOpen(false); + fetchList(); + } } } catch { - alert("출고 등록 중 오류가 발생했습니다."); + toast.error(editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다."); } finally { setSaving(false); } }; + + // 합계 계산 const totalSummary = useMemo(() => { return { @@ -593,6 +661,7 @@ export default function OutboundPage() { checkedIds.includes(row.id) && "bg-primary/5" )} onClick={() => toggleCheck(row.id)} + onDoubleClick={() => openEditModal(row)} > - 출고 등록 - 출고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해주세요. + {editMode ? "출고 수정" : "출고 등록"} + {editMode ? "출고 정보를 수정해주세요." : "출고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가해주세요."} - {/* 출고유형 선택 */} -
+ {/* 출고유형 선택 (수정 모드에서는 숨김) */} + {!editMode &&
출고유형 )}
- + } - e.stopPropagation()} /> + {!editMode && e.stopPropagation()} />} {/* 우측: 출고 정보 + 선택 품목 */} - +

출고 정보

@@ -906,7 +975,7 @@ export default function OutboundPage() { 수량 단가 금액 - + {!editMode && } @@ -946,7 +1015,7 @@ export default function OutboundPage() { {item.total_amount.toLocaleString()} - + {!editMode && - + } ))} diff --git a/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx index 1a17a9a7..c95e0cbf 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx @@ -43,6 +43,7 @@ import { ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { Plus, Trash2, @@ -51,6 +52,9 @@ import { MapPin, Building2, Settings2, + Layers, + Info, + Eye, } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; @@ -135,6 +139,18 @@ export default function WarehouseManagementPage() { const [locationForm, setLocationForm] = useState>({}); const [locationSaving, setLocationSaving] = useState(false); + // 모달: 랙 구조 일괄 등록 + const [rackModalOpen, setRackModalOpen] = useState(false); + const [rackFloor, setRackFloor] = useState(""); + const [rackZone, setRackZone] = useState(""); + const [rackConditions, setRackConditions] = useState< + { id: string; startRow: number; endRow: number; levels: number }[] + >([]); + const [rackLocationType, setRackLocationType] = useState(""); + const [rackStatus, setRackStatus] = useState(""); + const [rackPreview, setRackPreview] = useState([]); + const [rackSaving, setRackSaving] = useState(false); + // 카테고리 옵션 const [categoryOptions, setCategoryOptions] = useState< Record @@ -169,7 +185,7 @@ export default function WarehouseManagementPage() { setCategoryOptions(whOpts); const locOpts: Record = {}; - for (const col of ["location_type", "status"]) { + for (const col of ["location_type", "status", "floor", "zone"]) { try { const res = await apiClient.get( `/table-categories/${LOCATION_TABLE}/${col}/values` @@ -461,6 +477,134 @@ export default function WarehouseManagementPage() { } }; + // ─── 랙 구조 일괄 등록 ─── + + const openRackModal = () => { + setRackFloor(""); + setRackZone(""); + setRackConditions([]); + setRackLocationType(""); + setRackStatus(""); + setRackPreview([]); + setRackSaving(false); + setRackModalOpen(true); + }; + + const addRackCondition = () => { + const lastEnd = rackConditions.length > 0 + ? rackConditions[rackConditions.length - 1].endRow + : 0; + setRackConditions((prev) => [ + ...prev, + { id: crypto.randomUUID(), startRow: lastEnd + 1, endRow: lastEnd + 1, levels: 1 }, + ]); + }; + + const updateRackCondition = (id: string, field: string, value: number) => { + setRackConditions((prev) => + prev.map((c) => (c.id === id ? { ...c, [field]: value } : c)) + ); + }; + + const removeRackCondition = (id: string) => { + setRackConditions((prev) => prev.filter((c) => c.id !== id)); + }; + + const generateRackPreview = () => { + if (!rackFloor.trim() || !rackZone.trim()) { + toast.error("층과 구역을 입력해주세요"); + return; + } + if (rackConditions.length === 0) { + toast.error("조건을 1개 이상 추가해주세요"); + return; + } + // 조건 유효성 검사 + for (const cond of rackConditions) { + if (cond.startRow < 1 || cond.endRow < 1 || cond.levels < 1) { + toast.error("열 범위와 단 수는 1 이상이어야 합니다"); + return; + } + if (cond.endRow < cond.startRow) { + toast.error("끝 열은 시작 열보다 크거나 같아야 합니다"); + return; + } + } + + const whCode = selectedWarehouse?.warehouse_code || ""; + // 카테고리 코드→라벨 변환 (셀렉트에서 코드가 저장되므로) + const floorOpts = locationCategoryOptions["floor"] || []; + const zoneOpts = locationCategoryOptions["zone"] || []; + const floorLabel = floorOpts.find(o => o.code === rackFloor)?.label || rackFloor.trim(); + const zoneLabel = zoneOpts.find(o => o.code === rackZone)?.label || rackZone.trim(); + const floorCode = floorLabel.replace(/층$/, ""); + const zoneCode = zoneLabel.replace(/구역$/, ""); + + const items: any[] = []; + for (const cond of rackConditions) { + for (let row = cond.startRow; row <= cond.endRow; row++) { + for (let level = 1; level <= cond.levels; level++) { + const rowStr = String(row).padStart(2, "0"); + const locationCode = `${whCode}-${floorCode}${zoneCode}-${rowStr}-${level}`; + const locationName = `${zoneCode}구역-${rowStr}열-${level}단`; + items.push({ + location_code: locationCode, + location_name: locationName, + warehouse_code: whCode, + floor: floorLabel, + zone: zoneLabel, + row_num: String(row), + level_num: String(level), + location_type: rackLocationType, + status: rackStatus, + }); + } + } + } + setRackPreview(items); + }; + + const handleRackBulkSave = async () => { + if (rackPreview.length === 0) { + toast.error("미리보기를 먼저 생성해주세요"); + return; + } + setRackSaving(true); + try { + let successCount = 0; + for (const item of rackPreview) { + await apiClient.post( + `/table-management/tables/${LOCATION_TABLE}/add`, + { id: crypto.randomUUID(), ...item } + ); + successCount++; + } + toast.success(`${successCount}개의 위치가 등록되었어요`); + setRackModalOpen(false); + fetchLocations(); + } catch (err) { + toast.error("일괄 등록 중 오류가 발생했어요"); + } finally { + setRackSaving(false); + } + }; + + // 랙 조건별 통계 + const getRackConditionCount = (cond: { startRow: number; endRow: number; levels: number }) => { + if (cond.endRow < cond.startRow || cond.startRow < 1 || cond.levels < 1) return 0; + return (cond.endRow - cond.startRow + 1) * cond.levels; + }; + + // 랙 미리보기 통계 + const rackStats = { + totalLocations: rackPreview.length, + totalRows: rackConditions.reduce((acc, c) => { + if (c.endRow >= c.startRow && c.startRow >= 1) return acc + (c.endRow - c.startRow + 1); + return acc; + }, 0), + maxLevels: rackConditions.reduce((acc, c) => Math.max(acc, c.levels || 0), 0), + }; + // 엑셀 내보내기 const handleExcelExport = () => { if (warehouses.length === 0) { @@ -655,6 +799,15 @@ export default function WarehouseManagementPage() { 위치 등록 + +
+ + {rackConditions.length === 0 ? ( +
+ +

+ 조건을 추가하여 랙 구조를 설정하세요 +

+ +
+ ) : ( +
+ {rackConditions.map((cond, idx) => ( +
+ + 조건{idx + 1} + +
+ + updateRackCondition(cond.id, "startRow", Number(e.target.value) || 0) + } + placeholder="시작" + /> + ~ + + updateRackCondition(cond.id, "endRow", Number(e.target.value) || 0) + } + placeholder="끝" + /> + 열, + + updateRackCondition(cond.id, "levels", Number(e.target.value) || 0) + } + placeholder="단" + /> + +
+ {getRackConditionCount(cond) > 0 && ( + + {cond.startRow}열 ~ {cond.endRow}열 × {cond.levels}단 = {getRackConditionCount(cond)}개 + + )} + +
+ ))} +
+ )} +
+ + {/* 공통 설정 */} +
+

+ ⚙️ 공통 설정 +

+
+
+ + +
+
+ + +
+
+
+ + {/* 등록 미리보기 */} +
+
+

+ 👁️ 등록 미리보기 +

+ +
+ + {rackPreview.length > 0 && ( + <> + {/* 통계 카드 */} +
+
+

총 위치

+

{rackStats.totalLocations}개

+
+
+

열 수

+

{rackStats.totalRows}개

+
+
+

최대 단

+

{rackStats.maxLevels}

+
+
+ + {/* 미리보기 테이블 */} +
+ + + + No + 위치코드 + 위치명 + + 구역 + + + 유형 + 비고 + + + + {rackPreview.map((item, idx) => ( + + + {idx + 1} + + {item.location_code} + {item.location_name} + {item.floor} + {item.zone} + {item.row_num} + {item.level_num} + + {resolveCategory(locationCategoryOptions, "location_type", item.location_type) || "-"} + + - + + ))} + +
+
+ + )} + + {rackPreview.length === 0 && ( +
+ 위의 설정을 완료한 뒤 미리보기 생성 버튼을 클릭하세요 +
+ )} +
+
+ + + + + + +
+ + >({}); + const [userOptions, setUserOptions] = useState<{ code: string; label: string }[]>([]); /* ═══════════════════ 카테고리 로드 ═══════════════════ */ useEffect(() => { @@ -98,7 +117,14 @@ export default function InspectionManagementPage() { const catList = [ { table: INSPECTION_TABLE, col: "inspection_type" }, { table: INSPECTION_TABLE, col: "apply_type" }, + { table: INSPECTION_TABLE, col: "inspection_method" }, + { table: INSPECTION_TABLE, col: "judgment_criteria" }, + { table: INSPECTION_TABLE, col: "unit" }, { table: DEFECT_TABLE, col: "defect_type" }, + { table: DEFECT_TABLE, col: "severity" }, + { table: DEFECT_TABLE, col: "inspection_type" }, + { table: DEFECT_TABLE, col: "is_active" }, + { table: EQUIPMENT_TABLE, col: "equipment_type" }, { table: EQUIPMENT_TABLE, col: "equipment_status" }, ]; await Promise.all( @@ -108,27 +134,63 @@ export default function InspectionManagementPage() { if (res.data?.data?.length > 0) { optMap[`${table}.${col}`] = flattenCategories(res.data.data); } - } catch { /* skip */ } - }) + } catch { + /* skip */ + } + }), ); setCatOptions(optMap); + // 사용자 목록 로드 + try { + const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { + page: 1, + size: 500, + autoFilter: true, + }); + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; + setUserOptions( + users.map((u: any) => ({ + code: u.user_id || u.id, + label: `${u.user_name || u.name || u.user_id}${u.dept_name ? ` (${u.dept_name})` : ""}`, + })), + ); + } catch { + /* skip */ + } }; load(); }, []); const getCatLabel = (table: string, col: string, code: string) => { + if (!code) return ""; const opts = catOptions[`${table}.${col}`]; if (!opts) return code; - return opts.find(o => o.code === code)?.label || code; + // 쉼표 구분 다중 코드 지원 + if (code.includes(",")) { + return code + .split(",") + .filter(Boolean) + .map((c) => opts.find((o) => o.code === c)?.label || c) + .join(", "); + } + return opts.find((o) => o.code === code)?.label || code; }; /* ═══════════════════ 데이터 조회 ═══════════════════ */ + // 다중값 컬럼 (쉼표 구분 저장) — 서버 equals 대신 contains 사용 + const MULTI_VALUE_COLUMNS = ["inspection_type"]; + const fetchInspections = useCallback(async () => { setInspLoading(true); try { - const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); + const filters = searchFilters.map((f) => ({ + columnName: f.columnName, + operator: MULTI_VALUE_COLUMNS.includes(f.columnName) ? "contains" : f.operator, + value: f.value, + })); const res = await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/data`, { - page: 1, size: 500, + page: 1, + size: 500, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); @@ -146,7 +208,9 @@ export default function InspectionManagementPage() { setDefLoading(true); try { const res = await apiClient.post(`/table-management/tables/${DEFECT_TABLE}/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, + size: 500, + autoFilter: true, }); const rows = res.data?.data?.data || res.data?.data?.rows || []; setDefects(rows); @@ -162,7 +226,9 @@ export default function InspectionManagementPage() { setEqLoading(true); try { const res = await apiClient.post(`/table-management/tables/${EQUIPMENT_TABLE}/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, + size: 500, + autoFilter: true, }); const rows = res.data?.data?.data || res.data?.data?.rows || []; setEquipments(rows); @@ -174,63 +240,149 @@ export default function InspectionManagementPage() { } }, []); - useEffect(() => { fetchInspections(); }, [fetchInspections]); - useEffect(() => { fetchDefects(); fetchEquipments(); }, []); + useEffect(() => { + fetchInspections(); + }, [fetchInspections]); + useEffect(() => { + fetchDefects(); + fetchEquipments(); + }, []); /* ───── 클라이언트 필터 ───── */ const filteredDefects = defKeyword.trim() - ? defects.filter(r => (r.defect_name || "").toLowerCase().includes(defKeyword.toLowerCase()) || (r.defect_type || "").toLowerCase().includes(defKeyword.toLowerCase())) + ? defects.filter( + (r) => + (r.defect_name || "").toLowerCase().includes(defKeyword.toLowerCase()) || + (r.defect_type || "").toLowerCase().includes(defKeyword.toLowerCase()), + ) : defects; const filteredEquipments = eqKeyword.trim() - ? equipments.filter(r => (r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) || (r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase())) + ? equipments.filter( + (r) => + (r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) || + (r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()), + ) : equipments; /* ═══════════════════ 검사기준 CRUD ═══════════════════ */ - const openInspCreate = () => { setInspForm({}); setInspEditMode(false); setInspModalOpen(true); }; - const openInspEdit = (row: any) => { setInspForm({ ...row }); setInspEditMode(true); setInspModalOpen(true); }; + const openInspCreate = () => { + setInspForm({}); + setInspEditMode(false); + setInspModalOpen(true); + }; + const openInspEdit = (row: any) => { + setInspForm({ ...row }); + setInspEditMode(true); + setInspModalOpen(true); + }; const saveInspection = async () => { - if (!inspForm.inspection_standard) { toast.error("검사기준은 필수 입력이에요"); return; } + if (!inspForm.inspection_code) { + toast.error("검사코드는 필수예요"); + return; + } + if (!inspForm.inspection_type) { + toast.error("유형을 1개 이상 선택해주세요"); + return; + } + if (!inspForm.inspection_criteria) { + toast.error("검사기준은 필수예요"); + return; + } + if (!inspForm.inspection_item) { + toast.error("검사항목은 필수예요"); + return; + } + if (!inspForm.judgment_criteria) { + toast.error("판단기준은 필수예요"); + return; + } setInspSaving(true); try { if (inspEditMode) { await apiClient.put(`/table-management/tables/${INSPECTION_TABLE}/edit`, { - originalData: { id: inspForm.id }, updatedData: inspForm, + originalData: { id: inspForm.id }, + updatedData: inspForm, }); toast.success("검사기준을 수정했어요"); } else { - await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/add`, { id: crypto.randomUUID(), ...inspForm }); + await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/add`, { + id: crypto.randomUUID(), + ...inspForm, + }); toast.success("검사기준을 등록했어요"); } setInspModalOpen(false); fetchInspections(); - } catch { toast.error("저장에 실패했어요"); } - finally { setInspSaving(false); } + } catch { + toast.error("저장에 실패했어요"); + } finally { + setInspSaving(false); + } }; const deleteInspections = async () => { - if (inspChecked.length === 0) { toast.error("삭제할 항목을 선택해주세요"); return; } - const ok = await confirm("검사기준 삭제", { description: `선택한 ${inspChecked.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.` }); + if (inspChecked.length === 0) { + toast.error("삭제할 항목을 선택해주세요"); + return; + } + const ok = await confirm("검사기준 삭제", { + description: `선택한 ${inspChecked.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.`, + }); if (!ok) return; try { await apiClient.delete(`/table-management/tables/${INSPECTION_TABLE}/delete`, { - data: inspChecked.map(id => ({ id })), + data: inspChecked.map((id) => ({ id })), }); toast.success(`${inspChecked.length}건을 삭제했어요`); setInspChecked([]); fetchInspections(); - } catch { toast.error("삭제에 실패했어요"); } + } catch { + toast.error("삭제에 실패했어요"); + } }; /* ═══════════════════ 불량관리 CRUD ═══════════════════ */ - const openDefCreate = () => { setDefForm({}); setDefEditMode(false); setDefModalOpen(true); }; - const openDefEdit = (row: any) => { setDefForm({ ...row }); setDefEditMode(true); setDefModalOpen(true); }; + const openDefCreate = () => { + setDefForm({}); + setDefEditMode(false); + setDefModalOpen(true); + }; + const openDefEdit = (row: any) => { + setDefForm({ ...row }); + setDefEditMode(true); + setDefModalOpen(true); + }; const saveDefect = async () => { - if (!defForm.defect_name) { toast.error("불량명은 필수 입력이에요"); return; } + if (!defForm.defect_code) { + toast.error("불량코드는 필수예요"); + return; + } + if (!defForm.defect_type) { + toast.error("불량유형은 필수예요"); + return; + } + if (!defForm.defect_name) { + toast.error("불량명은 필수예요"); + return; + } + if (!defForm.severity) { + toast.error("심각도는 필수예요"); + return; + } + if (!defForm.defect_content) { + toast.error("불량내용은 필수예요"); + return; + } + if (!defForm.inspection_type) { + toast.error("검사유형을 1개 이상 선택해주세요"); + return; + } setDefSaving(true); try { if (defEditMode) { await apiClient.put(`/table-management/tables/${DEFECT_TABLE}/edit`, { - originalData: { id: defForm.id }, updatedData: defForm, + originalData: { id: defForm.id }, + updatedData: defForm, }); toast.success("불량유형을 수정했어요"); } else { @@ -239,33 +391,73 @@ export default function InspectionManagementPage() { } setDefModalOpen(false); fetchDefects(); - } catch { toast.error("저장에 실패했어요"); } - finally { setDefSaving(false); } + } catch { + toast.error("저장에 실패했어요"); + } finally { + setDefSaving(false); + } }; const deleteDefects = async () => { - if (defChecked.length === 0) { toast.error("삭제할 항목을 선택해주세요"); return; } - const ok = await confirm("불량유형 삭제", { description: `선택한 ${defChecked.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.` }); + if (defChecked.length === 0) { + toast.error("삭제할 항목을 선택해주세요"); + return; + } + const ok = await confirm("불량유형 삭제", { + description: `선택한 ${defChecked.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.`, + }); if (!ok) return; try { await apiClient.delete(`/table-management/tables/${DEFECT_TABLE}/delete`, { - data: defChecked.map(id => ({ id })), + data: defChecked.map((id) => ({ id })), }); toast.success(`${defChecked.length}건을 삭제했어요`); setDefChecked([]); fetchDefects(); - } catch { toast.error("삭제에 실패했어요"); } + } catch { + toast.error("삭제에 실패했어요"); + } }; /* ═══════════════════ 검사장비 CRUD ═══════════════════ */ - const openEqCreate = () => { setEqForm({}); setEqEditMode(false); setEqModalOpen(true); }; - const openEqEdit = (row: any) => { setEqForm({ ...row }); setEqEditMode(true); setEqModalOpen(true); }; + const openEqCreate = () => { + const maxNum = + equipments + .map((e: any) => e.equipment_code || "") + .filter((c: string) => /^EQP-\d+$/.test(c)) + .map((c: string) => parseInt(c.replace("EQP-", ""), 10)) + .sort((a: number, b: number) => b - a)[0] || 0; + setEqForm({ + equipment_code: `EQP-${String(maxNum + 1).padStart(3, "0")}`, + calibration_period: "12", + equipment_status: "NORMAL", + }); + setEqEditMode(false); + setEqModalOpen(true); + }; + const openEqEdit = (row: any) => { + setEqForm({ ...row }); + setEqEditMode(true); + setEqModalOpen(true); + }; const saveEquipment = async () => { - if (!eqForm.equipment_name) { toast.error("장비명은 필수 입력이에요"); return; } + if (!eqForm.equipment_code) { + toast.error("장비코드는 필수예요"); + return; + } + if (!eqForm.equipment_name) { + toast.error("장비명은 필수예요"); + return; + } + if (!eqForm.equipment_type) { + toast.error("장비유형은 필수예요"); + return; + } setEqSaving(true); try { if (eqEditMode) { await apiClient.put(`/table-management/tables/${EQUIPMENT_TABLE}/edit`, { - originalData: { id: eqForm.id }, updatedData: eqForm, + originalData: { id: eqForm.id }, + updatedData: eqForm, }); toast.success("검사장비를 수정했어요"); } else { @@ -274,21 +466,31 @@ export default function InspectionManagementPage() { } setEqModalOpen(false); fetchEquipments(); - } catch { toast.error("저장에 실패했어요"); } - finally { setEqSaving(false); } + } catch { + toast.error("저장에 실패했어요"); + } finally { + setEqSaving(false); + } }; const deleteEquipments = async () => { - if (eqChecked.length === 0) { toast.error("삭제할 항목을 선택해주세요"); return; } - const ok = await confirm("검사장비 삭제", { description: `선택한 ${eqChecked.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.` }); + if (eqChecked.length === 0) { + toast.error("삭제할 항목을 선택해주세요"); + return; + } + const ok = await confirm("검사장비 삭제", { + description: `선택한 ${eqChecked.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.`, + }); if (!ok) return; try { await apiClient.delete(`/table-management/tables/${EQUIPMENT_TABLE}/delete`, { - data: eqChecked.map(id => ({ id })), + data: eqChecked.map((id) => ({ id })), }); toast.success(`${eqChecked.length}건을 삭제했어요`); setEqChecked([]); fetchEquipments(); - } catch { toast.error("삭제에 실패했어요"); } + } catch { + toast.error("삭제에 실패했어요"); + } }; /* ═══════════════════ JSX ═══════════════════ */ @@ -296,39 +498,45 @@ export default function InspectionManagementPage() {
{ConfirmDialogComponent} -
+
- + - + 검사기준 - {inspCount} + + {inspCount} + - + 불량관리 - {defCount} + + {defCount} + - + 검사장비 - {eqCount} + + {eqCount} +
{/* ──── 검사기준 탭 ──── */} - +
- - - + + + @@ -352,186 +573,445 @@ export default function InspectionManagementPage() { } />
-
+
0 && inspChecked.length === inspections.length} - onCheckedChange={(v) => setInspChecked(v ? inspections.map(r => r.id) : [])} + onCheckedChange={(v) => setInspChecked(v ? inspections.map((r) => r.id) : [])} /> {ts.visibleColumns.map((col) => ( - {col.label} + + {col.label} + ))} {inspLoading ? ( - - ) : inspections.length === 0 ? ( -

등록된 검사기준이 없어요

- ) : inspections.map((row) => ( - setInspChecked(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id])} - onDoubleClick={() => openInspEdit(row)} - > - e.stopPropagation()}> - setInspChecked(prev => v ? [...prev, row.id] : prev.filter(id => id !== row.id))} /> + + + - {ts.visibleColumns.map((col) => { - if (col.key === "inspection_type") return {getCatLabel(INSPECTION_TABLE, "inspection_type", row.inspection_type)}; - if (col.key === "apply_type") return {getCatLabel(INSPECTION_TABLE, "apply_type", row.apply_type)}; - if (col.key === "is_active") return {row.is_active ? "사용" : "미사용"}; - return {row[col.key] ?? ""}; - })} - ))} + ) : inspections.length === 0 ? ( + + + +

등록된 검사기준이 없어요

+
+
+ ) : ( + inspections.map((row) => ( + + setInspChecked((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id], + ) + } + onDoubleClick={() => openInspEdit(row)} + > + e.stopPropagation()}> + + setInspChecked((prev) => (v ? [...prev, row.id] : prev.filter((id) => id !== row.id))) + } + /> + + {ts.visibleColumns.map((col) => { + if (col.key === "inspection_type") + return ( + + {getCatLabel(INSPECTION_TABLE, "inspection_type", row.inspection_type)} + + ); + if (col.key === "inspection_method") + return ( + + {getCatLabel(INSPECTION_TABLE, "inspection_method", row.inspection_method)} + + ); + if (col.key === "judgment_criteria") + return ( + + {getCatLabel(INSPECTION_TABLE, "judgment_criteria", row.judgment_criteria)} + + ); + if (col.key === "unit") + return ( + {getCatLabel(INSPECTION_TABLE, "unit", row.unit)} + ); + if (col.key === "apply_type") + return ( + + {getCatLabel(INSPECTION_TABLE, "apply_type", row.apply_type)} + + ); + return {row[col.key] ?? ""}; + })} + + )) + )}
{/* ──── 불량관리 탭 ──── */} - -
+ +
- + setDefKeyword(e.target.value)} />
- {filteredDefects.length}건 + + {filteredDefects.length}건 +
- - - + + +
-
+
0 && defChecked.length === filteredDefects.length} - onCheckedChange={(v) => setDefChecked(v ? filteredDefects.map(r => r.id) : [])} + onCheckedChange={(v) => setDefChecked(v ? filteredDefects.map((r) => r.id) : [])} /> - 불량유형 - 불량명 - 심각도 - 사용여부 + + 불량코드 + + + 불량유형 + + + 불량명 + + + 불량내용 + + + 심각도 + + + 검사유형 + + + 적용대상 + + + 사용여부 + + + 등록일 + + + 관리자 + + + 비고 + {defLoading ? ( - - ) : filteredDefects.length === 0 ? ( -

등록된 불량유형이 없어요

- ) : filteredDefects.map((row) => ( - setDefChecked(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id])} - onDoubleClick={() => openDefEdit(row)} - > - e.stopPropagation()}> - setDefChecked(prev => v ? [...prev, row.id] : prev.filter(id => id !== row.id))} /> - - {getCatLabel(DEFECT_TABLE, "defect_type", row.defect_type)} - {row.defect_name} - - {row.severity} - - - {row.is_active ? "사용" : "미사용"} + + + - ))} + ) : filteredDefects.length === 0 ? ( + + + +

등록된 불량유형이 없어요

+
+
+ ) : ( + filteredDefects.map((row) => { + const severityLabel = getCatLabel(DEFECT_TABLE, "severity", row.severity); + const severityColor = + severityLabel === "치명적" + ? "destructive" + : severityLabel === "심각" + ? "destructive" + : severityLabel === "보통" + ? "secondary" + : "outline"; + return ( + + setDefChecked((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id], + ) + } + onDoubleClick={() => openDefEdit(row)} + > + e.stopPropagation()}> + + setDefChecked((prev) => (v ? [...prev, row.id] : prev.filter((id) => id !== row.id))) + } + /> + + {row.defect_code || "-"} + + + {getCatLabel(DEFECT_TABLE, "defect_type", row.defect_type)} + + + {row.defect_name || "-"} + + {row.defect_content || "-"} + + + + {severityLabel || "-"} + + + +
+ {row.inspection_type + ? row.inspection_type + .split(",") + .filter(Boolean) + .map((c: string) => ( + + {getCatLabel(DEFECT_TABLE, "inspection_type", c)} + + )) + : "-"} +
+
+ +
+ {row.apply_target + ? row.apply_target + .split(",") + .filter(Boolean) + .map((t: string) => ( + + {t} + + )) + : "-"} +
+
+ + + {getCatLabel(DEFECT_TABLE, "is_active", row.is_active) || "-"} + + + + {row.reg_date || (row.created_date ? row.created_date.slice(0, 10) : "-")} + + {row.manager_id || "-"} + {row.remarks || "-"} +
+ ); + }) + )}
{/* ──── 검사장비 탭 ──── */} - -
+ +
- + setEqKeyword(e.target.value)} />
- {filteredEquipments.length}건 + + {filteredEquipments.length}건 +
- - - + + +
-
+
0 && eqChecked.length === filteredEquipments.length} - onCheckedChange={(v) => setEqChecked(v ? filteredEquipments.map(r => r.id) : [])} + onCheckedChange={(v) => setEqChecked(v ? filteredEquipments.map((r) => r.id) : [])} /> - 장비명 - 모델명 - 제조사 - 교정주기 - 최종교정일 - 장비상태 + + 장비코드 + + + 장비명 + + + 장비유형 + + + 모델명 + + + 제조사 + + + 설치장소 + + + 최근교정일 + + + 교정주기(개월) + + + 장비상태 + + + 담당자 + {eqLoading ? ( - - ) : filteredEquipments.length === 0 ? ( -

등록된 검사장비가 없어요

- ) : filteredEquipments.map((row) => ( - setEqChecked(prev => prev.includes(row.id) ? prev.filter(id => id !== row.id) : [...prev, row.id])} - onDoubleClick={() => openEqEdit(row)} - > - e.stopPropagation()}> - setEqChecked(prev => v ? [...prev, row.id] : prev.filter(id => id !== row.id))} /> + + + - {row.equipment_name} - {row.model_name} - {row.manufacturer} - {row.calibration_cycle} - {row.last_calibration_date} - {getCatLabel(EQUIPMENT_TABLE, "equipment_status", row.equipment_status)} - ))} + ) : filteredEquipments.length === 0 ? ( + + + +

등록된 검사장비가 없어요

+
+
+ ) : ( + filteredEquipments.map((row) => { + const statusLabel = getCatLabel(EQUIPMENT_TABLE, "equipment_status", row.equipment_status); + const statusColor = + statusLabel === "정상" ? "default" : statusLabel === "폐기" ? "destructive" : "secondary"; + return ( + + setEqChecked((prev) => + prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id], + ) + } + onDoubleClick={() => openEqEdit(row)} + > + e.stopPropagation()}> + + setEqChecked((prev) => (v ? [...prev, row.id] : prev.filter((id) => id !== row.id))) + } + /> + + {row.equipment_code || "-"} + {row.equipment_name || "-"} + + + {getCatLabel(EQUIPMENT_TABLE, "equipment_type", row.equipment_type) || "-"} + + + {row.model_name || "-"} + {row.manufacturer || "-"} + {row.installation_location || "-"} + {row.last_calibration_date || "-"} + {row.calibration_period ? `${row.calibration_period}개월` : "-"} + + + {statusLabel || "-"} + + + + {userOptions.find((u) => u.code === row.manager_id)?.label || row.manager_id || "-"} + + + ); + }) + )}
@@ -541,62 +1021,205 @@ export default function InspectionManagementPage() { {/* ═══════════════════ 검사기준 모달 ═══════════════════ */} - + {inspEditMode ? "검사기준 수정" : "검사기준 등록"} 검사기준 정보를 입력해주세요 -
+
+ {/* 검사코드 */}
- - + + setInspForm((p) => ({ ...p, inspection_code: e.target.value }))} + placeholder="검사코드 입력" + />
+ {/* 유형 (다중선택) */}
- - -
-
- - setInspForm(p => ({ ...p, inspection_standard: e.target.value }))} placeholder="검사기준을 입력해주세요" /> -
-
- - setInspForm(p => ({ ...p, inspection_item_name: e.target.value }))} placeholder="검사항목명을 입력해주세요" /> -
-
- - setInspForm(p => ({ ...p, inspection_method: e.target.value }))} placeholder="검사방법" /> -
-
- - setInspForm(p => ({ ...p, unit: e.target.value }))} placeholder="단위" /> -
-
-
- setInspForm(p => ({ ...p, is_active: !!v }))} /> - + +
+ {(catOptions[`${INSPECTION_TABLE}.inspection_type`] || []).map((o) => { + const types: string[] = inspForm.inspection_type + ? inspForm.inspection_type.split(",").filter(Boolean) + : []; + const checked = types.includes(o.code); + return ( +
+ { + const next = v ? [...types, o.code] : types.filter((t) => t !== o.code); + setInspForm((p) => ({ ...p, inspection_type: next.join(",") })); + }} + /> + +
+ ); + })}
+ {/* 검사기준 */} +
+ + setInspForm((p) => ({ ...p, inspection_criteria: e.target.value }))} + placeholder="검사기준 입력" + /> +
+ {/* 기준상세 */} +
+ + setInspForm((p) => ({ ...p, criteria_detail: e.target.value }))} + placeholder="기준상세 입력" + /> +
+ {/* 검사항목 */} +
+ + setInspForm((p) => ({ ...p, inspection_item: e.target.value }))} + placeholder="검사항목 입력" + /> +
+ {/* 검사방법 */} +
+ + +
+ {/* 판단기준 */} +
+ + +
+ {/* 단위 */} +
+ + +
+ {/* 적용구분 */} +
+ + +
+ {/* 관리자 */} +
+ + +
+ {/* 비고 */} +
+ +