diff --git a/frontend/app/(main)/COMPANY_10/equipment/inspection-record/page.tsx b/frontend/app/(main)/COMPANY_10/equipment/inspection-record/page.tsx new file mode 100644 index 00000000..b75f5f3d --- /dev/null +++ b/frontend/app/(main)/COMPANY_10/equipment/inspection-record/page.tsx @@ -0,0 +1,324 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { RefreshCw, Loader2, Inbox, Wrench, Download } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; + +// ─── 타입 ───────────────────────────────────────────────── + +interface InspectionRecord { + id: string; + equipment_code: string; + inspection_item_objid: string; + inspection_date: string; + status: string; + inspector: string; + remark: string; + created_date: string; +} + +interface InspectionItem { + id: string; + equipment_code: string; + inspection_item: string; + inspection_cycle: string; + inspection_content: string; + inspection_method: string; + lower_limit: string; + upper_limit: string; + unit: string; +} + +interface EquipmentInfo { + id: string; + equipment_code: string; + equipment_name: string; +} + +const RECORD_TABLE = "equipment_inspection_record"; + +// ─── 메인 컴포넌트 ─────────────────────────────────────── + +export default function EquipmentInspectionRecordPage() { + const [records, setRecords] = useState([]); + const [inspectionItems, setInspectionItems] = useState>(new Map()); + const [equipments, setEquipments] = useState>(new Map()); + const [loading, setLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [filterValues, setFilterValues] = useState([]); + + // ─── 데이터 조회 ──────────────────────────────────────── + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const search: Record = {}; + filterValues.forEach((f) => { + if (f.value) search[f.column] = f.value; + }); + + const [recordRes, itemRes, equipRes] = await Promise.all([ + apiClient.post(`/table-management/tables/${RECORD_TABLE}/data`, { + page: 1, + size: 1000, + autoFilter: true, + search, + }), + apiClient.post(`/table-management/tables/equipment_inspection_item/data`, { + page: 1, + size: 1000, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), + apiClient.post(`/table-management/tables/equipment_mng/data`, { + page: 1, + size: 500, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), + ]); + + const rRows: InspectionRecord[] = recordRes.data?.data?.data ?? recordRes.data?.data?.rows ?? []; + setRecords(rRows); + + const iRows: InspectionItem[] = itemRes.data?.data?.data ?? itemRes.data?.data?.rows ?? []; + const iMap = new Map(); + iRows.forEach((i) => iMap.set(i.id, i)); + setInspectionItems(iMap); + + const eRows: EquipmentInfo[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? []; + const eMap = new Map(); + eRows.forEach((e) => { + eMap.set(e.equipment_code, e); + eMap.set(e.id, e); + }); + setEquipments(eMap); + } catch (err) { + console.error("점검기록 조회 실패:", err); + } finally { + setLoading(false); + } + }, [filterValues]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + // ─── 선택된 레코드 ───────────────────────────────────── + + const selectedRecord = useMemo(() => records.find((r) => r.id === selectedId), [records, selectedId]); + const selectedItem = useMemo( + () => (selectedRecord ? inspectionItems.get(selectedRecord.inspection_item_objid) : undefined), + [selectedRecord, inspectionItems], + ); + + // ─── 설비명 조회 ─────────────────────────────────────── + + const getEquipName = (code: string) => equipments.get(code)?.equipment_name || code || "-"; + + // ─── 엑셀 다운로드 ───────────────────────────────────── + + const handleExcel = async () => { + const data = records.map((r) => ({ + 설비코드: r.equipment_code, + 설비명: getEquipName(r.equipment_code), + 점검항목: inspectionItems.get(r.inspection_item_objid)?.inspection_item || "-", + 점검일자: fmtDate(r.inspection_date), + 상태: r.status, + 점검자: r.inspector, + 비고: r.remark, + })); + await exportToExcel(data, "설비점검기록.xlsx", "점검기록"); + }; + + // ─── 날짜/상태 포맷 ──────────────────────────────────── + + const fmtDate = (d: string) => { + if (!d) return "-"; + try { + return new Date(d).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }); + } catch { + return d; + } + }; + + const statusBadge = (v: string) => { + if (!v) return -; + const lower = v.toLowerCase(); + if (["완료", "pass", "정상", "합격", "completed"].includes(lower)) + return {v}; + if (["이상", "fail", "불합격", "비정상"].includes(lower)) + return {v}; + if (["점검중", "진행", "in_progress"].includes(lower)) + return {v}; + return {v}; + }; + + // ─── 렌더링 ───────────────────────────────────────────── + + return ( +
+ {/* 검색 필터 */} +
+ setFilterValues(filters)} + dataCount={records.length} + /> +
+ + {/* 헤더 */} +
+
+ +

점검관리

+ {records.length}건 +
+
+ + +
+
+ + {/* 분할 패널 */} + + {/* 좌측: 점검기록 테이블 */} + +
+ {loading && records.length === 0 ? ( +
+ + 데이터를 불러오는 중... +
+ ) : records.length === 0 ? ( +
+ + 점검 기록이 없습니다 +
+ ) : ( + + + + # + 설비코드 + 설비명 + 점검항목 + 점검일자 + 상태 + 점검자 + 비고 + + + + {records.map((row, idx) => { + const item = inspectionItems.get(row.inspection_item_objid); + return ( + setSelectedId(row.id)} + > + {idx + 1} + {row.equipment_code || "-"} + {getEquipName(row.equipment_code)} + {item?.inspection_item || "-"} + {fmtDate(row.inspection_date)} + {statusBadge(row.status)} + {row.inspector || "-"} + {row.remark || "-"} + + ); + })} + +
+ )} +
+
+ + + + {/* 우측: 점검항목 상세 */} + + {!selectedId || !selectedRecord ? ( +
+ +

좌측에서 점검 기록을 선택해주세요

+
+ ) : ( +
+ {/* 점검 기록 요약 */} +
+

점검 기록 정보

+
+ + + + + + +
+
+ + {/* 점검 항목 상세 */} + {selectedItem ? ( +
+

점검 항목 상세

+
+ + + + + + + +
+
+ ) : ( +
+

점검 항목 정보가 없습니다

+
+ )} +
+ )} +
+
+
+ ); +} + +// ─── 상세 행 ────────────────────────────────────────────── + +function DetailRow({ + label, + value, + badge, + span2, +}: { + label: string; + value: string; + badge?: React.ReactNode; + span2?: boolean; +}) { + return ( +
+ {label} + {badge || {value}} +
+ ); +} diff --git a/frontend/app/(main)/COMPANY_10/quality/inspection-result/page.tsx b/frontend/app/(main)/COMPANY_10/quality/inspection-result/page.tsx new file mode 100644 index 00000000..d9e49d5f --- /dev/null +++ b/frontend/app/(main)/COMPANY_10/quality/inspection-result/page.tsx @@ -0,0 +1,351 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { RefreshCw, Loader2, Inbox, ClipboardCheck, Download } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; + +// ─── 타입 ───────────────────────────────────────────────── + +interface InspectionMng { + id: string; + inspection_number: string; + item_code: string; + item_name: string; + inspection_type: string; + total_qty: number; + good_qty: number; + bad_qty: number; + overall_judgment: string; + inspector: string; + inspection_date: string; + is_completed: string; + defect_description: string; + memo: string; + supplier_name: string; + supplier_code: string; + created_date: string; +} + +interface InspectionDetail { + id: string; + master_id: string; + inspection_item_name: string; + inspection_standard: string; + pass_criteria: string; + measured_value: string; + judgment: string; + is_required: string; + memo: string; + item_code: string; + item_name: string; + inspection_type: string; +} + +const MASTER_TABLE = "inspection_result_mng"; +const DETAIL_TABLE = "inspection_result"; + +// ─── 메인 컴포넌트 ─────────────────────────────────────── + +export default function InspectionResultPage() { + const [masterData, setMasterData] = useState([]); + const [detailData, setDetailData] = useState([]); + const [loading, setLoading] = useState(false); + const [detailLoading, setDetailLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [filterValues, setFilterValues] = useState([]); + + // ─── 마스터 데이터 조회 ───────────────────────────────── + + const fetchMaster = useCallback(async () => { + setLoading(true); + try { + const search: Record = {}; + filterValues.forEach((f) => { + if (f.value) search[f.column] = f.value; + }); + + const res = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, { + page: 1, + size: 1000, + autoFilter: true, + search, + }); + const rows: InspectionMng[] = res.data?.data?.data ?? res.data?.data?.rows ?? []; + setMasterData(rows); + } catch (err) { + console.error("검사결과 조회 실패:", err); + } finally { + setLoading(false); + } + }, [filterValues]); + + useEffect(() => { + fetchMaster(); + }, [fetchMaster]); + + // ─── 디테일 데이터 조회 ───────────────────────────────── + + const fetchDetail = useCallback(async (masterId: string) => { + setDetailLoading(true); + try { + const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { + page: 1, + size: 500, + autoFilter: true, + search: { master_id: masterId }, + }); + const rows: InspectionDetail[] = res.data?.data?.data ?? res.data?.data?.rows ?? []; + setDetailData(rows); + } catch (err) { + console.error("검사상세 조회 실패:", err); + setDetailData([]); + } finally { + setDetailLoading(false); + } + }, []); + + useEffect(() => { + if (selectedId) fetchDetail(selectedId); + else setDetailData([]); + }, [selectedId, fetchDetail]); + + // ─── 선택된 마스터 ────────────────────────────────────── + + const selectedMaster = useMemo( + () => masterData.find((m) => m.id === selectedId), + [masterData, selectedId], + ); + + // ─── 엑셀 다운로드 ───────────────────────────────────── + + const handleExcel = async () => { + await exportToExcel(masterData, "검사결과관리.xlsx", "검사결과"); + }; + + // ─── 판정 배지 ────────────────────────────────────────── + + const judgmentBadge = (v: string) => { + if (!v) return null; + const lower = v.toLowerCase(); + if (["합격", "pass", "적합"].includes(lower)) + return {v}; + if (["불합격", "fail", "부적합"].includes(lower)) + return {v}; + return {v}; + }; + + // ─── 날짜 포맷 ────────────────────────────────────────── + + const fmtDate = (d: string) => { + if (!d) return "-"; + try { + return new Date(d).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }); + } catch { + return d; + } + }; + + // ─── 렌더링 ───────────────────────────────────────────── + + return ( +
+ {/* 검색 필터 */} +
+ { + setFilterValues(filters); + }} + dataCount={masterData.length} + /> +
+ + {/* 헤더 */} +
+
+ +

검사관리

