diff --git a/backend-node/src/controllers/popProductionController.ts b/backend-node/src/controllers/popProductionController.ts index 92cba89d..dabb2f1f 100644 --- a/backend-node/src/controllers/popProductionController.ts +++ b/backend-node/src/controllers/popProductionController.ts @@ -359,11 +359,21 @@ export const syncWorkInstructions = async ( }); // 미동기화 작업지시 조회: routing이 있지만 work_order_process가 없는 항목 + // header에 routing 없으면 detail에서 가져옴 (PC가 detail에만 저장하는 경우 대응) const unsyncedResult = await pool.query( - `SELECT wi.id, wi.work_instruction_no, wi.routing, wi.qty + `SELECT wi.id, wi.work_instruction_no, + COALESCE(wi.routing, wid.routing_version_id) AS routing, + COALESCE(NULLIF(wi.qty, ''), wid.qty) AS qty, + COALESCE(wi.item_id, (SELECT id FROM item_info WHERE item_number = wid.item_number AND company_code = $1 LIMIT 1)) AS item_id FROM work_instruction wi + LEFT JOIN LATERAL ( + SELECT routing_version_id, qty, item_number + FROM work_instruction_detail + WHERE work_instruction_no = wi.work_instruction_no AND company_code = $1 + LIMIT 1 + ) wid ON true WHERE wi.company_code = $1 - AND wi.routing IS NOT NULL + AND COALESCE(wi.routing, wid.routing_version_id) IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM work_order_process wop WHERE wop.wo_id = wi.id AND wop.company_code = $1 @@ -373,6 +383,20 @@ export const syncWorkInstructions = async ( const unsynced = unsyncedResult.rows; + // header에 routing/qty/item_id가 비어있으면 자동 보정 (detail → header 동기화) + for (const wi of unsynced) { + await pool.query( + `UPDATE work_instruction SET + routing = COALESCE(routing, $2), + qty = COALESCE(NULLIF(qty, ''), $3), + item_id = COALESCE(item_id, $4), + updated_date = NOW() + WHERE id = $1 AND company_code = $5 + AND (routing IS NULL OR qty IS NULL OR qty = '' OR item_id IS NULL)`, + [wi.id, wi.routing, wi.qty, wi.item_id, companyCode], + ); + } + if (unsynced.length === 0) { return res.json({ success: true, diff --git a/backend-node/src/controllers/receivingController.ts b/backend-node/src/controllers/receivingController.ts index ef732ace..c57cd7c0 100644 --- a/backend-node/src/controllers/receivingController.ts +++ b/backend-node/src/controllers/receivingController.ts @@ -1050,7 +1050,7 @@ export async function getWarehouses(req: AuthenticatedRequest, res: Response) { const result = await pool.query( `SELECT warehouse_code, warehouse_name, warehouse_type FROM warehouse_info - WHERE company_code = $1 AND status != '삭제' + WHERE company_code = $1 AND COALESCE(status, '') != '삭제' ORDER BY warehouse_name`, [companyCode], ); 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..40a1521a --- /dev/null +++ b/frontend/app/(main)/COMPANY_10/equipment/inspection-record/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, 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 [categoryMap, setCategoryMap] = useState>>({}); + 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); + + // 카테고리 코드→라벨 매핑 (점검주기, 점검방법) + try { + const catCols = ["inspection_cycle", "inspection_method"]; + const catResults = await Promise.all( + catCols.map((col) => + apiClient.get(`/table-categories/equipment_inspection_item/${col}/values`).catch(() => ({ data: [] })), + ), + ); + const cMap: Record> = {}; + catCols.forEach((col, idx) => { + const vals: { code: string; label: string }[] = catResults[idx].data?.data ?? catResults[idx].data ?? []; + cMap[col] = {}; + vals.forEach((v: any) => { cMap[col][v.valueCode || v.code] = v.valueLabel || v.label; }); + }); + setCategoryMap(cMap); + } catch { + // 카테고리 조회 실패 무시 + } + } 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 resolveCategory = (col: string, code: string) => { + if (!code) return "-"; + return categoryMap[col]?.[code] || 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 ?
{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..40a1521a --- /dev/null +++ b/frontend/app/(main)/COMPANY_16/equipment/inspection-record/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, 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 [categoryMap, setCategoryMap] = useState>>({}); + 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); + + // 카테고리 코드→라벨 매핑 (점검주기, 점검방법) + try { + const catCols = ["inspection_cycle", "inspection_method"]; + const catResults = await Promise.all( + catCols.map((col) => + apiClient.get(`/table-categories/equipment_inspection_item/${col}/values`).catch(() => ({ data: [] })), + ), + ); + const cMap: Record> = {}; + catCols.forEach((col, idx) => { + const vals: { code: string; label: string }[] = catResults[idx].data?.data ?? catResults[idx].data ?? []; + cMap[col] = {}; + vals.forEach((v: any) => { cMap[col][v.valueCode || v.code] = v.valueLabel || v.label; }); + }); + setCategoryMap(cMap); + } catch { + // 카테고리 조회 실패 무시 + } + } 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 resolveCategory = (col: string, code: string) => { + if (!code) return "-"; + return categoryMap[col]?.[code] || 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 ?
{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..40a1521a --- /dev/null +++ b/frontend/app/(main)/COMPANY_29/equipment/inspection-record/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, 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 [categoryMap, setCategoryMap] = useState>>({}); + 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); + + // 카테고리 코드→라벨 매핑 (점검주기, 점검방법) + try { + const catCols = ["inspection_cycle", "inspection_method"]; + const catResults = await Promise.all( + catCols.map((col) => + apiClient.get(`/table-categories/equipment_inspection_item/${col}/values`).catch(() => ({ data: [] })), + ), + ); + const cMap: Record> = {}; + catCols.forEach((col, idx) => { + const vals: { code: string; label: string }[] = catResults[idx].data?.data ?? catResults[idx].data ?? []; + cMap[col] = {}; + vals.forEach((v: any) => { cMap[col][v.valueCode || v.code] = v.valueLabel || v.label; }); + }); + setCategoryMap(cMap); + } catch { + // 카테고리 조회 실패 무시 + } + } 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 resolveCategory = (col: string, code: string) => { + if (!code) return "-"; + return categoryMap[col]?.[code] || 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 ?
{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..40a1521a --- /dev/null +++ b/frontend/app/(main)/COMPANY_30/equipment/inspection-record/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, 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 [categoryMap, setCategoryMap] = useState>>({}); + 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); + + // 카테고리 코드→라벨 매핑 (점검주기, 점검방법) + try { + const catCols = ["inspection_cycle", "inspection_method"]; + const catResults = await Promise.all( + catCols.map((col) => + apiClient.get(`/table-categories/equipment_inspection_item/${col}/values`).catch(() => ({ data: [] })), + ), + ); + const cMap: Record> = {}; + catCols.forEach((col, idx) => { + const vals: { code: string; label: string }[] = catResults[idx].data?.data ?? catResults[idx].data ?? []; + cMap[col] = {}; + vals.forEach((v: any) => { cMap[col][v.valueCode || v.code] = v.valueLabel || v.label; }); + }); + setCategoryMap(cMap); + } catch { + // 카테고리 조회 실패 무시 + } + } 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 resolveCategory = (col: string, code: string) => { + if (!code) return "-"; + return categoryMap[col]?.[code] || 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 ?
{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..40a1521a --- /dev/null +++ b/frontend/app/(main)/COMPANY_7/equipment/inspection-record/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, 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 [categoryMap, setCategoryMap] = useState>>({}); + 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); + + // 카테고리 코드→라벨 매핑 (점검주기, 점검방법) + try { + const catCols = ["inspection_cycle", "inspection_method"]; + const catResults = await Promise.all( + catCols.map((col) => + apiClient.get(`/table-categories/equipment_inspection_item/${col}/values`).catch(() => ({ data: [] })), + ), + ); + const cMap: Record> = {}; + catCols.forEach((col, idx) => { + const vals: { code: string; label: string }[] = catResults[idx].data?.data ?? catResults[idx].data ?? []; + cMap[col] = {}; + vals.forEach((v: any) => { cMap[col][v.valueCode || v.code] = v.valueLabel || v.label; }); + }); + setCategoryMap(cMap); + } catch { + // 카테고리 조회 실패 무시 + } + } 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 resolveCategory = (col: string, code: string) => { + if (!code) return "-"; + return categoryMap[col]?.[code] || 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 ?
{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..40a1521a --- /dev/null +++ b/frontend/app/(main)/COMPANY_8/equipment/inspection-record/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, 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 [categoryMap, setCategoryMap] = useState>>({}); + 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); + + // 카테고리 코드→라벨 매핑 (점검주기, 점검방법) + try { + const catCols = ["inspection_cycle", "inspection_method"]; + const catResults = await Promise.all( + catCols.map((col) => + apiClient.get(`/table-categories/equipment_inspection_item/${col}/values`).catch(() => ({ data: [] })), + ), + ); + const cMap: Record> = {}; + catCols.forEach((col, idx) => { + const vals: { code: string; label: string }[] = catResults[idx].data?.data ?? catResults[idx].data ?? []; + cMap[col] = {}; + vals.forEach((v: any) => { cMap[col][v.valueCode || v.code] = v.valueLabel || v.label; }); + }); + setCategoryMap(cMap); + } catch { + // 카테고리 조회 실패 무시 + } + } 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 resolveCategory = (col: string, code: string) => { + if (!code) return "-"; + return categoryMap[col]?.[code] || 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 ?
{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..40a1521a --- /dev/null +++ b/frontend/app/(main)/COMPANY_9/equipment/inspection-record/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, 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 [categoryMap, setCategoryMap] = useState>>({}); + 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); + + // 카테고리 코드→라벨 매핑 (점검주기, 점검방법) + try { + const catCols = ["inspection_cycle", "inspection_method"]; + const catResults = await Promise.all( + catCols.map((col) => + apiClient.get(`/table-categories/equipment_inspection_item/${col}/values`).catch(() => ({ data: [] })), + ), + ); + const cMap: Record> = {}; + catCols.forEach((col, idx) => { + const vals: { code: string; label: string }[] = catResults[idx].data?.data ?? catResults[idx].data ?? []; + cMap[col] = {}; + vals.forEach((v: any) => { cMap[col][v.valueCode || v.code] = v.valueLabel || v.label; }); + }); + setCategoryMap(cMap); + } catch { + // 카테고리 조회 실패 무시 + } + } 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 resolveCategory = (col: string, code: string) => { + if (!code) return "-"; + return categoryMap[col]?.[code] || 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 ?
{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/app/(pop)/pop/equipment/inspection/page.tsx b/frontend/app/(pop)/pop/equipment/inspection/page.tsx new file mode 100644 index 00000000..3c7a5444 --- /dev/null +++ b/frontend/app/(pop)/pop/equipment/inspection/page.tsx @@ -0,0 +1,6 @@ +"use client"; +import { EquipmentInspection } from "@/components/pop/hardcoded/equipment/EquipmentInspection"; + +export default function EquipmentInspectionPage() { + return ; +} diff --git a/frontend/app/(pop)/pop/equipment/management/page.tsx b/frontend/app/(pop)/pop/equipment/management/page.tsx new file mode 100644 index 00000000..90abb5ef --- /dev/null +++ b/frontend/app/(pop)/pop/equipment/management/page.tsx @@ -0,0 +1,6 @@ +"use client"; +import { EquipmentList } from "@/components/pop/hardcoded/equipment/EquipmentList"; + +export default function EquipmentManagementPage() { + return ; +} diff --git a/frontend/app/(pop)/pop/equipment/page.tsx b/frontend/app/(pop)/pop/equipment/page.tsx new file mode 100644 index 00000000..421afbc8 --- /dev/null +++ b/frontend/app/(pop)/pop/equipment/page.tsx @@ -0,0 +1,6 @@ +"use client"; +import { EquipmentHome } from "@/components/pop/hardcoded/equipment/EquipmentHome"; + +export default function EquipmentPage() { + return ; +} diff --git a/frontend/app/(pop)/pop/inventory/move/page.tsx b/frontend/app/(pop)/pop/inventory/move/page.tsx new file mode 100644 index 00000000..d47b06ef --- /dev/null +++ b/frontend/app/(pop)/pop/inventory/move/page.tsx @@ -0,0 +1,6 @@ +"use client"; +import { InventoryMove } from "@/components/pop/hardcoded/inventory/InventoryMove"; + +export default function InventoryMovePage() { + return ; +} diff --git a/frontend/app/(pop)/pop/inventory/transfer/page.tsx b/frontend/app/(pop)/pop/inventory/transfer/page.tsx new file mode 100644 index 00000000..c58b92db --- /dev/null +++ b/frontend/app/(pop)/pop/inventory/transfer/page.tsx @@ -0,0 +1,6 @@ +"use client"; +import { InventoryTransfer } from "@/components/pop/hardcoded/inventory/InventoryTransfer"; + +export default function TransferPage() { + return ; +} diff --git a/frontend/components/layout/AdminPageRenderer.tsx b/frontend/components/layout/AdminPageRenderer.tsx index ab7b6473..e47509a9 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 }), diff --git a/frontend/components/pop/hardcoded/MenuIcons.tsx b/frontend/components/pop/hardcoded/MenuIcons.tsx index 4d2f93e2..66ddc7a5 100644 --- a/frontend/components/pop/hardcoded/MenuIcons.tsx +++ b/frontend/components/pop/hardcoded/MenuIcons.tsx @@ -126,7 +126,7 @@ const MENU_ITEMS: MenuIconItem[] = [ /> ), - href: "/pop/screens/equipment", + href: "/pop/equipment", }, { id: "inventory", diff --git a/frontend/components/pop/hardcoded/equipment/EquipmentHome.tsx b/frontend/components/pop/hardcoded/equipment/EquipmentHome.tsx new file mode 100644 index 00000000..bfba61fe --- /dev/null +++ b/frontend/components/pop/hardcoded/equipment/EquipmentHome.tsx @@ -0,0 +1,198 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import React, { useEffect, useState } from "react"; +import { apiClient } from "@/lib/api/client"; + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +interface EquipmentItem { + id: string; + equipment_code: string; + equipment_name: string; + equipment_type?: string; + status?: string; +} + +interface KpiData { + total: number; + active: number; + idle: number; + inspect: number; + rate: string; +} + +/* ------------------------------------------------------------------ */ +/* Menu */ +/* ------------------------------------------------------------------ */ + +const MENU_ITEMS = [ + { + id: "management", + title: "설비관리", + gradient: "linear-gradient(135deg,#8b5cf6,#6d28d9)", + shadowColor: "rgba(139,92,246,.3)", + icon: ( + + + + ), + href: "/pop/equipment/management", + }, + { + id: "inspection", + title: "설비점검", + gradient: "linear-gradient(135deg,#f59e0b,#d97706)", + shadowColor: "rgba(245,158,11,.3)", + icon: ( + + + + ), + href: "/pop/equipment/inspection", + }, +]; + +/* ------------------------------------------------------------------ */ +/* Component */ +/* ------------------------------------------------------------------ */ + +export function EquipmentHome() { + const router = useRouter(); + + const [kpi, setKpi] = useState({ + total: 0, + active: 0, + idle: 0, + inspect: 0, + rate: "0%", + }); + const [recentItems, setRecentItems] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + setLoading(true); + try { + const res = await apiClient.get("/data/equipment_mng", { + params: { pageSize: 500 }, + }); + const data = res.data?.data?.data ?? res.data?.data ?? []; + const items = Array.isArray(data) ? data : []; + const total = items.length; + const active = items.filter((i: EquipmentItem) => !i.status || i.status === "가동" || i.status === "정상").length; + const idle = items.filter((i: EquipmentItem) => i.status === "대기").length; + const inspect = items.filter((i: EquipmentItem) => i.status === "점검").length; + const rate = total > 0 ? `${Math.round((active / total) * 100)}%` : "0%"; + setKpi({ total, active, idle, inspect, rate }); + setRecentItems(items.slice(0, 5)); + } catch { /* */ } + setLoading(false); + }; + fetchData(); + }, []); + + return ( +
+ {/* Header */} +
+
+ +
+

설비

+

설비 현황 및 점검 관리

+
+
+ + {/* KPI */} +
+
+ {[ + { value: loading ? "-" : kpi.total, label: "전체", color: "text-gray-900" }, + { value: loading ? "-" : kpi.active, label: "가동", color: "text-green-600" }, + { value: loading ? "-" : kpi.idle, label: "대기", color: "text-blue-600" }, + { value: loading ? "-" : kpi.inspect, label: "점검", color: "text-red-600" }, + { value: loading ? "-" : kpi.rate, label: "가동률", color: "text-purple-600" }, + ].map((item) => ( +
+

{item.value}

+

{item.label}

+
+ ))} +
+
+
+ + {/* Menu Icons */} +
+

+ + 설비 관리 +

+
+ {MENU_ITEMS.map((item) => ( + + ))} +
+
+ + {/* Recent Equipment */} +
+
+
+

최근 설비

+ 최근 5건 +
+ {loading ? ( +
+
+
+ ) : recentItems.length === 0 ? ( +
등록된 설비가 없습니다
+ ) : ( +
+ {recentItems.map((item) => ( +
+
+

{item.equipment_name || "-"}

+

{item.equipment_code} {item.equipment_type ? `· ${item.equipment_type}` : ""}

+
+ + {item.status || "정상"} + +
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/frontend/components/pop/hardcoded/equipment/EquipmentInspection.tsx b/frontend/components/pop/hardcoded/equipment/EquipmentInspection.tsx new file mode 100644 index 00000000..a78374b4 --- /dev/null +++ b/frontend/components/pop/hardcoded/equipment/EquipmentInspection.tsx @@ -0,0 +1,119 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { PopShell } from "../PopShell"; + +type TabType = "all" | "running" | "idle" | "inspect" | "stopped"; + +export function EquipmentInspection() { + const router = useRouter(); + const [activeTab, setActiveTab] = useState("all"); + const [searchKeyword, setSearchKeyword] = useState(""); + + // 시안 기준 KPI (점검 테이블 없으므로 0) + const kpi = { total: 0, running: 0, idle: 0, inspect: 0, rate: "0%" }; + + const tabs: { key: TabType; label: string; count: number }[] = [ + { key: "all", label: "전체", count: kpi.total }, + { key: "running", label: "가동", count: kpi.running }, + { key: "idle", label: "대기", count: kpi.idle }, + { key: "inspect", label: "점검", count: kpi.inspect }, + { key: "stopped", label: "비가동", count: 0 }, + ]; + + return ( + +
+ {/* Header */} +
+
+ +
+

🔧 점검관리

+
+
+ + {/* Search */} +
+ setSearchKeyword(e.target.value)} + className="flex-1 px-4 py-3 rounded-xl border border-gray-200 text-sm focus:outline-none focus:border-amber-400" + /> + + +
+
+ + {/* KPI */} +
+
+ {[ + { label: "전체", value: kpi.total, color: "border-t-amber-500", icon: "🎬" }, + { label: "가동", value: kpi.running, color: "border-t-green-500", icon: "🟢" }, + { label: "대기", value: kpi.idle, color: "border-t-blue-500", icon: "🔵" }, + { label: "점검", value: kpi.inspect, color: "border-t-red-500", icon: "🔴" }, + { label: "가동률", value: kpi.rate, color: "border-t-purple-500", icon: "📊" }, + ].map((item) => ( +
+

{item.icon}

+

{item.value}

+

{item.label}

+
+ ))} +
+
+ + {/* Tabs */} +
+
+ {tabs.map((tab) => ( + + ))} +
+
+ + {/* Card List — 데이터 없음 */} +
+
+ 🔧 +

등록된 설비 점검 정보가 없습니다

+

PC에서 설비 점검 데이터를 등록하면 여기에 표시됩니다

+
+
+
+
+ ); +} diff --git a/frontend/components/pop/hardcoded/equipment/EquipmentList.tsx b/frontend/components/pop/hardcoded/equipment/EquipmentList.tsx new file mode 100644 index 00000000..9de9b20b --- /dev/null +++ b/frontend/components/pop/hardcoded/equipment/EquipmentList.tsx @@ -0,0 +1,169 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { apiClient } from "@/lib/api/client"; +import { PopShell } from "../PopShell"; + +interface EquipmentItem { + id: string; + equipment_code: string; + equipment_name: string; + equipment_type?: string; + location?: string; + status?: string; + last_inspection_date?: string; + next_inspection_date?: string; + memo?: string; +} + +export function EquipmentList() { + const router = useRouter(); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [searchKeyword, setSearchKeyword] = useState(""); + + const fetchEquipments = useCallback(async () => { + setLoading(true); + try { + const res = await apiClient.get("/data/equipment_mng", { + params: { pageSize: 500 }, + }); + const data = res.data?.data?.data ?? res.data?.data ?? []; + setItems(Array.isArray(data) ? data : []); + } catch { + setItems([]); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchEquipments(); + }, [fetchEquipments]); + + const filtered = items.filter((item) => { + if (!searchKeyword) return true; + const kw = searchKeyword.toLowerCase(); + return ( + (item.equipment_code || "").toLowerCase().includes(kw) || + (item.equipment_name || "").toLowerCase().includes(kw) || + (item.equipment_type || "").toLowerCase().includes(kw) + ); + }); + + // KPI + const totalCount = items.length; + const activeCount = items.filter((i) => i.status === "가동" || i.status === "정상" || !i.status).length; + const stopCount = items.filter((i) => i.status === "정지" || i.status === "비가동").length; + + return ( + +
+ {/* Header */} +
+
+ +
+

설비관리

+

설비 현황을 조회합니다

+
+
+ + {/* Search */} +
+ + + + setSearchKeyword(e.target.value)} + className="w-full pl-10 pr-4 py-3 rounded-xl border border-gray-200 text-sm focus:outline-none focus:border-blue-400" + /> +
+
+ + {/* KPI */} +
+
+
+

전체

+

{totalCount}

+
+
+

가동

+

{activeCount}

+
+
+

비가동

+

{stopCount}

+
+
+
+ + {/* List */} +
+
+

설비 목록

+ {filtered.length}건 +
+ + {loading ? ( +
+
+
+ ) : filtered.length === 0 ? ( +
+ + + +

등록된 설비가 없습니다

+
+ ) : ( +
+ {filtered.map((item) => ( +
+
+
+

{item.equipment_name || "-"}

+

{item.equipment_code}

+
+ + {item.status || "정상"} + +
+
+ {item.equipment_type && ( +
유형: {item.equipment_type}
+ )} + {item.location && ( +
위치: {item.location}
+ )} +
+
+ ))} +
+ )} +
+
+ + ); +} diff --git a/frontend/components/pop/hardcoded/inventory/InventoryHome.tsx b/frontend/components/pop/hardcoded/inventory/InventoryHome.tsx index bf61e374..aa6d0ac9 100644 --- a/frontend/components/pop/hardcoded/inventory/InventoryHome.tsx +++ b/frontend/components/pop/hardcoded/inventory/InventoryHome.tsx @@ -95,7 +95,29 @@ const MENU_ITEMS = [ /> ), - href: "#", + href: "/pop/inventory/transfer", + }, + { + id: "move", + title: "재고이동", + gradient: "linear-gradient(135deg,#10b981,#059669)", + shadowColor: "rgba(16,185,129,.3)", + icon: ( + + + + ), + href: "/pop/inventory/move", }, ]; diff --git a/frontend/components/pop/hardcoded/inventory/InventoryMove.tsx b/frontend/components/pop/hardcoded/inventory/InventoryMove.tsx new file mode 100644 index 00000000..247076d9 --- /dev/null +++ b/frontend/components/pop/hardcoded/inventory/InventoryMove.tsx @@ -0,0 +1,289 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { apiClient } from "@/lib/api/client"; +import { PopShell } from "../PopShell"; + +interface Warehouse { + id: string; + warehouse_code: string; + warehouse_name: string; +} + +interface StockItem { + id: string; + item_code: string; + item_name?: string; + warehouse_code: string; + location_code?: string; + current_qty: string; +} + +interface PendingItem { + stock: StockItem; + moveQty: number; + toWarehouse: string; +} + +export function InventoryMove() { + const router = useRouter(); + const [warehouses, setWarehouses] = useState([]); + const [fromWarehouse, setFromWarehouse] = useState(""); + const [toWarehouse, setToWarehouse] = useState(""); + const [stockItems, setStockItems] = useState([]); + const [loading, setLoading] = useState(false); + const [searchKeyword, setSearchKeyword] = useState(""); + const [pendingItems, setPendingItems] = useState([]); + + const fetchWarehouses = useCallback(async () => { + try { + const res = await apiClient.get("/outbound/warehouses"); + setWarehouses(res.data?.data || []); + } catch { /* */ } + }, []); + + const fetchStock = useCallback(async () => { + if (!fromWarehouse) { setStockItems([]); return; } + setLoading(true); + try { + const res = await apiClient.get("/data/inventory_stock", { + params: { pageSize: "500", filters: JSON.stringify({ warehouse_code: fromWarehouse }) }, + }); + const data = res.data?.data?.data ?? res.data?.data ?? []; + setStockItems(Array.isArray(data) ? data.filter((d: StockItem) => parseFloat(d.current_qty || "0") > 0) : []); + } catch { + setStockItems([]); + } finally { + setLoading(false); + } + }, [fromWarehouse]); + + useEffect(() => { fetchWarehouses(); }, [fetchWarehouses]); + useEffect(() => { fetchStock(); }, [fetchStock]); + + const filtered = stockItems.filter((item) => { + if (!searchKeyword) return true; + const kw = searchKeyword.toLowerCase(); + return (item.item_code || "").toLowerCase().includes(kw) || (item.item_name || "").toLowerCase().includes(kw); + }); + + const addToPending = (stock: StockItem) => { + if (!toWarehouse) { alert("도착 창고를 먼저 선택하세요."); return; } + if (pendingItems.find((p) => p.stock.id === stock.id)) return; + const qty = parseFloat(stock.current_qty || "0"); + setPendingItems((prev) => [...prev, { stock, moveQty: qty, toWarehouse }]); + }; + + const removePending = (id: string) => { + setPendingItems((prev) => prev.filter((p) => p.stock.id !== id)); + }; + + const fromWh = warehouses.find((w) => w.warehouse_code === fromWarehouse); + const toWh = warehouses.find((w) => w.warehouse_code === toWarehouse); + + return ( + +
+ {/* Header */} +
+ +
+

📦 재고 이동

+

창고 간 재고를 이동합니다

+
+
+ + {/* 좌우 분할 */} +
+ {/* ===== 왼쪽: 출발 창고 + 품목 선택 ===== */} +
+ {/* 출발 창고 헤더 */} +
+
+ 📤 출발 창고 + FROM +
+
+ {warehouses.map((wh) => ( + + ))} +
+
+ + {/* 검색 */} + {fromWarehouse && ( +
+
+ setSearchKeyword(e.target.value)} + className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 text-sm focus:outline-none focus:border-blue-400" + /> + +
+
+ )} + + {/* 품목 리스트 */} +
+ {!fromWarehouse ? ( +
+ 📦 +

출발 창고를 선택하세요

+
+ ) : loading ? ( +
+
+
+ ) : filtered.length === 0 ? ( +
+

해당 창고에 재고가 없습니다

+
+ ) : ( +
+ {filtered.map((item) => { + const isPending = pendingItems.some((p) => p.stock.id === item.id); + return ( + + ); + })} +
+ )} +
+
+ + {/* ===== 오른쪽: 도착 창고 + 이동 대기열 ===== */} +
+ {/* 도착 창고 헤더 */} +
+
+ 📥 도착 창고 + TO +
+
+ {warehouses + .filter((wh) => wh.warehouse_code !== fromWarehouse) + .map((wh) => ( + + ))} +
+
+ + {/* 이동 방향 표시 */} + {fromWh && toWh && ( +
+ {fromWh.warehouse_name} + + {toWh.warehouse_name} +
+ )} + + {/* 이동 대기 목록 */} +
+ {pendingItems.length === 0 ? ( +
+ 📋 +

왼쪽에서 품목을 선택하세요

+

선택한 품목이 여기에 표시됩니다

+
+ ) : ( +
+ {pendingItems.map((p) => ( +
+
+

{p.stock.item_name || p.stock.item_code}

+ +
+

{p.stock.item_code}

+
+ + {p.moveQty.toLocaleString()} EA + + + {p.stock.warehouse_code} → {p.toWarehouse} + +
+
+ ))} +
+ )} +
+ + {/* 하단 확정 바 */} +
+
+ 이동 대기: {pendingItems.length}건 +
+ +
+
+
+
+ + ); +} diff --git a/frontend/components/pop/hardcoded/inventory/InventoryTransfer.tsx b/frontend/components/pop/hardcoded/inventory/InventoryTransfer.tsx new file mode 100644 index 00000000..427b36ae --- /dev/null +++ b/frontend/components/pop/hardcoded/inventory/InventoryTransfer.tsx @@ -0,0 +1,252 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { apiClient } from "@/lib/api/client"; +import { PopShell } from "../PopShell"; + +interface Warehouse { + id: string; + warehouse_code: string; + warehouse_name: string; + warehouse_type?: string; +} + +interface StockItem { + id: string; + item_code: string; + item_name?: string; + warehouse_code: string; + location_code?: string; + current_qty: string; + unit?: string; +} + +interface SelectedItem { + stock: StockItem; + adjustQty: string; + type: "confirm" | "adjust"; +} + +export function InventoryTransfer() { + const router = useRouter(); + const [warehouses, setWarehouses] = useState([]); + const [selectedWarehouse, setSelectedWarehouse] = useState("all"); + const [stockItems, setStockItems] = useState([]); + const [loading, setLoading] = useState(true); + const [searchKeyword, setSearchKeyword] = useState(""); + const [selectedItems, setSelectedItems] = useState([]); + + const fetchWarehouses = useCallback(async () => { + try { + const res = await apiClient.get("/outbound/warehouses"); + setWarehouses(res.data?.data || []); + } catch { /* */ } + }, []); + + const fetchStock = useCallback(async () => { + setLoading(true); + try { + const params: Record = { pageSize: "500" }; + if (selectedWarehouse !== "all") { + params.filters = JSON.stringify({ warehouse_code: selectedWarehouse }); + } + const res = await apiClient.get("/data/inventory_stock", { params }); + const data = res.data?.data?.data ?? res.data?.data ?? []; + setStockItems(Array.isArray(data) ? data.filter((d: StockItem) => parseFloat(d.current_qty || "0") > 0) : []); + } catch { + setStockItems([]); + } finally { + setLoading(false); + } + }, [selectedWarehouse]); + + useEffect(() => { fetchWarehouses(); }, [fetchWarehouses]); + useEffect(() => { fetchStock(); }, [fetchStock]); + + const filtered = stockItems.filter((item) => { + if (!searchKeyword) return true; + const kw = searchKeyword.toLowerCase(); + return (item.item_code || "").toLowerCase().includes(kw) || (item.item_name || "").toLowerCase().includes(kw); + }); + + const addItem = (stock: StockItem) => { + if (selectedItems.find((s) => s.stock.id === stock.id)) return; + setSelectedItems((prev) => [...prev, { stock, adjustQty: "", type: "confirm" }]); + }; + + const removeItem = (id: string) => { + setSelectedItems((prev) => prev.filter((s) => s.stock.id !== id)); + }; + + const confirmCount = selectedItems.filter((s) => s.type === "confirm").length; + const adjustCount = selectedItems.filter((s) => s.type === "adjust").length; + + return ( + +
+ {/* Header */} +
+
+ +

📦 재고조정

+
+
+ + {/* Main — 2단 레이아웃 */} +
+ {/* 왼쪽: 제품 선택 */} +
+
+
+

📦 제품 선택

+ +
+ + {/* 창고 탭 */} +
+ + {warehouses.map((wh) => ( + + ))} +
+ + {/* 검색 */} +
+ setSearchKeyword(e.target.value)} + className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 text-sm focus:outline-none focus:border-amber-400" + /> + +
+
+ + {/* 품목 리스트 */} +
+ {loading ? ( +
+
+
+ ) : filtered.length === 0 ? ( +
+ 📦 +

해당 창고에 재고가 없습니다

+
+ ) : ( +
+ {filtered.map((item) => ( +
+
+
📦
+
+

+ {item.item_name || item.item_code} + {item.item_name && ({item.item_code})} +

+

{item.warehouse_code}{item.location_code ? ` · ${item.location_code}` : ""}

+
+
+
+
+

{parseFloat(item.current_qty || "0").toLocaleString()}

+

{item.location_code || item.warehouse_code}

+
+ +
+
+ ))} +
+ )} +
+
+ + {/* 오른쪽: 처리 결과 */} +
+
+

📋 처리 결과

+ + {selectedItems.length}건 + +
+ +
+ {selectedItems.length === 0 ? ( +
+ 📋 +

제품을 스캔/선택하여 처리하세요

+
+ ) : ( +
+ {selectedItems.map((sel) => ( +
+
+

{sel.stock.item_name || sel.stock.item_code}

+ +
+

현재 재고: {parseFloat(sel.stock.current_qty || "0").toLocaleString()}

+ +
+ ))} +
+ )} +
+ + {/* Footer */} +
+
+ 확인 {confirmCount} + 조정 {adjustCount} +
+
+ + +
+
+
+
+
+ + ); +} diff --git a/frontend/components/pop/hardcoded/production/ProcessWork.tsx b/frontend/components/pop/hardcoded/production/ProcessWork.tsx index 1675aad3..748627a0 100644 --- a/frontend/components/pop/hardcoded/production/ProcessWork.tsx +++ b/frontend/components/pop/hardcoded/production/ProcessWork.tsx @@ -1208,34 +1208,45 @@ export function ProcessWork({ processId }: ProcessWorkProps) { behavior: "smooth", }); }} - className={`w-full flex items-center gap-2 px-3 py-2 text-left transition-all ${ + className={`w-full flex items-center gap-3 mx-2 mb-1.5 px-3 py-3 rounded-xl text-left transition-all ${ isSelected - ? "border-l-[3px] border-l-gray-900 bg-white" - : "border-l-[3px] border-l-transparent hover:bg-gray-50" + ? "bg-blue-50 border-2 border-blue-400 shadow-sm" + : isDone + ? "bg-green-50/50 border border-green-200" + : "bg-white border border-gray-200 hover:border-blue-300 hover:shadow-sm" }`} + style={{ width: "calc(100% - 16px)" }} > {g.title} {g.completed}/{g.total} @@ -1259,41 +1270,22 @@ export function ProcessWork({ processId }: ProcessWorkProps) { behavior: "smooth", }); }} - className={`w-full flex items-center gap-2 px-3 py-2.5 mb-2 text-left transition-all ${ + className={`w-full flex items-center gap-3 mx-2 mb-1.5 px-3 py-3 rounded-xl text-left transition-all ${ activeSection === "material" - ? "border-l-[3px] border-l-gray-900 bg-white" - : "border-l-[3px] border-l-transparent hover:bg-gray-50" + ? "bg-blue-50 border-2 border-blue-400 shadow-sm" + : "bg-white border border-gray-200 hover:border-blue-300 hover:shadow-sm" }`} + style={{ width: "calc(100% - 16px)" }} > - 📦 + 📦 자재 투입 )} -
-
- - - -
- - 실적 - -
)} @@ -2767,18 +2708,16 @@ function MaterialQtyInputRow({ }) { const [open, setOpen] = useState(false); return ( -
+
- {material.unit} setOpen(false)} @@ -2903,50 +2842,44 @@ function MaterialInputSection({ processId }: { processId: string }) { } return ( -
- {/* BOM 기준 자재 목록 */} -
-

- BOM 자재 목록 -

+
+ {/* BOM 기준 자재 목록 — 컴팩트 */} +
+
+

BOM 자재 목록

+ {bomMaterials.length}건 +
{bomMaterials.length === 0 ? (

BOM 자재 정보가 없습니다

) : ( -
- {bomMaterials.map((m) => ( -
-
-
-

- {m.child_item_name} -

-

{m.child_item_code}

-
-
-

소요량

-

- {m.required_qty} {m.unit} -

+
+
+ {bomMaterials.map((m) => ( +
+ {/* 자재명(코드) + 소요량 */} +
+ {m.child_item_name} + ({m.child_item_code}) + 소요 {m.required_qty}
+ {/* 입력 버튼 + 단위 */} + + setInputValues((prev) => ({ ...prev, [m.id]: v })) + } + /> + {m.unit}
- - setInputValues((prev) => ({ ...prev, [m.id]: v })) - } - /> -
- ))} + ))} +