From f9f5a7e7e56a8b518150ad4bdfbf78278cd492ea Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 8 Apr 2026 09:46:34 +0900 Subject: [PATCH] feat: Add inspection management page for COMPANY_16 and COMPANY_29 - Introduced a new inspection management page for both COMPANY_16 and COMPANY_29, featuring a comprehensive table for managing inspection standards. - Implemented dynamic category loading and user options for enhanced functionality. - Integrated various UI components such as buttons, inputs, and dialogs to facilitate user interactions. - Established state management for inspections, defects, and equipment, ensuring a smooth user experience. These additions aim to improve the quality management processes within the application, providing users with the necessary tools to manage inspections effectively. --- .../COMPANY_16/quality/inspection/page.tsx | 1703 +++++++++++++++++ .../COMPANY_29/quality/inspection/page.tsx | 1703 +++++++++++++++++ .../components/layout/AdminPageRenderer.tsx | 16 + 3 files changed, 3422 insertions(+) create mode 100644 frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx create mode 100644 frontend/app/(main)/COMPANY_29/quality/inspection/page.tsx diff --git a/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx b/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx new file mode 100644 index 00000000..cacd9d02 --- /dev/null +++ b/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx @@ -0,0 +1,1703 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { + Plus, + Trash2, + Save, + Loader2, + Pencil, + ClipboardCheck, + AlertTriangle, + Wrench, + Search, + Inbox, + Settings2, +} from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; +import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; +import { useConfirmDialog } from "@/components/common/ConfirmDialog"; +import { useTableSettings } from "@/hooks/useTableSettings"; +import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; + +/* ───── 테이블명 ───── */ +const INSPECTION_TABLE = "inspection_standard"; + +const INSPECTION_COLUMNS = [ + { key: "inspection_code", label: "검사코드" }, + { key: "inspection_type", label: "검사유형" }, + { key: "inspection_criteria", label: "검사기준" }, + { key: "inspection_item", label: "검사항목" }, + { key: "inspection_method", label: "검사방법" }, + { key: "judgment_criteria", label: "판단기준" }, + { key: "unit", label: "단위" }, + { key: "apply_type", label: "적용구분" }, + { key: "manager", label: "관리자" }, +]; +const DEFECT_TABLE = "defect_standard_mng"; +const EQUIPMENT_TABLE = "inspection_equipment_mng"; + +/* ───── 카테고리 flatten ───── */ +const flattenCategories = (vals: any[]): { code: string; label: string }[] => { + const result: { code: string; label: string }[] = []; + for (const v of vals) { + result.push({ code: v.valueCode, label: v.valueLabel }); + if (v.children?.length) result.push(...flattenCategories(v.children)); + } + return result; +}; + +export default function InspectionManagementPage() { + const { user } = useAuth(); + const { confirm, ConfirmDialogComponent } = useConfirmDialog(); + const ts = useTableSettings("c16-inspection", INSPECTION_TABLE, INSPECTION_COLUMNS); + + const [activeTab, setActiveTab] = useState("inspection"); + + /* ───── 검사기준 ───── */ + const [inspections, setInspections] = useState([]); + const [inspLoading, setInspLoading] = useState(false); + const [inspCount, setInspCount] = useState(0); + const [inspChecked, setInspChecked] = useState([]); + const [inspModalOpen, setInspModalOpen] = useState(false); + const [inspEditMode, setInspEditMode] = useState(false); + const [inspForm, setInspForm] = useState>({}); + const [inspSaving, setInspSaving] = useState(false); + const [searchFilters, setSearchFilters] = useState([]); + + /* ───── 불량관리 ───── */ + const [defects, setDefects] = useState([]); + const [defLoading, setDefLoading] = useState(false); + const [defCount, setDefCount] = useState(0); + const [defChecked, setDefChecked] = useState([]); + const [defModalOpen, setDefModalOpen] = useState(false); + const [defEditMode, setDefEditMode] = useState(false); + const [defForm, setDefForm] = useState>({}); + const [defSaving, setDefSaving] = useState(false); + const [defKeyword, setDefKeyword] = useState(""); + + /* ───── 검사장비 ───── */ + const [equipments, setEquipments] = useState([]); + const [eqLoading, setEqLoading] = useState(false); + const [eqCount, setEqCount] = useState(0); + const [eqChecked, setEqChecked] = useState([]); + const [eqModalOpen, setEqModalOpen] = useState(false); + const [eqEditMode, setEqEditMode] = useState(false); + const [eqForm, setEqForm] = useState>({}); + const [eqSaving, setEqSaving] = useState(false); + const [eqKeyword, setEqKeyword] = useState(""); + + /* ───── 채번 ───── */ + const [numberingRuleId, setNumberingRuleId] = useState(null); + const [previewCode, setPreviewCode] = useState(null); + + /* ───── 카테고리 옵션 ───── */ + const [catOptions, setCatOptions] = useState>({}); + const [userOptions, setUserOptions] = useState<{ code: string; label: string }[]>([]); + + /* ═══════════════════ 카테고리 로드 ═══════════════════ */ + useEffect(() => { + const load = async () => { + const optMap: Record = {}; + 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( + catList.map(async ({ table, col }) => { + try { + const res = await apiClient.get(`/table-categories/${table}/${col}/values`); + if (res.data?.data?.length > 0) { + optMap[`${table}.${col}`] = flattenCategories(res.data.data); + } + } 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; + // 쉼표 구분 다중 코드 지원 + 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; + }; + + const inspTableColumns = useMemo(() => { + return ts.visibleColumns.map((col) => { + const base: EDataTableColumn = { key: col.key, label: col.label }; + if (["inspection_type", "inspection_method", "judgment_criteria", "unit", "apply_type"].includes(col.key)) { + base.render = (v: any, row: any) => getCatLabel(INSPECTION_TABLE, col.key, row[col.key]); + } + return base; + }); + }, [ts.visibleColumns, catOptions]); // eslint-disable-line react-hooks/exhaustive-deps + + /* ═══════════════════ 데이터 조회 ═══════════════════ */ + // 다중값 컬럼 (쉼표 구분 저장) — 서버 equals 대신 contains 사용 + const MULTI_VALUE_COLUMNS = ["inspection_type"]; + + const fetchInspections = useCallback(async () => { + setInspLoading(true); + try { + 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, + dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, + autoFilter: true, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setInspections(rows); + setInspCount(rows.length); + } catch { + toast.error("검사기준 조회에 실패했어요"); + } finally { + setInspLoading(false); + } + }, [searchFilters]); + + const fetchDefects = useCallback(async () => { + setDefLoading(true); + try { + const res = await apiClient.post(`/table-management/tables/${DEFECT_TABLE}/data`, { + page: 1, + size: 500, + autoFilter: true, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setDefects(rows); + setDefCount(rows.length); + } catch { + toast.error("불량관리 조회에 실패했어요"); + } finally { + setDefLoading(false); + } + }, []); + + const fetchEquipments = useCallback(async () => { + setEqLoading(true); + try { + const res = await apiClient.post(`/table-management/tables/${EQUIPMENT_TABLE}/data`, { + page: 1, + size: 500, + autoFilter: true, + }); + const rows = res.data?.data?.data || res.data?.data?.rows || []; + setEquipments(rows); + setEqCount(rows.length); + } catch { + toast.error("검사장비 조회에 실패했어요"); + } finally { + setEqLoading(false); + } + }, []); + + 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; + + const filteredEquipments = eqKeyword.trim() + ? equipments.filter( + (r) => + (r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) || + (r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()), + ) + : equipments; + + /* ═══════════════════ 검사기준 CRUD ═══════════════════ */ + const openInspCreate = async () => { + setInspForm({}); + setInspEditMode(false); + setNumberingRuleId(null); + setPreviewCode(null); + setInspModalOpen(true); + try { + const ruleRes = await apiClient.get(`/numbering-rules/by-column/${INSPECTION_TABLE}/inspection_code`); + const ruleData = ruleRes.data; + if (ruleData?.success && ruleData?.data?.ruleId) { + const ruleId = ruleData.data.ruleId; + setNumberingRuleId(ruleId); + const prev = await previewNumberingCode(ruleId); + if (prev.success && prev.data?.generatedCode) { + setPreviewCode(prev.data.generatedCode); + } + } + } catch { /* 채번 규칙 없으면 무시 */ } + }; + const openInspEdit = (row: any) => { + setInspForm({ ...row }); + setInspEditMode(true); + setInspModalOpen(true); + }; + const saveInspection = async () => { + if (!numberingRuleId && !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 { + let finalCode = inspForm.inspection_code || ""; + if (!inspEditMode && numberingRuleId) { + const allocRes = await allocateNumberingCode(numberingRuleId); + if (allocRes.success && allocRes.data?.generatedCode) { + finalCode = allocRes.data.generatedCode; + } else { + toast.error("채번 코드 할당에 실패했습니다."); + setInspSaving(false); + return; + } + } + if (inspEditMode) { + await apiClient.put(`/table-management/tables/${INSPECTION_TABLE}/edit`, { + originalData: { id: inspForm.id }, + updatedData: inspForm, + }); + toast.success("검사기준을 수정했어요"); + } else { + await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/add`, { + id: crypto.randomUUID(), + ...inspForm, + inspection_code: finalCode, + }); + toast.success("검사기준을 등록했어요"); + } + setInspModalOpen(false); + fetchInspections(); + } catch { + toast.error("저장에 실패했어요"); + } finally { + setInspSaving(false); + } + }; + const deleteInspections = async () => { + 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 })), + }); + toast.success(`${inspChecked.length}건을 삭제했어요`); + setInspChecked([]); + fetchInspections(); + } catch { + toast.error("삭제에 실패했어요"); + } + }; + + /* ═══════════════════ 불량관리 CRUD ═══════════════════ */ + const openDefCreate = async () => { + setDefForm({}); + setDefEditMode(false); + setNumberingRuleId(null); + setPreviewCode(null); + setDefModalOpen(true); + try { + const ruleRes = await apiClient.get(`/numbering-rules/by-column/${DEFECT_TABLE}/defect_code`); + const ruleData = ruleRes.data; + if (ruleData?.success && ruleData?.data?.ruleId) { + const ruleId = ruleData.data.ruleId; + setNumberingRuleId(ruleId); + const prev = await previewNumberingCode(ruleId); + if (prev.success && prev.data?.generatedCode) { + setPreviewCode(prev.data.generatedCode); + } + } + } catch { /* 채번 규칙 없으면 무시 */ } + }; + const openDefEdit = (row: any) => { + setDefForm({ ...row }); + setDefEditMode(true); + setDefModalOpen(true); + }; + const saveDefect = async () => { + if (!numberingRuleId && !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 { + let finalCode = defForm.defect_code || ""; + if (!defEditMode && numberingRuleId) { + const allocRes = await allocateNumberingCode(numberingRuleId); + if (allocRes.success && allocRes.data?.generatedCode) { + finalCode = allocRes.data.generatedCode; + } else { + toast.error("채번 코드 할당에 실패했습니다."); + setDefSaving(false); + return; + } + } + if (defEditMode) { + await apiClient.put(`/table-management/tables/${DEFECT_TABLE}/edit`, { + originalData: { id: defForm.id }, + updatedData: defForm, + }); + toast.success("불량유형을 수정했어요"); + } else { + await apiClient.post(`/table-management/tables/${DEFECT_TABLE}/add`, { id: crypto.randomUUID(), ...defForm, defect_code: finalCode }); + toast.success("불량유형을 등록했어요"); + } + setDefModalOpen(false); + fetchDefects(); + } catch { + toast.error("저장에 실패했어요"); + } finally { + setDefSaving(false); + } + }; + const deleteDefects = async () => { + 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 })), + }); + toast.success(`${defChecked.length}건을 삭제했어요`); + setDefChecked([]); + fetchDefects(); + } catch { + toast.error("삭제에 실패했어요"); + } + }; + + /* ═══════════════════ 검사장비 CRUD ═══════════════════ */ + const openEqCreate = async () => { + setEqForm({ + calibration_period: "12", + equipment_status: "NORMAL", + }); + setEqEditMode(false); + setNumberingRuleId(null); + setPreviewCode(null); + setEqModalOpen(true); + try { + const ruleRes = await apiClient.get(`/numbering-rules/by-column/${EQUIPMENT_TABLE}/equipment_code`); + const ruleData = ruleRes.data; + if (ruleData?.success && ruleData?.data?.ruleId) { + const ruleId = ruleData.data.ruleId; + setNumberingRuleId(ruleId); + const prev = await previewNumberingCode(ruleId); + if (prev.success && prev.data?.generatedCode) { + setPreviewCode(prev.data.generatedCode); + } + } else { + // 채번 규칙 없으면 기존 수동 채번 fallback + 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((p) => ({ ...p, equipment_code: `EQP-${String(maxNum + 1).padStart(3, "0")}` })); + } + } catch { + // 채번 규칙 조회 실패 시 기존 수동 채번 fallback + 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((p) => ({ ...p, equipment_code: `EQP-${String(maxNum + 1).padStart(3, "0")}` })); + } + }; + const openEqEdit = (row: any) => { + setEqForm({ ...row }); + setEqEditMode(true); + setEqModalOpen(true); + }; + const saveEquipment = async () => { + if (!numberingRuleId && !eqForm.equipment_code) { + toast.error("장비코드는 필수예요"); + return; + } + if (!eqForm.equipment_name) { + toast.error("장비명은 필수예요"); + return; + } + if (!eqForm.equipment_type) { + toast.error("장비유형은 필수예요"); + return; + } + setEqSaving(true); + try { + let finalCode = eqForm.equipment_code || ""; + if (!eqEditMode && numberingRuleId) { + const allocRes = await allocateNumberingCode(numberingRuleId); + if (allocRes.success && allocRes.data?.generatedCode) { + finalCode = allocRes.data.generatedCode; + } else { + toast.error("채번 코드 할당에 실패했습니다."); + setEqSaving(false); + return; + } + } + if (eqEditMode) { + await apiClient.put(`/table-management/tables/${EQUIPMENT_TABLE}/edit`, { + originalData: { id: eqForm.id }, + updatedData: eqForm, + }); + toast.success("검사장비를 수정했어요"); + } else { + await apiClient.post(`/table-management/tables/${EQUIPMENT_TABLE}/add`, { id: crypto.randomUUID(), ...eqForm, equipment_code: finalCode }); + toast.success("검사장비를 등록했어요"); + } + setEqModalOpen(false); + fetchEquipments(); + } catch { + toast.error("저장에 실패했어요"); + } finally { + setEqSaving(false); + } + }; + const deleteEquipments = async () => { + 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 })), + }); + toast.success(`${eqChecked.length}건을 삭제했어요`); + setEqChecked([]); + fetchEquipments(); + } catch { + toast.error("삭제에 실패했어요"); + } + }; + + /* ═══════════════════ JSX ═══════════════════ */ + return ( +
+ {ConfirmDialogComponent} + +
+ +
+ + + + 검사기준 + + {inspCount} + + + + + 불량관리 + + {defCount} + + + + + 검사장비 + + {eqCount} + + + +
+ + {/* ──── 검사기준 탭 ──── */} + +
+ + + + + +
+ } + /> +
+
+ openInspEdit(row)} + showPagination={true} + draggableColumns={false} + columnOrderKey="c16-inspection-main" + /> +
+ + + {/* ──── 불량관리 탭 ──── */} + +
+
+
+ + setDefKeyword(e.target.value)} + /> +
+ + {filteredDefects.length}건 + +
+
+ + + +
+
+
+ + + + + 0 && defChecked.length === filteredDefects.length} + onCheckedChange={(v) => setDefChecked(v ? filteredDefects.map((r) => r.id) : [])} + /> + + + 불량코드 + + + 불량유형 + + + 불량명 + + + 불량내용 + + + 심각도 + + + 검사유형 + + + 적용대상 + + + 사용여부 + + + 등록일 + + + 관리자 + + + 비고 + + + + + {defLoading ? ( + + + + + + ) : 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}건 + +
+
+ + + +
+
+
+ + + + + 0 && eqChecked.length === filteredEquipments.length} + onCheckedChange={(v) => setEqChecked(v ? filteredEquipments.map((r) => r.id) : [])} + /> + + + 장비코드 + + + 장비명 + + + 장비유형 + + + 모델명 + + + 제조사 + + + 설치장소 + + + 최근교정일 + + + 교정주기(개월) + + + 장비상태 + + + 담당자 + + + + + {eqLoading ? ( + + + + + + ) : 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 || "-"} + + + ); + }) + )} +
+
+
+
+ +
+ + {/* ═══════════════════ 검사기준 모달 ═══════════════════ */} + + + + {inspEditMode ? "검사기준 수정" : "검사기준 등록"} + 검사기준 정보를 입력해주세요 + +
+ {/* 검사코드 */} +
+ + {!inspEditMode && numberingRuleId ? ( + + ) : inspEditMode ? ( + + ) : ( + setInspForm((p) => ({ ...p, inspection_code: e.target.value }))} + placeholder="검사코드 입력" + /> + )} +
+ {/* 유형 (다중선택) */} +
+ +
+ {(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="검사항목 입력" + /> +
+ {/* 검사방법 */} +
+ + +
+ {/* 판단기준 */} +
+ + +
+ {/* 단위 */} +
+ + +
+ {/* 적용구분 */} +
+ + +
+ {/* 관리자 */} +
+ + +
+ {/* 비고 */} +
+ +