+ {masterData.length}건 +
+
+ + +
+
+ + {/* 분할 패널 */} + + {/* 좌측: 마스터 테이블 */} + +
+ {loading && masterData.length === 0 ? ( +
+ + 데이터를 불러오는 중... +
+ ) : masterData.length === 0 ? ( +
+ + 검사 데이터가 없습니다 +
+ ) : ( + + + + # + 검사번호 + 검사유형 + 품목코드 + 품목명 + 검사수량 + 양품 + 불량 + 판정 + 검사자 + 검사일자 + 완료 + 거래처 + + + + {masterData.map((row, idx) => ( + setSelectedId(row.id)} + > + {idx + 1} + {row.inspection_number || "-"} + + {row.inspection_type || "-"} + + {row.item_code || "-"} + {row.item_name || "-"} + {row.total_qty ?? "-"} + {row.good_qty ?? "-"} + {row.bad_qty ?? "-"} + {judgmentBadge(row.overall_judgment)} + {row.inspector || "-"} + {fmtDate(row.inspection_date)} + + {row.is_completed === "Y" ? ( + 완료 + ) : ( + 진행중 + )} + + {row.supplier_name || "-"} + + ))} + +
+ )} +
+
+ + + + {/* 우측: 디테일 */} + + {!selectedId ? ( +
+ +

좌측에서 검사를 선택해주세요

+
+ ) : ( +
+ {/* 상세 헤더 */} +
+
+

검사 상세

+ {selectedMaster && ( + + {selectedMaster.inspection_number} + + )} +
+ {selectedMaster && ( +
+ {selectedMaster.item_name} + · + {judgmentBadge(selectedMaster.overall_judgment)} +
+ )} +
+ + {/* 상세 테이블 */} +
+ {detailLoading ? ( +
+ +
+ ) : detailData.length === 0 ? ( +
+ +

검사 항목이 없습니다

+
+ ) : ( + + + + # + 검사항목 + 검사기준 + 합격기준 + 측정값 + 판정 + 필수 + 비고 + + + + {detailData.map((d, idx) => ( + + {idx + 1} + {d.inspection_item_name || "-"} + {d.inspection_standard || "-"} + {d.pass_criteria || "-"} + {d.measured_value || "-"} + {judgmentBadge(d.judgment)} + + {d.is_required === "Y" ? ( + 필수 + ) : ( + - + )} + + {d.memo || "-"} + + ))} + +
+ )} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/app/(main)/COMPANY_16/equipment/inspection-record/page.tsx b/frontend/app/(main)/COMPANY_16/equipment/inspection-record/page.tsx new file mode 100644 index 00000000..b75f5f3d --- /dev/null +++ b/frontend/app/(main)/COMPANY_16/equipment/inspection-record/page.tsx @@ -0,0 +1,324 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { RefreshCw, Loader2, Inbox, Wrench, Download } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; + +// ─── 타입 ───────────────────────────────────────────────── + +interface InspectionRecord { + id: string; + equipment_code: string; + inspection_item_objid: string; + inspection_date: string; + status: string; + inspector: string; + remark: string; + created_date: string; +} + +interface InspectionItem { + id: string; + equipment_code: string; + inspection_item: string; + inspection_cycle: string; + inspection_content: string; + inspection_method: string; + lower_limit: string; + upper_limit: string; + unit: string; +} + +interface EquipmentInfo { + id: string; + equipment_code: string; + equipment_name: string; +} + +const RECORD_TABLE = "equipment_inspection_record"; + +// ─── 메인 컴포넌트 ─────────────────────────────────────── + +export default function EquipmentInspectionRecordPage() { + const [records, setRecords] = useState([]); + const [inspectionItems, setInspectionItems] = useState>(new Map()); + const [equipments, setEquipments] = useState>(new Map()); + const [loading, setLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [filterValues, setFilterValues] = useState([]); + + // ─── 데이터 조회 ──────────────────────────────────────── + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const search: Record = {}; + filterValues.forEach((f) => { + if (f.value) search[f.column] = f.value; + }); + + const [recordRes, itemRes, equipRes] = await Promise.all([ + apiClient.post(`/table-management/tables/${RECORD_TABLE}/data`, { + page: 1, + size: 1000, + autoFilter: true, + search, + }), + apiClient.post(`/table-management/tables/equipment_inspection_item/data`, { + page: 1, + size: 1000, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), + apiClient.post(`/table-management/tables/equipment_mng/data`, { + page: 1, + size: 500, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), + ]); + + const rRows: InspectionRecord[] = recordRes.data?.data?.data ?? recordRes.data?.data?.rows ?? []; + setRecords(rRows); + + const iRows: InspectionItem[] = itemRes.data?.data?.data ?? itemRes.data?.data?.rows ?? []; + const iMap = new Map(); + iRows.forEach((i) => iMap.set(i.id, i)); + setInspectionItems(iMap); + + const eRows: EquipmentInfo[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? []; + const eMap = new Map(); + eRows.forEach((e) => { + eMap.set(e.equipment_code, e); + eMap.set(e.id, e); + }); + setEquipments(eMap); + } catch (err) { + console.error("점검기록 조회 실패:", err); + } finally { + setLoading(false); + } + }, [filterValues]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + // ─── 선택된 레코드 ───────────────────────────────────── + + const selectedRecord = useMemo(() => records.find((r) => r.id === selectedId), [records, selectedId]); + const selectedItem = useMemo( + () => (selectedRecord ? inspectionItems.get(selectedRecord.inspection_item_objid) : undefined), + [selectedRecord, inspectionItems], + ); + + // ─── 설비명 조회 ─────────────────────────────────────── + + const getEquipName = (code: string) => equipments.get(code)?.equipment_name || code || "-"; + + // ─── 엑셀 다운로드 ───────────────────────────────────── + + const handleExcel = async () => { + const data = records.map((r) => ({ + 설비코드: r.equipment_code, + 설비명: getEquipName(r.equipment_code), + 점검항목: inspectionItems.get(r.inspection_item_objid)?.inspection_item || "-", + 점검일자: fmtDate(r.inspection_date), + 상태: r.status, + 점검자: r.inspector, + 비고: r.remark, + })); + await exportToExcel(data, "설비점검기록.xlsx", "점검기록"); + }; + + // ─── 날짜/상태 포맷 ──────────────────────────────────── + + const fmtDate = (d: string) => { + if (!d) return "-"; + try { + return new Date(d).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }); + } catch { + return d; + } + }; + + const statusBadge = (v: string) => { + if (!v) return -; + const lower = v.toLowerCase(); + if (["완료", "pass", "정상", "합격", "completed"].includes(lower)) + return {v}; + if (["이상", "fail", "불합격", "비정상"].includes(lower)) + return {v}; + if (["점검중", "진행", "in_progress"].includes(lower)) + return {v}; + return {v}; + }; + + // ─── 렌더링 ───────────────────────────────────────────── + + return ( +
+ {/* 검색 필터 */} +
+ setFilterValues(filters)} + dataCount={records.length} + /> +
+ + {/* 헤더 */} +
+
+ +

점검관리

+ {records.length}건 +
+
+ + +
+
+ + {/* 분할 패널 */} + + {/* 좌측: 점검기록 테이블 */} + +
+ {loading && records.length === 0 ? ( +
+ + 데이터를 불러오는 중... +
+ ) : records.length === 0 ? ( +
+ + 점검 기록이 없습니다 +
+ ) : ( + + + + # + 설비코드 + 설비명 + 점검항목 + 점검일자 + 상태 + 점검자 + 비고 + + + + {records.map((row, idx) => { + const item = inspectionItems.get(row.inspection_item_objid); + return ( + setSelectedId(row.id)} + > + {idx + 1} + {row.equipment_code || "-"} + {getEquipName(row.equipment_code)} + {item?.inspection_item || "-"} + {fmtDate(row.inspection_date)} + {statusBadge(row.status)} + {row.inspector || "-"} + {row.remark || "-"} + + ); + })} + +
+ )} +
+
+ + + + {/* 우측: 점검항목 상세 */} + + {!selectedId || !selectedRecord ? ( +
+ +

좌측에서 점검 기록을 선택해주세요

+
+ ) : ( +
+ {/* 점검 기록 요약 */} +
+

점검 기록 정보

+
+ + + + + + +
+
+ + {/* 점검 항목 상세 */} + {selectedItem ? ( +
+

점검 항목 상세

+
+ + + + + + + +
+
+ ) : ( +
+

점검 항목 정보가 없습니다

+
+ )} +
+ )} +
+
+
+ ); +} + +// ─── 상세 행 ────────────────────────────────────────────── + +function DetailRow({ + label, + value, + badge, + span2, +}: { + label: string; + value: string; + badge?: React.ReactNode; + span2?: boolean; +}) { + return ( +
+ {label} + {badge || {value}} +
+ ); +} diff --git a/frontend/app/(main)/COMPANY_16/quality/inspection-result/page.tsx b/frontend/app/(main)/COMPANY_16/quality/inspection-result/page.tsx new file mode 100644 index 00000000..d9e49d5f --- /dev/null +++ b/frontend/app/(main)/COMPANY_16/quality/inspection-result/page.tsx @@ -0,0 +1,351 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { RefreshCw, Loader2, Inbox, ClipboardCheck, Download } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; + +// ─── 타입 ───────────────────────────────────────────────── + +interface InspectionMng { + id: string; + inspection_number: string; + item_code: string; + item_name: string; + inspection_type: string; + total_qty: number; + good_qty: number; + bad_qty: number; + overall_judgment: string; + inspector: string; + inspection_date: string; + is_completed: string; + defect_description: string; + memo: string; + supplier_name: string; + supplier_code: string; + created_date: string; +} + +interface InspectionDetail { + id: string; + master_id: string; + inspection_item_name: string; + inspection_standard: string; + pass_criteria: string; + measured_value: string; + judgment: string; + is_required: string; + memo: string; + item_code: string; + item_name: string; + inspection_type: string; +} + +const MASTER_TABLE = "inspection_result_mng"; +const DETAIL_TABLE = "inspection_result"; + +// ─── 메인 컴포넌트 ─────────────────────────────────────── + +export default function InspectionResultPage() { + const [masterData, setMasterData] = useState([]); + const [detailData, setDetailData] = useState([]); + const [loading, setLoading] = useState(false); + const [detailLoading, setDetailLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [filterValues, setFilterValues] = useState([]); + + // ─── 마스터 데이터 조회 ───────────────────────────────── + + const fetchMaster = useCallback(async () => { + setLoading(true); + try { + const search: Record = {}; + filterValues.forEach((f) => { + if (f.value) search[f.column] = f.value; + }); + + const res = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, { + page: 1, + size: 1000, + autoFilter: true, + search, + }); + const rows: InspectionMng[] = res.data?.data?.data ?? res.data?.data?.rows ?? []; + setMasterData(rows); + } catch (err) { + console.error("검사결과 조회 실패:", err); + } finally { + setLoading(false); + } + }, [filterValues]); + + useEffect(() => { + fetchMaster(); + }, [fetchMaster]); + + // ─── 디테일 데이터 조회 ───────────────────────────────── + + const fetchDetail = useCallback(async (masterId: string) => { + setDetailLoading(true); + try { + const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { + page: 1, + size: 500, + autoFilter: true, + search: { master_id: masterId }, + }); + const rows: InspectionDetail[] = res.data?.data?.data ?? res.data?.data?.rows ?? []; + setDetailData(rows); + } catch (err) { + console.error("검사상세 조회 실패:", err); + setDetailData([]); + } finally { + setDetailLoading(false); + } + }, []); + + useEffect(() => { + if (selectedId) fetchDetail(selectedId); + else setDetailData([]); + }, [selectedId, fetchDetail]); + + // ─── 선택된 마스터 ────────────────────────────────────── + + const selectedMaster = useMemo( + () => masterData.find((m) => m.id === selectedId), + [masterData, selectedId], + ); + + // ─── 엑셀 다운로드 ───────────────────────────────────── + + const handleExcel = async () => { + await exportToExcel(masterData, "검사결과관리.xlsx", "검사결과"); + }; + + // ─── 판정 배지 ────────────────────────────────────────── + + const judgmentBadge = (v: string) => { + if (!v) return null; + const lower = v.toLowerCase(); + if (["합격", "pass", "적합"].includes(lower)) + return {v}; + if (["불합격", "fail", "부적합"].includes(lower)) + return {v}; + return {v}; + }; + + // ─── 날짜 포맷 ────────────────────────────────────────── + + const fmtDate = (d: string) => { + if (!d) return "-"; + try { + return new Date(d).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }); + } catch { + return d; + } + }; + + // ─── 렌더링 ───────────────────────────────────────────── + + return ( +
+ {/* 검색 필터 */} +
+ { + setFilterValues(filters); + }} + dataCount={masterData.length} + /> +
+ + {/* 헤더 */} +
+
+ +

검사관리

+ {masterData.length}건 +
+
+ + +
+
+ + {/* 분할 패널 */} + + {/* 좌측: 마스터 테이블 */} + +
+ {loading && masterData.length === 0 ? ( +
+ + 데이터를 불러오는 중... +
+ ) : masterData.length === 0 ? ( +
+ + 검사 데이터가 없습니다 +
+ ) : ( + + + + # + 검사번호 + 검사유형 + 품목코드 + 품목명 + 검사수량 + 양품 + 불량 + 판정 + 검사자 + 검사일자 + 완료 + 거래처 + + + + {masterData.map((row, idx) => ( + setSelectedId(row.id)} + > + {idx + 1} + {row.inspection_number || "-"} + + {row.inspection_type || "-"} + + {row.item_code || "-"} + {row.item_name || "-"} + {row.total_qty ?? "-"} + {row.good_qty ?? "-"} + {row.bad_qty ?? "-"} + {judgmentBadge(row.overall_judgment)} + {row.inspector || "-"} + {fmtDate(row.inspection_date)} + + {row.is_completed === "Y" ? ( + 완료 + ) : ( + 진행중 + )} + + {row.supplier_name || "-"} + + ))} + +
+ )} +
+
+ + + + {/* 우측: 디테일 */} + + {!selectedId ? ( +
+ +

좌측에서 검사를 선택해주세요

+
+ ) : ( +
+ {/* 상세 헤더 */} +
+
+

검사 상세

+ {selectedMaster && ( + + {selectedMaster.inspection_number} + + )} +
+ {selectedMaster && ( +
+ {selectedMaster.item_name} + · + {judgmentBadge(selectedMaster.overall_judgment)} +
+ )} +
+ + {/* 상세 테이블 */} +
+ {detailLoading ? ( +
+ +
+ ) : detailData.length === 0 ? ( +
+ +

검사 항목이 없습니다

+
+ ) : ( + + + + # + 검사항목 + 검사기준 + 합격기준 + 측정값 + 판정 + 필수 + 비고 + + + + {detailData.map((d, idx) => ( + + {idx + 1} + {d.inspection_item_name || "-"} + {d.inspection_standard || "-"} + {d.pass_criteria || "-"} + {d.measured_value || "-"} + {judgmentBadge(d.judgment)} + + {d.is_required === "Y" ? ( + 필수 + ) : ( + - + )} + + {d.memo || "-"} + + ))} + +
+ )} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/app/(main)/COMPANY_29/equipment/inspection-record/page.tsx b/frontend/app/(main)/COMPANY_29/equipment/inspection-record/page.tsx new file mode 100644 index 00000000..b75f5f3d --- /dev/null +++ b/frontend/app/(main)/COMPANY_29/equipment/inspection-record/page.tsx @@ -0,0 +1,324 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { RefreshCw, Loader2, Inbox, Wrench, Download } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; + +// ─── 타입 ───────────────────────────────────────────────── + +interface InspectionRecord { + id: string; + equipment_code: string; + inspection_item_objid: string; + inspection_date: string; + status: string; + inspector: string; + remark: string; + created_date: string; +} + +interface InspectionItem { + id: string; + equipment_code: string; + inspection_item: string; + inspection_cycle: string; + inspection_content: string; + inspection_method: string; + lower_limit: string; + upper_limit: string; + unit: string; +} + +interface EquipmentInfo { + id: string; + equipment_code: string; + equipment_name: string; +} + +const RECORD_TABLE = "equipment_inspection_record"; + +// ─── 메인 컴포넌트 ─────────────────────────────────────── + +export default function EquipmentInspectionRecordPage() { + const [records, setRecords] = useState([]); + const [inspectionItems, setInspectionItems] = useState>(new Map()); + const [equipments, setEquipments] = useState>(new Map()); + const [loading, setLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [filterValues, setFilterValues] = useState([]); + + // ─── 데이터 조회 ──────────────────────────────────────── + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const search: Record = {}; + filterValues.forEach((f) => { + if (f.value) search[f.column] = f.value; + }); + + const [recordRes, itemRes, equipRes] = await Promise.all([ + apiClient.post(`/table-management/tables/${RECORD_TABLE}/data`, { + page: 1, + size: 1000, + autoFilter: true, + search, + }), + apiClient.post(`/table-management/tables/equipment_inspection_item/data`, { + page: 1, + size: 1000, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), + apiClient.post(`/table-management/tables/equipment_mng/data`, { + page: 1, + size: 500, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), + ]); + + const rRows: InspectionRecord[] = recordRes.data?.data?.data ?? recordRes.data?.data?.rows ?? []; + setRecords(rRows); + + const iRows: InspectionItem[] = itemRes.data?.data?.data ?? itemRes.data?.data?.rows ?? []; + const iMap = new Map(); + iRows.forEach((i) => iMap.set(i.id, i)); + setInspectionItems(iMap); + + const eRows: EquipmentInfo[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? []; + const eMap = new Map(); + eRows.forEach((e) => { + eMap.set(e.equipment_code, e); + eMap.set(e.id, e); + }); + setEquipments(eMap); + } catch (err) { + console.error("점검기록 조회 실패:", err); + } finally { + setLoading(false); + } + }, [filterValues]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + // ─── 선택된 레코드 ───────────────────────────────────── + + const selectedRecord = useMemo(() => records.find((r) => r.id === selectedId), [records, selectedId]); + const selectedItem = useMemo( + () => (selectedRecord ? inspectionItems.get(selectedRecord.inspection_item_objid) : undefined), + [selectedRecord, inspectionItems], + ); + + // ─── 설비명 조회 ─────────────────────────────────────── + + const getEquipName = (code: string) => equipments.get(code)?.equipment_name || code || "-"; + + // ─── 엑셀 다운로드 ───────────────────────────────────── + + const handleExcel = async () => { + const data = records.map((r) => ({ + 설비코드: r.equipment_code, + 설비명: getEquipName(r.equipment_code), + 점검항목: inspectionItems.get(r.inspection_item_objid)?.inspection_item || "-", + 점검일자: fmtDate(r.inspection_date), + 상태: r.status, + 점검자: r.inspector, + 비고: r.remark, + })); + await exportToExcel(data, "설비점검기록.xlsx", "점검기록"); + }; + + // ─── 날짜/상태 포맷 ──────────────────────────────────── + + const fmtDate = (d: string) => { + if (!d) return "-"; + try { + return new Date(d).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }); + } catch { + return d; + } + }; + + const statusBadge = (v: string) => { + if (!v) return -; + const lower = v.toLowerCase(); + if (["완료", "pass", "정상", "합격", "completed"].includes(lower)) + return {v}; + if (["이상", "fail", "불합격", "비정상"].includes(lower)) + return {v}; + if (["점검중", "진행", "in_progress"].includes(lower)) + return {v}; + return {v}; + }; + + // ─── 렌더링 ───────────────────────────────────────────── + + return ( +
+ {/* 검색 필터 */} +
+ setFilterValues(filters)} + dataCount={records.length} + /> +
+ + {/* 헤더 */} +
+
+ +

점검관리

+ {records.length}건 +
+
+ + +
+
+ + {/* 분할 패널 */} + + {/* 좌측: 점검기록 테이블 */} + +
+ {loading && records.length === 0 ? ( +
+ + 데이터를 불러오는 중... +
+ ) : records.length === 0 ? ( +
+ + 점검 기록이 없습니다 +
+ ) : ( + + + + # + 설비코드 + 설비명 + 점검항목 + 점검일자 + 상태 + 점검자 + 비고 + + + + {records.map((row, idx) => { + const item = inspectionItems.get(row.inspection_item_objid); + return ( + setSelectedId(row.id)} + > + {idx + 1} + {row.equipment_code || "-"} + {getEquipName(row.equipment_code)} + {item?.inspection_item || "-"} + {fmtDate(row.inspection_date)} + {statusBadge(row.status)} + {row.inspector || "-"} + {row.remark || "-"} + + ); + })} + +
+ )} +
+
+ + + + {/* 우측: 점검항목 상세 */} + + {!selectedId || !selectedRecord ? ( +
+ +

좌측에서 점검 기록을 선택해주세요

+
+ ) : ( +
+ {/* 점검 기록 요약 */} +
+

점검 기록 정보

+
+ + + + + + +
+
+ + {/* 점검 항목 상세 */} + {selectedItem ? ( +
+

점검 항목 상세

+
+ + + + + + + +
+
+ ) : ( +
+

점검 항목 정보가 없습니다

+
+ )} +
+ )} +
+
+
+ ); +} + +// ─── 상세 행 ────────────────────────────────────────────── + +function DetailRow({ + label, + value, + badge, + span2, +}: { + label: string; + value: string; + badge?: React.ReactNode; + span2?: boolean; +}) { + return ( +
+ {label} + {badge || {value}} +
+ ); +} diff --git a/frontend/app/(main)/COMPANY_29/quality/inspection-result/page.tsx b/frontend/app/(main)/COMPANY_29/quality/inspection-result/page.tsx new file mode 100644 index 00000000..d9e49d5f --- /dev/null +++ b/frontend/app/(main)/COMPANY_29/quality/inspection-result/page.tsx @@ -0,0 +1,351 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { RefreshCw, Loader2, Inbox, ClipboardCheck, Download } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; + +// ─── 타입 ───────────────────────────────────────────────── + +interface InspectionMng { + id: string; + inspection_number: string; + item_code: string; + item_name: string; + inspection_type: string; + total_qty: number; + good_qty: number; + bad_qty: number; + overall_judgment: string; + inspector: string; + inspection_date: string; + is_completed: string; + defect_description: string; + memo: string; + supplier_name: string; + supplier_code: string; + created_date: string; +} + +interface InspectionDetail { + id: string; + master_id: string; + inspection_item_name: string; + inspection_standard: string; + pass_criteria: string; + measured_value: string; + judgment: string; + is_required: string; + memo: string; + item_code: string; + item_name: string; + inspection_type: string; +} + +const MASTER_TABLE = "inspection_result_mng"; +const DETAIL_TABLE = "inspection_result"; + +// ─── 메인 컴포넌트 ─────────────────────────────────────── + +export default function InspectionResultPage() { + const [masterData, setMasterData] = useState([]); + const [detailData, setDetailData] = useState([]); + const [loading, setLoading] = useState(false); + const [detailLoading, setDetailLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [filterValues, setFilterValues] = useState([]); + + // ─── 마스터 데이터 조회 ───────────────────────────────── + + const fetchMaster = useCallback(async () => { + setLoading(true); + try { + const search: Record = {}; + filterValues.forEach((f) => { + if (f.value) search[f.column] = f.value; + }); + + const res = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, { + page: 1, + size: 1000, + autoFilter: true, + search, + }); + const rows: InspectionMng[] = res.data?.data?.data ?? res.data?.data?.rows ?? []; + setMasterData(rows); + } catch (err) { + console.error("검사결과 조회 실패:", err); + } finally { + setLoading(false); + } + }, [filterValues]); + + useEffect(() => { + fetchMaster(); + }, [fetchMaster]); + + // ─── 디테일 데이터 조회 ───────────────────────────────── + + const fetchDetail = useCallback(async (masterId: string) => { + setDetailLoading(true); + try { + const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { + page: 1, + size: 500, + autoFilter: true, + search: { master_id: masterId }, + }); + const rows: InspectionDetail[] = res.data?.data?.data ?? res.data?.data?.rows ?? []; + setDetailData(rows); + } catch (err) { + console.error("검사상세 조회 실패:", err); + setDetailData([]); + } finally { + setDetailLoading(false); + } + }, []); + + useEffect(() => { + if (selectedId) fetchDetail(selectedId); + else setDetailData([]); + }, [selectedId, fetchDetail]); + + // ─── 선택된 마스터 ────────────────────────────────────── + + const selectedMaster = useMemo( + () => masterData.find((m) => m.id === selectedId), + [masterData, selectedId], + ); + + // ─── 엑셀 다운로드 ───────────────────────────────────── + + const handleExcel = async () => { + await exportToExcel(masterData, "검사결과관리.xlsx", "검사결과"); + }; + + // ─── 판정 배지 ────────────────────────────────────────── + + const judgmentBadge = (v: string) => { + if (!v) return null; + const lower = v.toLowerCase(); + if (["합격", "pass", "적합"].includes(lower)) + return {v}; + if (["불합격", "fail", "부적합"].includes(lower)) + return {v}; + return {v}; + }; + + // ─── 날짜 포맷 ────────────────────────────────────────── + + const fmtDate = (d: string) => { + if (!d) return "-"; + try { + return new Date(d).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }); + } catch { + return d; + } + }; + + // ─── 렌더링 ───────────────────────────────────────────── + + return ( +
+ {/* 검색 필터 */} +
+ { + setFilterValues(filters); + }} + dataCount={masterData.length} + /> +
+ + {/* 헤더 */} +
+
+ +

검사관리

+ {masterData.length}건 +
+
+ + +
+
+ + {/* 분할 패널 */} + + {/* 좌측: 마스터 테이블 */} + +
+ {loading && masterData.length === 0 ? ( +
+ + 데이터를 불러오는 중... +
+ ) : masterData.length === 0 ? ( +
+ + 검사 데이터가 없습니다 +
+ ) : ( + + + + # + 검사번호 + 검사유형 + 품목코드 + 품목명 + 검사수량 + 양품 + 불량 + 판정 + 검사자 + 검사일자 + 완료 + 거래처 + + + + {masterData.map((row, idx) => ( + setSelectedId(row.id)} + > + {idx + 1} + {row.inspection_number || "-"} + + {row.inspection_type || "-"} + + {row.item_code || "-"} + {row.item_name || "-"} + {row.total_qty ?? "-"} + {row.good_qty ?? "-"} + {row.bad_qty ?? "-"} + {judgmentBadge(row.overall_judgment)} + {row.inspector || "-"} + {fmtDate(row.inspection_date)} + + {row.is_completed === "Y" ? ( + 완료 + ) : ( + 진행중 + )} + + {row.supplier_name || "-"} + + ))} + +
+ )} +
+
+ + + + {/* 우측: 디테일 */} + + {!selectedId ? ( +
+ +

좌측에서 검사를 선택해주세요

+
+ ) : ( +
+ {/* 상세 헤더 */} +
+
+

검사 상세

+ {selectedMaster && ( + + {selectedMaster.inspection_number} + + )} +
+ {selectedMaster && ( +
+ {selectedMaster.item_name} + · + {judgmentBadge(selectedMaster.overall_judgment)} +
+ )} +
+ + {/* 상세 테이블 */} +
+ {detailLoading ? ( +
+ +
+ ) : detailData.length === 0 ? ( +
+ +

검사 항목이 없습니다

+
+ ) : ( + + + + # + 검사항목 + 검사기준 + 합격기준 + 측정값 + 판정 + 필수 + 비고 + + + + {detailData.map((d, idx) => ( + + {idx + 1} + {d.inspection_item_name || "-"} + {d.inspection_standard || "-"} + {d.pass_criteria || "-"} + {d.measured_value || "-"} + {judgmentBadge(d.judgment)} + + {d.is_required === "Y" ? ( + 필수 + ) : ( + - + )} + + {d.memo || "-"} + + ))} + +
+ )} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/app/(main)/COMPANY_30/equipment/inspection-record/page.tsx b/frontend/app/(main)/COMPANY_30/equipment/inspection-record/page.tsx new file mode 100644 index 00000000..b75f5f3d --- /dev/null +++ b/frontend/app/(main)/COMPANY_30/equipment/inspection-record/page.tsx @@ -0,0 +1,324 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { RefreshCw, Loader2, Inbox, Wrench, Download } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; + +// ─── 타입 ───────────────────────────────────────────────── + +interface InspectionRecord { + id: string; + equipment_code: string; + inspection_item_objid: string; + inspection_date: string; + status: string; + inspector: string; + remark: string; + created_date: string; +} + +interface InspectionItem { + id: string; + equipment_code: string; + inspection_item: string; + inspection_cycle: string; + inspection_content: string; + inspection_method: string; + lower_limit: string; + upper_limit: string; + unit: string; +} + +interface EquipmentInfo { + id: string; + equipment_code: string; + equipment_name: string; +} + +const RECORD_TABLE = "equipment_inspection_record"; + +// ─── 메인 컴포넌트 ─────────────────────────────────────── + +export default function EquipmentInspectionRecordPage() { + const [records, setRecords] = useState([]); + const [inspectionItems, setInspectionItems] = useState>(new Map()); + const [equipments, setEquipments] = useState>(new Map()); + const [loading, setLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [filterValues, setFilterValues] = useState([]); + + // ─── 데이터 조회 ──────────────────────────────────────── + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const search: Record = {}; + filterValues.forEach((f) => { + if (f.value) search[f.column] = f.value; + }); + + const [recordRes, itemRes, equipRes] = await Promise.all([ + apiClient.post(`/table-management/tables/${RECORD_TABLE}/data`, { + page: 1, + size: 1000, + autoFilter: true, + search, + }), + apiClient.post(`/table-management/tables/equipment_inspection_item/data`, { + page: 1, + size: 1000, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), + apiClient.post(`/table-management/tables/equipment_mng/data`, { + page: 1, + size: 500, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), + ]); + + const rRows: InspectionRecord[] = recordRes.data?.data?.data ?? recordRes.data?.data?.rows ?? []; + setRecords(rRows); + + const iRows: InspectionItem[] = itemRes.data?.data?.data ?? itemRes.data?.data?.rows ?? []; + const iMap = new Map(); + iRows.forEach((i) => iMap.set(i.id, i)); + setInspectionItems(iMap); + + const eRows: EquipmentInfo[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? []; + const eMap = new Map(); + eRows.forEach((e) => { + eMap.set(e.equipment_code, e); + eMap.set(e.id, e); + }); + setEquipments(eMap); + } catch (err) { + console.error("점검기록 조회 실패:", err); + } finally { + setLoading(false); + } + }, [filterValues]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + // ─── 선택된 레코드 ───────────────────────────────────── + + const selectedRecord = useMemo(() => records.find((r) => r.id === selectedId), [records, selectedId]); + const selectedItem = useMemo( + () => (selectedRecord ? inspectionItems.get(selectedRecord.inspection_item_objid) : undefined), + [selectedRecord, inspectionItems], + ); + + // ─── 설비명 조회 ─────────────────────────────────────── + + const getEquipName = (code: string) => equipments.get(code)?.equipment_name || code || "-"; + + // ─── 엑셀 다운로드 ───────────────────────────────────── + + const handleExcel = async () => { + const data = records.map((r) => ({ + 설비코드: r.equipment_code, + 설비명: getEquipName(r.equipment_code), + 점검항목: inspectionItems.get(r.inspection_item_objid)?.inspection_item || "-", + 점검일자: fmtDate(r.inspection_date), + 상태: r.status, + 점검자: r.inspector, + 비고: r.remark, + })); + await exportToExcel(data, "설비점검기록.xlsx", "점검기록"); + }; + + // ─── 날짜/상태 포맷 ──────────────────────────────────── + + const fmtDate = (d: string) => { + if (!d) return "-"; + try { + return new Date(d).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }); + } catch { + return d; + } + }; + + const statusBadge = (v: string) => { + if (!v) return -; + const lower = v.toLowerCase(); + if (["완료", "pass", "정상", "합격", "completed"].includes(lower)) + return {v}; + if (["이상", "fail", "불합격", "비정상"].includes(lower)) + return {v}; + if (["점검중", "진행", "in_progress"].includes(lower)) + return {v}; + return {v}; + }; + + // ─── 렌더링 ───────────────────────────────────────────── + + return ( +
+ {/* 검색 필터 */} +
+ setFilterValues(filters)} + dataCount={records.length} + /> +
+ + {/* 헤더 */} +
+
+ +

점검관리

+ {records.length}건 +
+
+ + +
+
+ + {/* 분할 패널 */} + + {/* 좌측: 점검기록 테이블 */} + +
+ {loading && records.length === 0 ? ( +
+ + 데이터를 불러오는 중... +
+ ) : records.length === 0 ? ( +
+ + 점검 기록이 없습니다 +
+ ) : ( + + + + # + 설비코드 + 설비명 + 점검항목 + 점검일자 + 상태 + 점검자 + 비고 + + + + {records.map((row, idx) => { + const item = inspectionItems.get(row.inspection_item_objid); + return ( + setSelectedId(row.id)} + > + {idx + 1} + {row.equipment_code || "-"} + {getEquipName(row.equipment_code)} + {item?.inspection_item || "-"} + {fmtDate(row.inspection_date)} + {statusBadge(row.status)} + {row.inspector || "-"} + {row.remark || "-"} + + ); + })} + +
+ )} +
+
+ + + + {/* 우측: 점검항목 상세 */} + + {!selectedId || !selectedRecord ? ( +
+ +

좌측에서 점검 기록을 선택해주세요

+
+ ) : ( +
+ {/* 점검 기록 요약 */} +
+

점검 기록 정보

+
+ + + + + + +
+
+ + {/* 점검 항목 상세 */} + {selectedItem ? ( +
+

점검 항목 상세

+
+ + + + + + + +
+
+ ) : ( +
+

점검 항목 정보가 없습니다

+
+ )} +
+ )} +
+
+
+ ); +} + +// ─── 상세 행 ────────────────────────────────────────────── + +function DetailRow({ + label, + value, + badge, + span2, +}: { + label: string; + value: string; + badge?: React.ReactNode; + span2?: boolean; +}) { + return ( +
+ {label} + {badge || {value}} +
+ ); +} diff --git a/frontend/app/(main)/COMPANY_30/quality/inspection-result/page.tsx b/frontend/app/(main)/COMPANY_30/quality/inspection-result/page.tsx new file mode 100644 index 00000000..d9e49d5f --- /dev/null +++ b/frontend/app/(main)/COMPANY_30/quality/inspection-result/page.tsx @@ -0,0 +1,351 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { RefreshCw, Loader2, Inbox, ClipboardCheck, Download } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; + +// ─── 타입 ───────────────────────────────────────────────── + +interface InspectionMng { + id: string; + inspection_number: string; + item_code: string; + item_name: string; + inspection_type: string; + total_qty: number; + good_qty: number; + bad_qty: number; + overall_judgment: string; + inspector: string; + inspection_date: string; + is_completed: string; + defect_description: string; + memo: string; + supplier_name: string; + supplier_code: string; + created_date: string; +} + +interface InspectionDetail { + id: string; + master_id: string; + inspection_item_name: string; + inspection_standard: string; + pass_criteria: string; + measured_value: string; + judgment: string; + is_required: string; + memo: string; + item_code: string; + item_name: string; + inspection_type: string; +} + +const MASTER_TABLE = "inspection_result_mng"; +const DETAIL_TABLE = "inspection_result"; + +// ─── 메인 컴포넌트 ─────────────────────────────────────── + +export default function InspectionResultPage() { + const [masterData, setMasterData] = useState([]); + const [detailData, setDetailData] = useState([]); + const [loading, setLoading] = useState(false); + const [detailLoading, setDetailLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [filterValues, setFilterValues] = useState([]); + + // ─── 마스터 데이터 조회 ───────────────────────────────── + + const fetchMaster = useCallback(async () => { + setLoading(true); + try { + const search: Record = {}; + filterValues.forEach((f) => { + if (f.value) search[f.column] = f.value; + }); + + const res = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, { + page: 1, + size: 1000, + autoFilter: true, + search, + }); + const rows: InspectionMng[] = res.data?.data?.data ?? res.data?.data?.rows ?? []; + setMasterData(rows); + } catch (err) { + console.error("검사결과 조회 실패:", err); + } finally { + setLoading(false); + } + }, [filterValues]); + + useEffect(() => { + fetchMaster(); + }, [fetchMaster]); + + // ─── 디테일 데이터 조회 ───────────────────────────────── + + const fetchDetail = useCallback(async (masterId: string) => { + setDetailLoading(true); + try { + const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { + page: 1, + size: 500, + autoFilter: true, + search: { master_id: masterId }, + }); + const rows: InspectionDetail[] = res.data?.data?.data ?? res.data?.data?.rows ?? []; + setDetailData(rows); + } catch (err) { + console.error("검사상세 조회 실패:", err); + setDetailData([]); + } finally { + setDetailLoading(false); + } + }, []); + + useEffect(() => { + if (selectedId) fetchDetail(selectedId); + else setDetailData([]); + }, [selectedId, fetchDetail]); + + // ─── 선택된 마스터 ────────────────────────────────────── + + const selectedMaster = useMemo( + () => masterData.find((m) => m.id === selectedId), + [masterData, selectedId], + ); + + // ─── 엑셀 다운로드 ───────────────────────────────────── + + const handleExcel = async () => { + await exportToExcel(masterData, "검사결과관리.xlsx", "검사결과"); + }; + + // ─── 판정 배지 ────────────────────────────────────────── + + const judgmentBadge = (v: string) => { + if (!v) return null; + const lower = v.toLowerCase(); + if (["합격", "pass", "적합"].includes(lower)) + return {v}; + if (["불합격", "fail", "부적합"].includes(lower)) + return {v}; + return {v}; + }; + + // ─── 날짜 포맷 ────────────────────────────────────────── + + const fmtDate = (d: string) => { + if (!d) return "-"; + try { + return new Date(d).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }); + } catch { + return d; + } + }; + + // ─── 렌더링 ───────────────────────────────────────────── + + return ( +
+ {/* 검색 필터 */} +
+ { + setFilterValues(filters); + }} + dataCount={masterData.length} + /> +
+ + {/* 헤더 */} +
+
+ +

검사관리

+ {masterData.length}건 +
+
+ + +
+
+ + {/* 분할 패널 */} + + {/* 좌측: 마스터 테이블 */} + +
+ {loading && masterData.length === 0 ? ( +
+ + 데이터를 불러오는 중... +
+ ) : masterData.length === 0 ? ( +
+ + 검사 데이터가 없습니다 +
+ ) : ( + + + + # + 검사번호 + 검사유형 + 품목코드 + 품목명 + 검사수량 + 양품 + 불량 + 판정 + 검사자 + 검사일자 + 완료 + 거래처 + + + + {masterData.map((row, idx) => ( + setSelectedId(row.id)} + > + {idx + 1} + {row.inspection_number || "-"} + + {row.inspection_type || "-"} + + {row.item_code || "-"} + {row.item_name || "-"} + {row.total_qty ?? "-"} + {row.good_qty ?? "-"} + {row.bad_qty ?? "-"} + {judgmentBadge(row.overall_judgment)} + {row.inspector || "-"} + {fmtDate(row.inspection_date)} + + {row.is_completed === "Y" ? ( + 완료 + ) : ( + 진행중 + )} + + {row.supplier_name || "-"} + + ))} + +
+ )} +
+
+ + + + {/* 우측: 디테일 */} + + {!selectedId ? ( +
+ +

좌측에서 검사를 선택해주세요

+
+ ) : ( +
+ {/* 상세 헤더 */} +
+
+

검사 상세

+ {selectedMaster && ( + + {selectedMaster.inspection_number} + + )} +
+ {selectedMaster && ( +
+ {selectedMaster.item_name} + · + {judgmentBadge(selectedMaster.overall_judgment)} +
+ )} +
+ + {/* 상세 테이블 */} +
+ {detailLoading ? ( +
+ +
+ ) : detailData.length === 0 ? ( +
+ +

검사 항목이 없습니다

+
+ ) : ( + + + + # + 검사항목 + 검사기준 + 합격기준 + 측정값 + 판정 + 필수 + 비고 + + + + {detailData.map((d, idx) => ( + + {idx + 1} + {d.inspection_item_name || "-"} + {d.inspection_standard || "-"} + {d.pass_criteria || "-"} + {d.measured_value || "-"} + {judgmentBadge(d.judgment)} + + {d.is_required === "Y" ? ( + 필수 + ) : ( + - + )} + + {d.memo || "-"} + + ))} + +
+ )} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/app/(main)/COMPANY_7/equipment/inspection-record/page.tsx b/frontend/app/(main)/COMPANY_7/equipment/inspection-record/page.tsx new file mode 100644 index 00000000..b75f5f3d --- /dev/null +++ b/frontend/app/(main)/COMPANY_7/equipment/inspection-record/page.tsx @@ -0,0 +1,324 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { RefreshCw, Loader2, Inbox, Wrench, Download } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; + +// ─── 타입 ───────────────────────────────────────────────── + +interface InspectionRecord { + id: string; + equipment_code: string; + inspection_item_objid: string; + inspection_date: string; + status: string; + inspector: string; + remark: string; + created_date: string; +} + +interface InspectionItem { + id: string; + equipment_code: string; + inspection_item: string; + inspection_cycle: string; + inspection_content: string; + inspection_method: string; + lower_limit: string; + upper_limit: string; + unit: string; +} + +interface EquipmentInfo { + id: string; + equipment_code: string; + equipment_name: string; +} + +const RECORD_TABLE = "equipment_inspection_record"; + +// ─── 메인 컴포넌트 ─────────────────────────────────────── + +export default function EquipmentInspectionRecordPage() { + const [records, setRecords] = useState([]); + const [inspectionItems, setInspectionItems] = useState>(new Map()); + const [equipments, setEquipments] = useState>(new Map()); + const [loading, setLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [filterValues, setFilterValues] = useState([]); + + // ─── 데이터 조회 ──────────────────────────────────────── + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const search: Record = {}; + filterValues.forEach((f) => { + if (f.value) search[f.column] = f.value; + }); + + const [recordRes, itemRes, equipRes] = await Promise.all([ + apiClient.post(`/table-management/tables/${RECORD_TABLE}/data`, { + page: 1, + size: 1000, + autoFilter: true, + search, + }), + apiClient.post(`/table-management/tables/equipment_inspection_item/data`, { + page: 1, + size: 1000, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), + apiClient.post(`/table-management/tables/equipment_mng/data`, { + page: 1, + size: 500, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), + ]); + + const rRows: InspectionRecord[] = recordRes.data?.data?.data ?? recordRes.data?.data?.rows ?? []; + setRecords(rRows); + + const iRows: InspectionItem[] = itemRes.data?.data?.data ?? itemRes.data?.data?.rows ?? []; + const iMap = new Map(); + iRows.forEach((i) => iMap.set(i.id, i)); + setInspectionItems(iMap); + + const eRows: EquipmentInfo[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? []; + const eMap = new Map(); + eRows.forEach((e) => { + eMap.set(e.equipment_code, e); + eMap.set(e.id, e); + }); + setEquipments(eMap); + } catch (err) { + console.error("점검기록 조회 실패:", err); + } finally { + setLoading(false); + } + }, [filterValues]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + // ─── 선택된 레코드 ───────────────────────────────────── + + const selectedRecord = useMemo(() => records.find((r) => r.id === selectedId), [records, selectedId]); + const selectedItem = useMemo( + () => (selectedRecord ? inspectionItems.get(selectedRecord.inspection_item_objid) : undefined), + [selectedRecord, inspectionItems], + ); + + // ─── 설비명 조회 ─────────────────────────────────────── + + const getEquipName = (code: string) => equipments.get(code)?.equipment_name || code || "-"; + + // ─── 엑셀 다운로드 ───────────────────────────────────── + + const handleExcel = async () => { + const data = records.map((r) => ({ + 설비코드: r.equipment_code, + 설비명: getEquipName(r.equipment_code), + 점검항목: inspectionItems.get(r.inspection_item_objid)?.inspection_item || "-", + 점검일자: fmtDate(r.inspection_date), + 상태: r.status, + 점검자: r.inspector, + 비고: r.remark, + })); + await exportToExcel(data, "설비점검기록.xlsx", "점검기록"); + }; + + // ─── 날짜/상태 포맷 ──────────────────────────────────── + + const fmtDate = (d: string) => { + if (!d) return "-"; + try { + return new Date(d).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }); + } catch { + return d; + } + }; + + const statusBadge = (v: string) => { + if (!v) return -; + const lower = v.toLowerCase(); + if (["완료", "pass", "정상", "합격", "completed"].includes(lower)) + return {v}; + if (["이상", "fail", "불합격", "비정상"].includes(lower)) + return {v}; + if (["점검중", "진행", "in_progress"].includes(lower)) + return {v}; + return {v}; + }; + + // ─── 렌더링 ───────────────────────────────────────────── + + return ( +
+ {/* 검색 필터 */} +
+ setFilterValues(filters)} + dataCount={records.length} + /> +
+ + {/* 헤더 */} +
+
+ +

점검관리

+ {records.length}건 +
+
+ + +
+
+ + {/* 분할 패널 */} + + {/* 좌측: 점검기록 테이블 */} + +
+ {loading && records.length === 0 ? ( +
+ + 데이터를 불러오는 중... +
+ ) : records.length === 0 ? ( +
+ + 점검 기록이 없습니다 +
+ ) : ( + + + + # + 설비코드 + 설비명 + 점검항목 + 점검일자 + 상태 + 점검자 + 비고 + + + + {records.map((row, idx) => { + const item = inspectionItems.get(row.inspection_item_objid); + return ( + setSelectedId(row.id)} + > + {idx + 1} + {row.equipment_code || "-"} + {getEquipName(row.equipment_code)} + {item?.inspection_item || "-"} + {fmtDate(row.inspection_date)} + {statusBadge(row.status)} + {row.inspector || "-"} + {row.remark || "-"} + + ); + })} + +
+ )} +
+
+ + + + {/* 우측: 점검항목 상세 */} + + {!selectedId || !selectedRecord ? ( +
+ +

좌측에서 점검 기록을 선택해주세요

+
+ ) : ( +
+ {/* 점검 기록 요약 */} +
+

점검 기록 정보

+
+ + + + + + +
+
+ + {/* 점검 항목 상세 */} + {selectedItem ? ( +
+

점검 항목 상세

+
+ + + + + + + +
+
+ ) : ( +
+

점검 항목 정보가 없습니다

+
+ )} +
+ )} +
+
+
+ ); +} + +// ─── 상세 행 ────────────────────────────────────────────── + +function DetailRow({ + label, + value, + badge, + span2, +}: { + label: string; + value: string; + badge?: React.ReactNode; + span2?: boolean; +}) { + return ( +
+ {label} + {badge || {value}} +
+ ); +} diff --git a/frontend/app/(main)/COMPANY_7/quality/inspection-result/page.tsx b/frontend/app/(main)/COMPANY_7/quality/inspection-result/page.tsx new file mode 100644 index 00000000..d9e49d5f --- /dev/null +++ b/frontend/app/(main)/COMPANY_7/quality/inspection-result/page.tsx @@ -0,0 +1,351 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { RefreshCw, Loader2, Inbox, ClipboardCheck, Download } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; + +// ─── 타입 ───────────────────────────────────────────────── + +interface InspectionMng { + id: string; + inspection_number: string; + item_code: string; + item_name: string; + inspection_type: string; + total_qty: number; + good_qty: number; + bad_qty: number; + overall_judgment: string; + inspector: string; + inspection_date: string; + is_completed: string; + defect_description: string; + memo: string; + supplier_name: string; + supplier_code: string; + created_date: string; +} + +interface InspectionDetail { + id: string; + master_id: string; + inspection_item_name: string; + inspection_standard: string; + pass_criteria: string; + measured_value: string; + judgment: string; + is_required: string; + memo: string; + item_code: string; + item_name: string; + inspection_type: string; +} + +const MASTER_TABLE = "inspection_result_mng"; +const DETAIL_TABLE = "inspection_result"; + +// ─── 메인 컴포넌트 ─────────────────────────────────────── + +export default function InspectionResultPage() { + const [masterData, setMasterData] = useState([]); + const [detailData, setDetailData] = useState([]); + const [loading, setLoading] = useState(false); + const [detailLoading, setDetailLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [filterValues, setFilterValues] = useState([]); + + // ─── 마스터 데이터 조회 ───────────────────────────────── + + const fetchMaster = useCallback(async () => { + setLoading(true); + try { + const search: Record = {}; + filterValues.forEach((f) => { + if (f.value) search[f.column] = f.value; + }); + + const res = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, { + page: 1, + size: 1000, + autoFilter: true, + search, + }); + const rows: InspectionMng[] = res.data?.data?.data ?? res.data?.data?.rows ?? []; + setMasterData(rows); + } catch (err) { + console.error("검사결과 조회 실패:", err); + } finally { + setLoading(false); + } + }, [filterValues]); + + useEffect(() => { + fetchMaster(); + }, [fetchMaster]); + + // ─── 디테일 데이터 조회 ───────────────────────────────── + + const fetchDetail = useCallback(async (masterId: string) => { + setDetailLoading(true); + try { + const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { + page: 1, + size: 500, + autoFilter: true, + search: { master_id: masterId }, + }); + const rows: InspectionDetail[] = res.data?.data?.data ?? res.data?.data?.rows ?? []; + setDetailData(rows); + } catch (err) { + console.error("검사상세 조회 실패:", err); + setDetailData([]); + } finally { + setDetailLoading(false); + } + }, []); + + useEffect(() => { + if (selectedId) fetchDetail(selectedId); + else setDetailData([]); + }, [selectedId, fetchDetail]); + + // ─── 선택된 마스터 ────────────────────────────────────── + + const selectedMaster = useMemo( + () => masterData.find((m) => m.id === selectedId), + [masterData, selectedId], + ); + + // ─── 엑셀 다운로드 ───────────────────────────────────── + + const handleExcel = async () => { + await exportToExcel(masterData, "검사결과관리.xlsx", "검사결과"); + }; + + // ─── 판정 배지 ────────────────────────────────────────── + + const judgmentBadge = (v: string) => { + if (!v) return null; + const lower = v.toLowerCase(); + if (["합격", "pass", "적합"].includes(lower)) + return {v}; + if (["불합격", "fail", "부적합"].includes(lower)) + return {v}; + return {v}; + }; + + // ─── 날짜 포맷 ────────────────────────────────────────── + + const fmtDate = (d: string) => { + if (!d) return "-"; + try { + return new Date(d).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }); + } catch { + return d; + } + }; + + // ─── 렌더링 ───────────────────────────────────────────── + + return ( +
+ {/* 검색 필터 */} +
+ { + setFilterValues(filters); + }} + dataCount={masterData.length} + /> +
+ + {/* 헤더 */} +
+
+ +

검사관리

+ {masterData.length}건 +
+
+ + +
+
+ + {/* 분할 패널 */} + + {/* 좌측: 마스터 테이블 */} + +
+ {loading && masterData.length === 0 ? ( +
+ + 데이터를 불러오는 중... +
+ ) : masterData.length === 0 ? ( +
+ + 검사 데이터가 없습니다 +
+ ) : ( + + + + # + 검사번호 + 검사유형 + 품목코드 + 품목명 + 검사수량 + 양품 + 불량 + 판정 + 검사자 + 검사일자 + 완료 + 거래처 + + + + {masterData.map((row, idx) => ( + setSelectedId(row.id)} + > + {idx + 1} + {row.inspection_number || "-"} + + {row.inspection_type || "-"} + + {row.item_code || "-"} + {row.item_name || "-"} + {row.total_qty ?? "-"} + {row.good_qty ?? "-"} + {row.bad_qty ?? "-"} + {judgmentBadge(row.overall_judgment)} + {row.inspector || "-"} + {fmtDate(row.inspection_date)} + + {row.is_completed === "Y" ? ( + 완료 + ) : ( + 진행중 + )} + + {row.supplier_name || "-"} + + ))} + +
+ )} +
+
+ + + + {/* 우측: 디테일 */} + + {!selectedId ? ( +
+ +

좌측에서 검사를 선택해주세요

+
+ ) : ( +
+ {/* 상세 헤더 */} +
+
+

검사 상세

+ {selectedMaster && ( + + {selectedMaster.inspection_number} + + )} +
+ {selectedMaster && ( +
+ {selectedMaster.item_name} + · + {judgmentBadge(selectedMaster.overall_judgment)} +
+ )} +
+ + {/* 상세 테이블 */} +
+ {detailLoading ? ( +
+ +
+ ) : detailData.length === 0 ? ( +
+ +

검사 항목이 없습니다

+
+ ) : ( + + + + # + 검사항목 + 검사기준 + 합격기준 + 측정값 + 판정 + 필수 + 비고 + + + + {detailData.map((d, idx) => ( + + {idx + 1} + {d.inspection_item_name || "-"} + {d.inspection_standard || "-"} + {d.pass_criteria || "-"} + {d.measured_value || "-"} + {judgmentBadge(d.judgment)} + + {d.is_required === "Y" ? ( + 필수 + ) : ( + - + )} + + {d.memo || "-"} + + ))} + +
+ )} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/app/(main)/COMPANY_8/equipment/inspection-record/page.tsx b/frontend/app/(main)/COMPANY_8/equipment/inspection-record/page.tsx new file mode 100644 index 00000000..b75f5f3d --- /dev/null +++ b/frontend/app/(main)/COMPANY_8/equipment/inspection-record/page.tsx @@ -0,0 +1,324 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { RefreshCw, Loader2, Inbox, Wrench, Download } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; + +// ─── 타입 ───────────────────────────────────────────────── + +interface InspectionRecord { + id: string; + equipment_code: string; + inspection_item_objid: string; + inspection_date: string; + status: string; + inspector: string; + remark: string; + created_date: string; +} + +interface InspectionItem { + id: string; + equipment_code: string; + inspection_item: string; + inspection_cycle: string; + inspection_content: string; + inspection_method: string; + lower_limit: string; + upper_limit: string; + unit: string; +} + +interface EquipmentInfo { + id: string; + equipment_code: string; + equipment_name: string; +} + +const RECORD_TABLE = "equipment_inspection_record"; + +// ─── 메인 컴포넌트 ─────────────────────────────────────── + +export default function EquipmentInspectionRecordPage() { + const [records, setRecords] = useState([]); + const [inspectionItems, setInspectionItems] = useState>(new Map()); + const [equipments, setEquipments] = useState>(new Map()); + const [loading, setLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [filterValues, setFilterValues] = useState([]); + + // ─── 데이터 조회 ──────────────────────────────────────── + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const search: Record = {}; + filterValues.forEach((f) => { + if (f.value) search[f.column] = f.value; + }); + + const [recordRes, itemRes, equipRes] = await Promise.all([ + apiClient.post(`/table-management/tables/${RECORD_TABLE}/data`, { + page: 1, + size: 1000, + autoFilter: true, + search, + }), + apiClient.post(`/table-management/tables/equipment_inspection_item/data`, { + page: 1, + size: 1000, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), + apiClient.post(`/table-management/tables/equipment_mng/data`, { + page: 1, + size: 500, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), + ]); + + const rRows: InspectionRecord[] = recordRes.data?.data?.data ?? recordRes.data?.data?.rows ?? []; + setRecords(rRows); + + const iRows: InspectionItem[] = itemRes.data?.data?.data ?? itemRes.data?.data?.rows ?? []; + const iMap = new Map(); + iRows.forEach((i) => iMap.set(i.id, i)); + setInspectionItems(iMap); + + const eRows: EquipmentInfo[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? []; + const eMap = new Map(); + eRows.forEach((e) => { + eMap.set(e.equipment_code, e); + eMap.set(e.id, e); + }); + setEquipments(eMap); + } catch (err) { + console.error("점검기록 조회 실패:", err); + } finally { + setLoading(false); + } + }, [filterValues]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + // ─── 선택된 레코드 ───────────────────────────────────── + + const selectedRecord = useMemo(() => records.find((r) => r.id === selectedId), [records, selectedId]); + const selectedItem = useMemo( + () => (selectedRecord ? inspectionItems.get(selectedRecord.inspection_item_objid) : undefined), + [selectedRecord, inspectionItems], + ); + + // ─── 설비명 조회 ─────────────────────────────────────── + + const getEquipName = (code: string) => equipments.get(code)?.equipment_name || code || "-"; + + // ─── 엑셀 다운로드 ───────────────────────────────────── + + const handleExcel = async () => { + const data = records.map((r) => ({ + 설비코드: r.equipment_code, + 설비명: getEquipName(r.equipment_code), + 점검항목: inspectionItems.get(r.inspection_item_objid)?.inspection_item || "-", + 점검일자: fmtDate(r.inspection_date), + 상태: r.status, + 점검자: r.inspector, + 비고: r.remark, + })); + await exportToExcel(data, "설비점검기록.xlsx", "점검기록"); + }; + + // ─── 날짜/상태 포맷 ──────────────────────────────────── + + const fmtDate = (d: string) => { + if (!d) return "-"; + try { + return new Date(d).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }); + } catch { + return d; + } + }; + + const statusBadge = (v: string) => { + if (!v) return -; + const lower = v.toLowerCase(); + if (["완료", "pass", "정상", "합격", "completed"].includes(lower)) + return {v}; + if (["이상", "fail", "불합격", "비정상"].includes(lower)) + return {v}; + if (["점검중", "진행", "in_progress"].includes(lower)) + return {v}; + return {v}; + }; + + // ─── 렌더링 ───────────────────────────────────────────── + + return ( +
+ {/* 검색 필터 */} +
+ setFilterValues(filters)} + dataCount={records.length} + /> +
+ + {/* 헤더 */} +
+
+ +

점검관리

+ {records.length}건 +
+
+ + +
+
+ + {/* 분할 패널 */} + + {/* 좌측: 점검기록 테이블 */} + +
+ {loading && records.length === 0 ? ( +
+ + 데이터를 불러오는 중... +
+ ) : records.length === 0 ? ( +
+ + 점검 기록이 없습니다 +
+ ) : ( + + + + # + 설비코드 + 설비명 + 점검항목 + 점검일자 + 상태 + 점검자 + 비고 + + + + {records.map((row, idx) => { + const item = inspectionItems.get(row.inspection_item_objid); + return ( + setSelectedId(row.id)} + > + {idx + 1} + {row.equipment_code || "-"} + {getEquipName(row.equipment_code)} + {item?.inspection_item || "-"} + {fmtDate(row.inspection_date)} + {statusBadge(row.status)} + {row.inspector || "-"} + {row.remark || "-"} + + ); + })} + +
+ )} +
+
+ + + + {/* 우측: 점검항목 상세 */} + + {!selectedId || !selectedRecord ? ( +
+ +

좌측에서 점검 기록을 선택해주세요

+
+ ) : ( +
+ {/* 점검 기록 요약 */} +
+

점검 기록 정보

+
+ + + + + + +
+
+ + {/* 점검 항목 상세 */} + {selectedItem ? ( +
+

점검 항목 상세

+
+ + + + + + + +
+
+ ) : ( +
+

점검 항목 정보가 없습니다

+
+ )} +
+ )} +
+
+
+ ); +} + +// ─── 상세 행 ────────────────────────────────────────────── + +function DetailRow({ + label, + value, + badge, + span2, +}: { + label: string; + value: string; + badge?: React.ReactNode; + span2?: boolean; +}) { + return ( +
+ {label} + {badge || {value}} +
+ ); +} diff --git a/frontend/app/(main)/COMPANY_8/quality/inspection-result/page.tsx b/frontend/app/(main)/COMPANY_8/quality/inspection-result/page.tsx new file mode 100644 index 00000000..d9e49d5f --- /dev/null +++ b/frontend/app/(main)/COMPANY_8/quality/inspection-result/page.tsx @@ -0,0 +1,351 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { RefreshCw, Loader2, Inbox, ClipboardCheck, Download } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; + +// ─── 타입 ───────────────────────────────────────────────── + +interface InspectionMng { + id: string; + inspection_number: string; + item_code: string; + item_name: string; + inspection_type: string; + total_qty: number; + good_qty: number; + bad_qty: number; + overall_judgment: string; + inspector: string; + inspection_date: string; + is_completed: string; + defect_description: string; + memo: string; + supplier_name: string; + supplier_code: string; + created_date: string; +} + +interface InspectionDetail { + id: string; + master_id: string; + inspection_item_name: string; + inspection_standard: string; + pass_criteria: string; + measured_value: string; + judgment: string; + is_required: string; + memo: string; + item_code: string; + item_name: string; + inspection_type: string; +} + +const MASTER_TABLE = "inspection_result_mng"; +const DETAIL_TABLE = "inspection_result"; + +// ─── 메인 컴포넌트 ─────────────────────────────────────── + +export default function InspectionResultPage() { + const [masterData, setMasterData] = useState([]); + const [detailData, setDetailData] = useState([]); + const [loading, setLoading] = useState(false); + const [detailLoading, setDetailLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [filterValues, setFilterValues] = useState([]); + + // ─── 마스터 데이터 조회 ───────────────────────────────── + + const fetchMaster = useCallback(async () => { + setLoading(true); + try { + const search: Record = {}; + filterValues.forEach((f) => { + if (f.value) search[f.column] = f.value; + }); + + const res = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, { + page: 1, + size: 1000, + autoFilter: true, + search, + }); + const rows: InspectionMng[] = res.data?.data?.data ?? res.data?.data?.rows ?? []; + setMasterData(rows); + } catch (err) { + console.error("검사결과 조회 실패:", err); + } finally { + setLoading(false); + } + }, [filterValues]); + + useEffect(() => { + fetchMaster(); + }, [fetchMaster]); + + // ─── 디테일 데이터 조회 ───────────────────────────────── + + const fetchDetail = useCallback(async (masterId: string) => { + setDetailLoading(true); + try { + const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { + page: 1, + size: 500, + autoFilter: true, + search: { master_id: masterId }, + }); + const rows: InspectionDetail[] = res.data?.data?.data ?? res.data?.data?.rows ?? []; + setDetailData(rows); + } catch (err) { + console.error("검사상세 조회 실패:", err); + setDetailData([]); + } finally { + setDetailLoading(false); + } + }, []); + + useEffect(() => { + if (selectedId) fetchDetail(selectedId); + else setDetailData([]); + }, [selectedId, fetchDetail]); + + // ─── 선택된 마스터 ────────────────────────────────────── + + const selectedMaster = useMemo( + () => masterData.find((m) => m.id === selectedId), + [masterData, selectedId], + ); + + // ─── 엑셀 다운로드 ───────────────────────────────────── + + const handleExcel = async () => { + await exportToExcel(masterData, "검사결과관리.xlsx", "검사결과"); + }; + + // ─── 판정 배지 ────────────────────────────────────────── + + const judgmentBadge = (v: string) => { + if (!v) return null; + const lower = v.toLowerCase(); + if (["합격", "pass", "적합"].includes(lower)) + return {v}; + if (["불합격", "fail", "부적합"].includes(lower)) + return {v}; + return {v}; + }; + + // ─── 날짜 포맷 ────────────────────────────────────────── + + const fmtDate = (d: string) => { + if (!d) return "-"; + try { + return new Date(d).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }); + } catch { + return d; + } + }; + + // ─── 렌더링 ───────────────────────────────────────────── + + return ( +
+ {/* 검색 필터 */} +
+ { + setFilterValues(filters); + }} + dataCount={masterData.length} + /> +
+ + {/* 헤더 */} +
+
+ +

검사관리

+ {masterData.length}건 +
+
+ + +
+
+ + {/* 분할 패널 */} + + {/* 좌측: 마스터 테이블 */} + +
+ {loading && masterData.length === 0 ? ( +
+ + 데이터를 불러오는 중... +
+ ) : masterData.length === 0 ? ( +
+ + 검사 데이터가 없습니다 +
+ ) : ( + + + + # + 검사번호 + 검사유형 + 품목코드 + 품목명 + 검사수량 + 양품 + 불량 + 판정 + 검사자 + 검사일자 + 완료 + 거래처 + + + + {masterData.map((row, idx) => ( + setSelectedId(row.id)} + > + {idx + 1} + {row.inspection_number || "-"} + + {row.inspection_type || "-"} + + {row.item_code || "-"} + {row.item_name || "-"} + {row.total_qty ?? "-"} + {row.good_qty ?? "-"} + {row.bad_qty ?? "-"} + {judgmentBadge(row.overall_judgment)} + {row.inspector || "-"} + {fmtDate(row.inspection_date)} + + {row.is_completed === "Y" ? ( + 완료 + ) : ( + 진행중 + )} + + {row.supplier_name || "-"} + + ))} + +
+ )} +
+
+ + + + {/* 우측: 디테일 */} + + {!selectedId ? ( +
+ +

좌측에서 검사를 선택해주세요

+
+ ) : ( +
+ {/* 상세 헤더 */} +
+
+

검사 상세

+ {selectedMaster && ( + + {selectedMaster.inspection_number} + + )} +
+ {selectedMaster && ( +
+ {selectedMaster.item_name} + · + {judgmentBadge(selectedMaster.overall_judgment)} +
+ )} +
+ + {/* 상세 테이블 */} +
+ {detailLoading ? ( +
+ +
+ ) : detailData.length === 0 ? ( +
+ +

검사 항목이 없습니다

+
+ ) : ( + + + + # + 검사항목 + 검사기준 + 합격기준 + 측정값 + 판정 + 필수 + 비고 + + + + {detailData.map((d, idx) => ( + + {idx + 1} + {d.inspection_item_name || "-"} + {d.inspection_standard || "-"} + {d.pass_criteria || "-"} + {d.measured_value || "-"} + {judgmentBadge(d.judgment)} + + {d.is_required === "Y" ? ( + 필수 + ) : ( + - + )} + + {d.memo || "-"} + + ))} + +
+ )} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/app/(main)/COMPANY_9/equipment/inspection-record/page.tsx b/frontend/app/(main)/COMPANY_9/equipment/inspection-record/page.tsx new file mode 100644 index 00000000..b75f5f3d --- /dev/null +++ b/frontend/app/(main)/COMPANY_9/equipment/inspection-record/page.tsx @@ -0,0 +1,324 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { RefreshCw, Loader2, Inbox, Wrench, Download } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; + +// ─── 타입 ───────────────────────────────────────────────── + +interface InspectionRecord { + id: string; + equipment_code: string; + inspection_item_objid: string; + inspection_date: string; + status: string; + inspector: string; + remark: string; + created_date: string; +} + +interface InspectionItem { + id: string; + equipment_code: string; + inspection_item: string; + inspection_cycle: string; + inspection_content: string; + inspection_method: string; + lower_limit: string; + upper_limit: string; + unit: string; +} + +interface EquipmentInfo { + id: string; + equipment_code: string; + equipment_name: string; +} + +const RECORD_TABLE = "equipment_inspection_record"; + +// ─── 메인 컴포넌트 ─────────────────────────────────────── + +export default function EquipmentInspectionRecordPage() { + const [records, setRecords] = useState([]); + const [inspectionItems, setInspectionItems] = useState>(new Map()); + const [equipments, setEquipments] = useState>(new Map()); + const [loading, setLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [filterValues, setFilterValues] = useState([]); + + // ─── 데이터 조회 ──────────────────────────────────────── + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const search: Record = {}; + filterValues.forEach((f) => { + if (f.value) search[f.column] = f.value; + }); + + const [recordRes, itemRes, equipRes] = await Promise.all([ + apiClient.post(`/table-management/tables/${RECORD_TABLE}/data`, { + page: 1, + size: 1000, + autoFilter: true, + search, + }), + apiClient.post(`/table-management/tables/equipment_inspection_item/data`, { + page: 1, + size: 1000, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), + apiClient.post(`/table-management/tables/equipment_mng/data`, { + page: 1, + size: 500, + autoFilter: true, + }).catch(() => ({ data: { data: { data: [] } } })), + ]); + + const rRows: InspectionRecord[] = recordRes.data?.data?.data ?? recordRes.data?.data?.rows ?? []; + setRecords(rRows); + + const iRows: InspectionItem[] = itemRes.data?.data?.data ?? itemRes.data?.data?.rows ?? []; + const iMap = new Map(); + iRows.forEach((i) => iMap.set(i.id, i)); + setInspectionItems(iMap); + + const eRows: EquipmentInfo[] = equipRes.data?.data?.data ?? equipRes.data?.data?.rows ?? []; + const eMap = new Map(); + eRows.forEach((e) => { + eMap.set(e.equipment_code, e); + eMap.set(e.id, e); + }); + setEquipments(eMap); + } catch (err) { + console.error("점검기록 조회 실패:", err); + } finally { + setLoading(false); + } + }, [filterValues]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + // ─── 선택된 레코드 ───────────────────────────────────── + + const selectedRecord = useMemo(() => records.find((r) => r.id === selectedId), [records, selectedId]); + const selectedItem = useMemo( + () => (selectedRecord ? inspectionItems.get(selectedRecord.inspection_item_objid) : undefined), + [selectedRecord, inspectionItems], + ); + + // ─── 설비명 조회 ─────────────────────────────────────── + + const getEquipName = (code: string) => equipments.get(code)?.equipment_name || code || "-"; + + // ─── 엑셀 다운로드 ───────────────────────────────────── + + const handleExcel = async () => { + const data = records.map((r) => ({ + 설비코드: r.equipment_code, + 설비명: getEquipName(r.equipment_code), + 점검항목: inspectionItems.get(r.inspection_item_objid)?.inspection_item || "-", + 점검일자: fmtDate(r.inspection_date), + 상태: r.status, + 점검자: r.inspector, + 비고: r.remark, + })); + await exportToExcel(data, "설비점검기록.xlsx", "점검기록"); + }; + + // ─── 날짜/상태 포맷 ──────────────────────────────────── + + const fmtDate = (d: string) => { + if (!d) return "-"; + try { + return new Date(d).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }); + } catch { + return d; + } + }; + + const statusBadge = (v: string) => { + if (!v) return -; + const lower = v.toLowerCase(); + if (["완료", "pass", "정상", "합격", "completed"].includes(lower)) + return {v}; + if (["이상", "fail", "불합격", "비정상"].includes(lower)) + return {v}; + if (["점검중", "진행", "in_progress"].includes(lower)) + return {v}; + return {v}; + }; + + // ─── 렌더링 ───────────────────────────────────────────── + + return ( +
+ {/* 검색 필터 */} +
+ setFilterValues(filters)} + dataCount={records.length} + /> +
+ + {/* 헤더 */} +
+
+ +

점검관리

+ {records.length}건 +
+
+ + +
+
+ + {/* 분할 패널 */} + + {/* 좌측: 점검기록 테이블 */} + +
+ {loading && records.length === 0 ? ( +
+ + 데이터를 불러오는 중... +
+ ) : records.length === 0 ? ( +
+ + 점검 기록이 없습니다 +
+ ) : ( + + + + # + 설비코드 + 설비명 + 점검항목 + 점검일자 + 상태 + 점검자 + 비고 + + + + {records.map((row, idx) => { + const item = inspectionItems.get(row.inspection_item_objid); + return ( + setSelectedId(row.id)} + > + {idx + 1} + {row.equipment_code || "-"} + {getEquipName(row.equipment_code)} + {item?.inspection_item || "-"} + {fmtDate(row.inspection_date)} + {statusBadge(row.status)} + {row.inspector || "-"} + {row.remark || "-"} + + ); + })} + +
+ )} +
+
+ + + + {/* 우측: 점검항목 상세 */} + + {!selectedId || !selectedRecord ? ( +
+ +

좌측에서 점검 기록을 선택해주세요

+
+ ) : ( +
+ {/* 점검 기록 요약 */} +
+

점검 기록 정보

+
+ + + + + + +
+
+ + {/* 점검 항목 상세 */} + {selectedItem ? ( +
+

점검 항목 상세

+
+ + + + + + + +
+
+ ) : ( +
+

점검 항목 정보가 없습니다

+
+ )} +
+ )} +
+
+
+ ); +} + +// ─── 상세 행 ────────────────────────────────────────────── + +function DetailRow({ + label, + value, + badge, + span2, +}: { + label: string; + value: string; + badge?: React.ReactNode; + span2?: boolean; +}) { + return ( +
+ {label} + {badge || {value}} +
+ ); +} diff --git a/frontend/app/(main)/COMPANY_9/quality/inspection-result/page.tsx b/frontend/app/(main)/COMPANY_9/quality/inspection-result/page.tsx new file mode 100644 index 00000000..d9e49d5f --- /dev/null +++ b/frontend/app/(main)/COMPANY_9/quality/inspection-result/page.tsx @@ -0,0 +1,351 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { RefreshCw, Loader2, Inbox, ClipboardCheck, Download } from "lucide-react"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; + +// ─── 타입 ───────────────────────────────────────────────── + +interface InspectionMng { + id: string; + inspection_number: string; + item_code: string; + item_name: string; + inspection_type: string; + total_qty: number; + good_qty: number; + bad_qty: number; + overall_judgment: string; + inspector: string; + inspection_date: string; + is_completed: string; + defect_description: string; + memo: string; + supplier_name: string; + supplier_code: string; + created_date: string; +} + +interface InspectionDetail { + id: string; + master_id: string; + inspection_item_name: string; + inspection_standard: string; + pass_criteria: string; + measured_value: string; + judgment: string; + is_required: string; + memo: string; + item_code: string; + item_name: string; + inspection_type: string; +} + +const MASTER_TABLE = "inspection_result_mng"; +const DETAIL_TABLE = "inspection_result"; + +// ─── 메인 컴포넌트 ─────────────────────────────────────── + +export default function InspectionResultPage() { + const [masterData, setMasterData] = useState([]); + const [detailData, setDetailData] = useState([]); + const [loading, setLoading] = useState(false); + const [detailLoading, setDetailLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [filterValues, setFilterValues] = useState([]); + + // ─── 마스터 데이터 조회 ───────────────────────────────── + + const fetchMaster = useCallback(async () => { + setLoading(true); + try { + const search: Record = {}; + filterValues.forEach((f) => { + if (f.value) search[f.column] = f.value; + }); + + const res = await apiClient.post(`/table-management/tables/${MASTER_TABLE}/data`, { + page: 1, + size: 1000, + autoFilter: true, + search, + }); + const rows: InspectionMng[] = res.data?.data?.data ?? res.data?.data?.rows ?? []; + setMasterData(rows); + } catch (err) { + console.error("검사결과 조회 실패:", err); + } finally { + setLoading(false); + } + }, [filterValues]); + + useEffect(() => { + fetchMaster(); + }, [fetchMaster]); + + // ─── 디테일 데이터 조회 ───────────────────────────────── + + const fetchDetail = useCallback(async (masterId: string) => { + setDetailLoading(true); + try { + const res = await apiClient.post(`/table-management/tables/${DETAIL_TABLE}/data`, { + page: 1, + size: 500, + autoFilter: true, + search: { master_id: masterId }, + }); + const rows: InspectionDetail[] = res.data?.data?.data ?? res.data?.data?.rows ?? []; + setDetailData(rows); + } catch (err) { + console.error("검사상세 조회 실패:", err); + setDetailData([]); + } finally { + setDetailLoading(false); + } + }, []); + + useEffect(() => { + if (selectedId) fetchDetail(selectedId); + else setDetailData([]); + }, [selectedId, fetchDetail]); + + // ─── 선택된 마스터 ────────────────────────────────────── + + const selectedMaster = useMemo( + () => masterData.find((m) => m.id === selectedId), + [masterData, selectedId], + ); + + // ─── 엑셀 다운로드 ───────────────────────────────────── + + const handleExcel = async () => { + await exportToExcel(masterData, "검사결과관리.xlsx", "검사결과"); + }; + + // ─── 판정 배지 ────────────────────────────────────────── + + const judgmentBadge = (v: string) => { + if (!v) return null; + const lower = v.toLowerCase(); + if (["합격", "pass", "적합"].includes(lower)) + return {v}; + if (["불합격", "fail", "부적합"].includes(lower)) + return {v}; + return {v}; + }; + + // ─── 날짜 포맷 ────────────────────────────────────────── + + const fmtDate = (d: string) => { + if (!d) return "-"; + try { + return new Date(d).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }); + } catch { + return d; + } + }; + + // ─── 렌더링 ───────────────────────────────────────────── + + return ( +
+ {/* 검색 필터 */} +
+ { + setFilterValues(filters); + }} + dataCount={masterData.length} + /> +
+ + {/* 헤더 */} +
+
+ +

검사관리

+ {masterData.length}건 +
+
+ + +
+
+ + {/* 분할 패널 */} + + {/* 좌측: 마스터 테이블 */} + +
+ {loading && masterData.length === 0 ? ( +
+ + 데이터를 불러오는 중... +
+ ) : masterData.length === 0 ? ( +
+ + 검사 데이터가 없습니다 +
+ ) : ( + + + + # + 검사번호 + 검사유형 + 품목코드 + 품목명 + 검사수량 + 양품 + 불량 + 판정 + 검사자 + 검사일자 + 완료 + 거래처 + + + + {masterData.map((row, idx) => ( + setSelectedId(row.id)} + > + {idx + 1} + {row.inspection_number || "-"} + + {row.inspection_type || "-"} + + {row.item_code || "-"} + {row.item_name || "-"} + {row.total_qty ?? "-"} + {row.good_qty ?? "-"} + {row.bad_qty ?? "-"} + {judgmentBadge(row.overall_judgment)} + {row.inspector || "-"} + {fmtDate(row.inspection_date)} + + {row.is_completed === "Y" ? ( + 완료 + ) : ( + 진행중 + )} + + {row.supplier_name || "-"} + + ))} + +
+ )} +
+
+ + + + {/* 우측: 디테일 */} + + {!selectedId ? ( +
+ +

좌측에서 검사를 선택해주세요

+
+ ) : ( +
+ {/* 상세 헤더 */} +
+
+

검사 상세

+ {selectedMaster && ( + + {selectedMaster.inspection_number} + + )} +
+ {selectedMaster && ( +
+ {selectedMaster.item_name} + · + {judgmentBadge(selectedMaster.overall_judgment)} +
+ )} +
+ + {/* 상세 테이블 */} +
+ {detailLoading ? ( +
+ +
+ ) : detailData.length === 0 ? ( +
+ +

검사 항목이 없습니다

+
+ ) : ( + + + + # + 검사항목 + 검사기준 + 합격기준 + 측정값 + 판정 + 필수 + 비고 + + + + {detailData.map((d, idx) => ( + + {idx + 1} + {d.inspection_item_name || "-"} + {d.inspection_standard || "-"} + {d.pass_criteria || "-"} + {d.measured_value || "-"} + {judgmentBadge(d.judgment)} + + {d.is_required === "Y" ? ( + 필수 + ) : ( + - + )} + + {d.memo || "-"} + + ))} + +
+ )} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/components/layout/AdminPageRenderer.tsx b/frontend/components/layout/AdminPageRenderer.tsx index 6dbd56b6..c28e83bf 100644 --- a/frontend/components/layout/AdminPageRenderer.tsx +++ b/frontend/components/layout/AdminPageRenderer.tsx @@ -110,6 +110,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_7/production/plan-management": dynamic(() => import("@/app/(main)/COMPANY_7/production/plan-management/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/production/bom": dynamic(() => import("@/app/(main)/COMPANY_7/production/bom/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/equipment/info": dynamic(() => import("@/app/(main)/COMPANY_7/equipment/info/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_7/equipment/inspection-record": dynamic(() => import("@/app/(main)/COMPANY_7/equipment/inspection-record/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/equipment/plc-settings": dynamic(() => import("@/app/(main)/COMPANY_7/equipment/plc-settings/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/logistics/outbound": dynamic(() => import("@/app/(main)/COMPANY_7/logistics/outbound/page"), { ssr: false, loading: LoadingFallback }), @@ -144,6 +145,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_16/production/plan-management": dynamic(() => import("@/app/(main)/COMPANY_16/production/plan-management/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/production/bom": dynamic(() => import("@/app/(main)/COMPANY_16/production/bom/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/equipment/info": dynamic(() => import("@/app/(main)/COMPANY_16/equipment/info/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_16/equipment/inspection-record": dynamic(() => import("@/app/(main)/COMPANY_16/equipment/inspection-record/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/equipment/plc-settings": dynamic(() => import("@/app/(main)/COMPANY_16/equipment/plc-settings/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/monitoring/production": dynamic(() => import("@/app/(main)/COMPANY_16/monitoring/production/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/monitoring/equipment": dynamic(() => import("@/app/(main)/COMPANY_16/monitoring/equipment/page"), { ssr: false, loading: LoadingFallback }), @@ -157,6 +159,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_7/purchase/purchase-item": dynamic(() => import("@/app/(main)/COMPANY_7/purchase/purchase-item/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/purchase/supplier": dynamic(() => import("@/app/(main)/COMPANY_7/purchase/supplier/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/quality/inspection": dynamic(() => import("@/app/(main)/COMPANY_7/quality/inspection/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_7/quality/inspection-result": dynamic(() => import("@/app/(main)/COMPANY_7/quality/inspection-result/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/quality/item-inspection": dynamic(() => import("@/app/(main)/COMPANY_7/quality/item-inspection/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/mold/info": dynamic(() => import("@/app/(main)/COMPANY_7/mold/info/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_16/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }), @@ -173,6 +176,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_16/purchase/purchase-item": dynamic(() => import("@/app/(main)/COMPANY_16/purchase/purchase-item/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/purchase/supplier": dynamic(() => import("@/app/(main)/COMPANY_16/purchase/supplier/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/quality/inspection": dynamic(() => import("@/app/(main)/COMPANY_16/quality/inspection/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_16/quality/inspection-result": dynamic(() => import("@/app/(main)/COMPANY_16/quality/inspection-result/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/quality/item-inspection": dynamic(() => import("@/app/(main)/COMPANY_16/quality/item-inspection/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/mold/info": dynamic(() => import("@/app/(main)/COMPANY_16/mold/info/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/design/project": dynamic(() => import("@/app/(main)/COMPANY_16/design/project/page"), { ssr: false, loading: LoadingFallback }), @@ -198,6 +202,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_8/production/plan-management": dynamic(() => import("@/app/(main)/COMPANY_8/production/plan-management/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/production/bom": dynamic(() => import("@/app/(main)/COMPANY_8/production/bom/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/equipment/info": dynamic(() => import("@/app/(main)/COMPANY_8/equipment/info/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_8/equipment/inspection-record": dynamic(() => import("@/app/(main)/COMPANY_8/equipment/inspection-record/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/equipment/plc-settings": dynamic(() => import("@/app/(main)/COMPANY_8/equipment/plc-settings/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/monitoring/production": dynamic(() => import("@/app/(main)/COMPANY_8/monitoring/production/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/monitoring/equipment": dynamic(() => import("@/app/(main)/COMPANY_8/monitoring/equipment/page"), { ssr: false, loading: LoadingFallback }), @@ -217,6 +222,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_8/purchase/purchase-item": dynamic(() => import("@/app/(main)/COMPANY_8/purchase/purchase-item/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/purchase/supplier": dynamic(() => import("@/app/(main)/COMPANY_8/purchase/supplier/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/quality/inspection": dynamic(() => import("@/app/(main)/COMPANY_8/quality/inspection/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_8/quality/inspection-result": dynamic(() => import("@/app/(main)/COMPANY_8/quality/inspection-result/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/quality/item-inspection": dynamic(() => import("@/app/(main)/COMPANY_8/quality/item-inspection/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/mold/info": dynamic(() => import("@/app/(main)/COMPANY_8/mold/info/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/design/project": dynamic(() => import("@/app/(main)/COMPANY_8/design/project/page"), { ssr: false, loading: LoadingFallback }), @@ -242,6 +248,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_10/production/plan-management": dynamic(() => import("@/app/(main)/COMPANY_10/production/plan-management/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/production/bom": dynamic(() => import("@/app/(main)/COMPANY_10/production/bom/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/equipment/info": dynamic(() => import("@/app/(main)/COMPANY_10/equipment/info/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_10/equipment/inspection-record": dynamic(() => import("@/app/(main)/COMPANY_10/equipment/inspection-record/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/equipment/plc-settings": dynamic(() => import("@/app/(main)/COMPANY_10/equipment/plc-settings/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/monitoring/production": dynamic(() => import("@/app/(main)/COMPANY_10/monitoring/production/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/monitoring/equipment": dynamic(() => import("@/app/(main)/COMPANY_10/monitoring/equipment/page"), { ssr: false, loading: LoadingFallback }), @@ -261,6 +268,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_10/purchase/purchase-item": dynamic(() => import("@/app/(main)/COMPANY_10/purchase/purchase-item/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/purchase/supplier": dynamic(() => import("@/app/(main)/COMPANY_10/purchase/supplier/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/quality/inspection": dynamic(() => import("@/app/(main)/COMPANY_10/quality/inspection/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_10/quality/inspection-result": dynamic(() => import("@/app/(main)/COMPANY_10/quality/inspection-result/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/quality/item-inspection": dynamic(() => import("@/app/(main)/COMPANY_10/quality/item-inspection/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/mold/info": dynamic(() => import("@/app/(main)/COMPANY_10/mold/info/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/design/project": dynamic(() => import("@/app/(main)/COMPANY_10/design/project/page"), { ssr: false, loading: LoadingFallback }), @@ -286,6 +294,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_29/production/plan-management": dynamic(() => import("@/app/(main)/COMPANY_29/production/plan-management/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/production/bom": dynamic(() => import("@/app/(main)/COMPANY_29/production/bom/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/equipment/info": dynamic(() => import("@/app/(main)/COMPANY_29/equipment/info/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_29/equipment/inspection-record": dynamic(() => import("@/app/(main)/COMPANY_29/equipment/inspection-record/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/equipment/plc-settings": dynamic(() => import("@/app/(main)/COMPANY_29/equipment/plc-settings/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/monitoring/production": dynamic(() => import("@/app/(main)/COMPANY_29/monitoring/production/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/monitoring/equipment": dynamic(() => import("@/app/(main)/COMPANY_29/monitoring/equipment/page"), { ssr: false, loading: LoadingFallback }), @@ -305,6 +314,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_29/purchase/purchase-item": dynamic(() => import("@/app/(main)/COMPANY_29/purchase/purchase-item/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/purchase/supplier": dynamic(() => import("@/app/(main)/COMPANY_29/purchase/supplier/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/quality/inspection": dynamic(() => import("@/app/(main)/COMPANY_29/quality/inspection/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_29/quality/inspection-result": dynamic(() => import("@/app/(main)/COMPANY_29/quality/inspection-result/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/quality/item-inspection": dynamic(() => import("@/app/(main)/COMPANY_29/quality/item-inspection/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/mold/info": dynamic(() => import("@/app/(main)/COMPANY_29/mold/info/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/design/project": dynamic(() => import("@/app/(main)/COMPANY_29/design/project/page"), { ssr: false, loading: LoadingFallback }), @@ -330,6 +340,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_9/production/plan-management": dynamic(() => import("@/app/(main)/COMPANY_9/production/plan-management/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/production/bom": dynamic(() => import("@/app/(main)/COMPANY_9/production/bom/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/equipment/info": dynamic(() => import("@/app/(main)/COMPANY_9/equipment/info/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_9/equipment/inspection-record": dynamic(() => import("@/app/(main)/COMPANY_9/equipment/inspection-record/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/equipment/plc-settings": dynamic(() => import("@/app/(main)/COMPANY_9/equipment/plc-settings/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/monitoring/production": dynamic(() => import("@/app/(main)/COMPANY_9/monitoring/production/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/monitoring/equipment": dynamic(() => import("@/app/(main)/COMPANY_9/monitoring/equipment/page"), { ssr: false, loading: LoadingFallback }), @@ -349,6 +360,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_9/purchase/purchase-item": dynamic(() => import("@/app/(main)/COMPANY_9/purchase/purchase-item/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/purchase/supplier": dynamic(() => import("@/app/(main)/COMPANY_9/purchase/supplier/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/quality/inspection": dynamic(() => import("@/app/(main)/COMPANY_9/quality/inspection/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_9/quality/inspection-result": dynamic(() => import("@/app/(main)/COMPANY_9/quality/inspection-result/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/quality/item-inspection": dynamic(() => import("@/app/(main)/COMPANY_9/quality/item-inspection/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/mold/info": dynamic(() => import("@/app/(main)/COMPANY_9/mold/info/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/design/project": dynamic(() => import("@/app/(main)/COMPANY_9/design/project/page"), { ssr: false, loading: LoadingFallback }), @@ -375,6 +387,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_30/production/plan-management": dynamic(() => import("@/app/(main)/COMPANY_30/production/plan-management/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/production/bom": dynamic(() => import("@/app/(main)/COMPANY_30/production/bom/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/equipment/info": dynamic(() => import("@/app/(main)/COMPANY_30/equipment/info/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_30/equipment/inspection-record": dynamic(() => import("@/app/(main)/COMPANY_30/equipment/inspection-record/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/equipment/plc-settings": dynamic(() => import("@/app/(main)/COMPANY_30/equipment/plc-settings/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/monitoring/production": dynamic(() => import("@/app/(main)/COMPANY_30/monitoring/production/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/monitoring/equipment": dynamic(() => import("@/app/(main)/COMPANY_30/monitoring/equipment/page"), { ssr: false, loading: LoadingFallback }), @@ -394,6 +407,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_30/purchase/purchase-item": dynamic(() => import("@/app/(main)/COMPANY_30/purchase/purchase-item/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/purchase/supplier": dynamic(() => import("@/app/(main)/COMPANY_30/purchase/supplier/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/quality/inspection": dynamic(() => import("@/app/(main)/COMPANY_30/quality/inspection/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_30/quality/inspection-result": dynamic(() => import("@/app/(main)/COMPANY_30/quality/inspection-result/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/quality/item-inspection": dynamic(() => import("@/app/(main)/COMPANY_30/quality/item-inspection/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/mold/info": dynamic(() => import("@/app/(main)/COMPANY_30/mold/info/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/design/project": dynamic(() => import("@/app/(main)/COMPANY_30/design/project/page"), { ssr: false, loading: LoadingFallback }),