diff --git a/backend-node/package-lock.json b/backend-node/package-lock.json index 8fba4591..60c4615f 100644 --- a/backend-node/package-lock.json +++ b/backend-node/package-lock.json @@ -39,7 +39,6 @@ "nodemailer": "^6.10.1", "oracledb": "^6.9.0", "pg": "^8.16.3", - "playwright": "^1.58.2", "quill": "^2.0.3", "react-quill": "^2.0.0", "redis": "^4.6.10", @@ -1051,6 +1050,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -2384,6 +2384,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -3501,6 +3502,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3746,6 +3748,7 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -3974,6 +3977,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4523,6 +4527,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -5898,6 +5903,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6176,6 +6182,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -7762,6 +7769,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -8731,7 +8739,6 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", - "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -9701,6 +9708,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -9920,50 +9928,6 @@ "node": ">=8" } }, - "node_modules/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.58.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -10668,7 +10632,6 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -11562,6 +11525,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -11667,6 +11631,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/backend-node/package.json b/backend-node/package.json index 8154371b..e827da0c 100644 --- a/backend-node/package.json +++ b/backend-node/package.json @@ -53,7 +53,6 @@ "nodemailer": "^6.10.1", "oracledb": "^6.9.0", "pg": "^8.16.3", - "playwright": "^1.58.2", "quill": "^2.0.3", "react-quill": "^2.0.0", "redis": "^4.6.10", diff --git a/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx b/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx index be480fcc..cacd9d02 100644 --- a/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -9,31 +9,51 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Checkbox } from "@/components/ui/checkbox"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; import { - Plus, Trash2, Save, Loader2, Pencil, - ClipboardCheck, AlertTriangle, Wrench, Search, Inbox, Settings2, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { + Plus, + Trash2, + Save, + Loader2, + Pencil, + ClipboardCheck, + AlertTriangle, + Wrench, + Search, + Inbox, + Settings2, } from "lucide-react"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; +import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; +import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; /* ───── 테이블명 ───── */ const INSPECTION_TABLE = "inspection_standard"; const INSPECTION_COLUMNS = [ + { key: "inspection_code", label: "검사코드" }, { key: "inspection_type", label: "검사유형" }, - { key: "inspection_standard", label: "검사기준" }, - { key: "inspection_item_name", label: "검사항목명" }, + { key: "inspection_criteria", label: "검사기준" }, + { key: "inspection_item", label: "검사항목" }, { key: "inspection_method", label: "검사방법" }, + { key: "judgment_criteria", label: "판단기준" }, { key: "unit", label: "단위" }, - { key: "apply_type", label: "적용유형" }, - { key: "is_active", label: "사용여부" }, + { key: "apply_type", label: "적용구분" }, + { key: "manager", label: "관리자" }, ]; const DEFECT_TABLE = "defect_standard_mng"; const EQUIPMENT_TABLE = "inspection_equipment_mng"; @@ -88,8 +108,13 @@ export default function InspectionManagementPage() { const [eqSaving, setEqSaving] = useState(false); const [eqKeyword, setEqKeyword] = useState(""); + /* ───── 채번 ───── */ + const [numberingRuleId, setNumberingRuleId] = useState(null); + const [previewCode, setPreviewCode] = useState(null); + /* ───── 카테고리 옵션 ───── */ const [catOptions, setCatOptions] = useState>({}); + const [userOptions, setUserOptions] = useState<{ code: string; label: string }[]>([]); /* ═══════════════════ 카테고리 로드 ═══════════════════ */ useEffect(() => { @@ -98,7 +123,14 @@ export default function InspectionManagementPage() { const catList = [ { table: INSPECTION_TABLE, col: "inspection_type" }, { table: INSPECTION_TABLE, col: "apply_type" }, + { table: INSPECTION_TABLE, col: "inspection_method" }, + { table: INSPECTION_TABLE, col: "judgment_criteria" }, + { table: INSPECTION_TABLE, col: "unit" }, { table: DEFECT_TABLE, col: "defect_type" }, + { table: DEFECT_TABLE, col: "severity" }, + { table: DEFECT_TABLE, col: "inspection_type" }, + { table: DEFECT_TABLE, col: "is_active" }, + { table: EQUIPMENT_TABLE, col: "equipment_type" }, { table: EQUIPMENT_TABLE, col: "equipment_status" }, ]; await Promise.all( @@ -108,27 +140,73 @@ export default function InspectionManagementPage() { if (res.data?.data?.length > 0) { optMap[`${table}.${col}`] = flattenCategories(res.data.data); } - } catch { /* skip */ } - }) + } catch { + /* skip */ + } + }), ); setCatOptions(optMap); + // 사용자 목록 로드 + try { + const userRes = await apiClient.post(`/table-management/tables/user_info/data`, { + page: 1, + size: 500, + autoFilter: true, + }); + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; + setUserOptions( + users.map((u: any) => ({ + code: u.user_id || u.id, + label: `${u.user_name || u.name || u.user_id}${u.dept_name ? ` (${u.dept_name})` : ""}`, + })), + ); + } catch { + /* skip */ + } }; load(); }, []); const getCatLabel = (table: string, col: string, code: string) => { + if (!code) return ""; const opts = catOptions[`${table}.${col}`]; if (!opts) return code; - return opts.find(o => o.code === code)?.label || code; + // 쉼표 구분 다중 코드 지원 + if (code.includes(",")) { + return code + .split(",") + .filter(Boolean) + .map((c) => opts.find((o) => o.code === c)?.label || c) + .join(", "); + } + return opts.find((o) => o.code === code)?.label || code; }; + const inspTableColumns = useMemo(() => { + return ts.visibleColumns.map((col) => { + const base: EDataTableColumn = { key: col.key, label: col.label }; + if (["inspection_type", "inspection_method", "judgment_criteria", "unit", "apply_type"].includes(col.key)) { + base.render = (v: any, row: any) => getCatLabel(INSPECTION_TABLE, col.key, row[col.key]); + } + return base; + }); + }, [ts.visibleColumns, catOptions]); // eslint-disable-line react-hooks/exhaustive-deps + /* ═══════════════════ 데이터 조회 ═══════════════════ */ + // 다중값 컬럼 (쉼표 구분 저장) — 서버 equals 대신 contains 사용 + const MULTI_VALUE_COLUMNS = ["inspection_type"]; + const fetchInspections = useCallback(async () => { setInspLoading(true); try { - const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); + const filters = searchFilters.map((f) => ({ + columnName: f.columnName, + operator: MULTI_VALUE_COLUMNS.includes(f.columnName) ? "contains" : f.operator, + value: f.value, + })); const res = await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/data`, { - page: 1, size: 500, + page: 1, + size: 500, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); @@ -146,7 +224,9 @@ export default function InspectionManagementPage() { setDefLoading(true); try { const res = await apiClient.post(`/table-management/tables/${DEFECT_TABLE}/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, + size: 500, + autoFilter: true, }); const rows = res.data?.data?.data || res.data?.data?.rows || []; setDefects(rows); @@ -162,7 +242,9 @@ export default function InspectionManagementPage() { setEqLoading(true); try { const res = await apiClient.post(`/table-management/tables/${EQUIPMENT_TABLE}/data`, { - page: 1, size: 500, autoFilter: true, + page: 1, + size: 500, + autoFilter: true, }); const rows = res.data?.data?.data || res.data?.data?.rows || []; setEquipments(rows); @@ -174,121 +256,344 @@ export default function InspectionManagementPage() { } }, []); - useEffect(() => { fetchInspections(); }, [fetchInspections]); - useEffect(() => { fetchDefects(); fetchEquipments(); }, []); + useEffect(() => { + fetchInspections(); + }, [fetchInspections]); + useEffect(() => { + fetchDefects(); + fetchEquipments(); + }, []); /* ───── 클라이언트 필터 ───── */ const filteredDefects = defKeyword.trim() - ? defects.filter(r => (r.defect_name || "").toLowerCase().includes(defKeyword.toLowerCase()) || (r.defect_type || "").toLowerCase().includes(defKeyword.toLowerCase())) + ? defects.filter( + (r) => + (r.defect_name || "").toLowerCase().includes(defKeyword.toLowerCase()) || + (r.defect_type || "").toLowerCase().includes(defKeyword.toLowerCase()), + ) : defects; const filteredEquipments = eqKeyword.trim() - ? equipments.filter(r => (r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) || (r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase())) + ? equipments.filter( + (r) => + (r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) || + (r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()), + ) : equipments; /* ═══════════════════ 검사기준 CRUD ═══════════════════ */ - const openInspCreate = () => { setInspForm({}); setInspEditMode(false); setInspModalOpen(true); }; - const openInspEdit = (row: any) => { setInspForm({ ...row }); setInspEditMode(true); setInspModalOpen(true); }; + const openInspCreate = async () => { + setInspForm({}); + setInspEditMode(false); + setNumberingRuleId(null); + setPreviewCode(null); + setInspModalOpen(true); + try { + const ruleRes = await apiClient.get(`/numbering-rules/by-column/${INSPECTION_TABLE}/inspection_code`); + const ruleData = ruleRes.data; + if (ruleData?.success && ruleData?.data?.ruleId) { + const ruleId = ruleData.data.ruleId; + setNumberingRuleId(ruleId); + const prev = await previewNumberingCode(ruleId); + if (prev.success && prev.data?.generatedCode) { + setPreviewCode(prev.data.generatedCode); + } + } + } catch { /* 채번 규칙 없으면 무시 */ } + }; + const openInspEdit = (row: any) => { + setInspForm({ ...row }); + setInspEditMode(true); + setInspModalOpen(true); + }; const saveInspection = async () => { - if (!inspForm.inspection_standard) { toast.error("검사기준은 필수 입력이에요"); return; } + if (!numberingRuleId && !inspForm.inspection_code) { + toast.error("검사코드는 필수예요"); + return; + } + if (!inspForm.inspection_type) { + toast.error("유형을 1개 이상 선택해주세요"); + return; + } + if (!inspForm.inspection_criteria) { + toast.error("검사기준은 필수예요"); + return; + } + if (!inspForm.inspection_item) { + toast.error("검사항목은 필수예요"); + return; + } + if (!inspForm.judgment_criteria) { + toast.error("판단기준은 필수예요"); + return; + } setInspSaving(true); try { + let finalCode = inspForm.inspection_code || ""; + if (!inspEditMode && numberingRuleId) { + const allocRes = await allocateNumberingCode(numberingRuleId); + if (allocRes.success && allocRes.data?.generatedCode) { + finalCode = allocRes.data.generatedCode; + } else { + toast.error("채번 코드 할당에 실패했습니다."); + setInspSaving(false); + return; + } + } if (inspEditMode) { await apiClient.put(`/table-management/tables/${INSPECTION_TABLE}/edit`, { - originalData: { id: inspForm.id }, updatedData: inspForm, + originalData: { id: inspForm.id }, + updatedData: inspForm, }); toast.success("검사기준을 수정했어요"); } else { - await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/add`, { id: crypto.randomUUID(), ...inspForm }); + await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/add`, { + id: crypto.randomUUID(), + ...inspForm, + inspection_code: finalCode, + }); toast.success("검사기준을 등록했어요"); } setInspModalOpen(false); fetchInspections(); - } catch { toast.error("저장에 실패했어요"); } - finally { setInspSaving(false); } + } catch { + toast.error("저장에 실패했어요"); + } finally { + setInspSaving(false); + } }; const deleteInspections = async () => { - if (inspChecked.length === 0) { toast.error("삭제할 항목을 선택해주세요"); return; } - const ok = await confirm("검사기준 삭제", { description: `선택한 ${inspChecked.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.` }); + if (inspChecked.length === 0) { + toast.error("삭제할 항목을 선택해주세요"); + return; + } + const ok = await confirm("검사기준 삭제", { + description: `선택한 ${inspChecked.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.`, + }); if (!ok) return; try { await apiClient.delete(`/table-management/tables/${INSPECTION_TABLE}/delete`, { - data: inspChecked.map(id => ({ id })), + data: inspChecked.map((id) => ({ id })), }); toast.success(`${inspChecked.length}건을 삭제했어요`); setInspChecked([]); fetchInspections(); - } catch { toast.error("삭제에 실패했어요"); } + } catch { + toast.error("삭제에 실패했어요"); + } }; /* ═══════════════════ 불량관리 CRUD ═══════════════════ */ - const openDefCreate = () => { setDefForm({}); setDefEditMode(false); setDefModalOpen(true); }; - const openDefEdit = (row: any) => { setDefForm({ ...row }); setDefEditMode(true); setDefModalOpen(true); }; + const openDefCreate = async () => { + setDefForm({}); + setDefEditMode(false); + setNumberingRuleId(null); + setPreviewCode(null); + setDefModalOpen(true); + try { + const ruleRes = await apiClient.get(`/numbering-rules/by-column/${DEFECT_TABLE}/defect_code`); + const ruleData = ruleRes.data; + if (ruleData?.success && ruleData?.data?.ruleId) { + const ruleId = ruleData.data.ruleId; + setNumberingRuleId(ruleId); + const prev = await previewNumberingCode(ruleId); + if (prev.success && prev.data?.generatedCode) { + setPreviewCode(prev.data.generatedCode); + } + } + } catch { /* 채번 규칙 없으면 무시 */ } + }; + const openDefEdit = (row: any) => { + setDefForm({ ...row }); + setDefEditMode(true); + setDefModalOpen(true); + }; const saveDefect = async () => { - if (!defForm.defect_name) { toast.error("불량명은 필수 입력이에요"); return; } + if (!numberingRuleId && !defForm.defect_code) { + toast.error("불량코드는 필수예요"); + return; + } + if (!defForm.defect_type) { + toast.error("불량유형은 필수예요"); + return; + } + if (!defForm.defect_name) { + toast.error("불량명은 필수예요"); + return; + } + if (!defForm.severity) { + toast.error("심각도는 필수예요"); + return; + } + if (!defForm.defect_content) { + toast.error("불량내용은 필수예요"); + return; + } + if (!defForm.inspection_type) { + toast.error("검사유형을 1개 이상 선택해주세요"); + return; + } setDefSaving(true); try { + let finalCode = defForm.defect_code || ""; + if (!defEditMode && numberingRuleId) { + const allocRes = await allocateNumberingCode(numberingRuleId); + if (allocRes.success && allocRes.data?.generatedCode) { + finalCode = allocRes.data.generatedCode; + } else { + toast.error("채번 코드 할당에 실패했습니다."); + setDefSaving(false); + return; + } + } if (defEditMode) { await apiClient.put(`/table-management/tables/${DEFECT_TABLE}/edit`, { - originalData: { id: defForm.id }, updatedData: defForm, + originalData: { id: defForm.id }, + updatedData: defForm, }); toast.success("불량유형을 수정했어요"); } else { - await apiClient.post(`/table-management/tables/${DEFECT_TABLE}/add`, { id: crypto.randomUUID(), ...defForm }); + await apiClient.post(`/table-management/tables/${DEFECT_TABLE}/add`, { id: crypto.randomUUID(), ...defForm, defect_code: finalCode }); toast.success("불량유형을 등록했어요"); } setDefModalOpen(false); fetchDefects(); - } catch { toast.error("저장에 실패했어요"); } - finally { setDefSaving(false); } + } catch { + toast.error("저장에 실패했어요"); + } finally { + setDefSaving(false); + } }; const deleteDefects = async () => { - if (defChecked.length === 0) { toast.error("삭제할 항목을 선택해주세요"); return; } - const ok = await confirm("불량유형 삭제", { description: `선택한 ${defChecked.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.` }); + if (defChecked.length === 0) { + toast.error("삭제할 항목을 선택해주세요"); + return; + } + const ok = await confirm("불량유형 삭제", { + description: `선택한 ${defChecked.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.`, + }); if (!ok) return; try { await apiClient.delete(`/table-management/tables/${DEFECT_TABLE}/delete`, { - data: defChecked.map(id => ({ id })), + data: defChecked.map((id) => ({ id })), }); toast.success(`${defChecked.length}건을 삭제했어요`); setDefChecked([]); fetchDefects(); - } catch { toast.error("삭제에 실패했어요"); } + } catch { + toast.error("삭제에 실패했어요"); + } }; /* ═══════════════════ 검사장비 CRUD ═══════════════════ */ - const openEqCreate = () => { setEqForm({}); setEqEditMode(false); setEqModalOpen(true); }; - const openEqEdit = (row: any) => { setEqForm({ ...row }); setEqEditMode(true); setEqModalOpen(true); }; + const openEqCreate = async () => { + setEqForm({ + calibration_period: "12", + equipment_status: "NORMAL", + }); + setEqEditMode(false); + setNumberingRuleId(null); + setPreviewCode(null); + setEqModalOpen(true); + try { + const ruleRes = await apiClient.get(`/numbering-rules/by-column/${EQUIPMENT_TABLE}/equipment_code`); + const ruleData = ruleRes.data; + if (ruleData?.success && ruleData?.data?.ruleId) { + const ruleId = ruleData.data.ruleId; + setNumberingRuleId(ruleId); + const prev = await previewNumberingCode(ruleId); + if (prev.success && prev.data?.generatedCode) { + setPreviewCode(prev.data.generatedCode); + } + } else { + // 채번 규칙 없으면 기존 수동 채번 fallback + const maxNum = + equipments + .map((e: any) => e.equipment_code || "") + .filter((c: string) => /^EQP-\d+$/.test(c)) + .map((c: string) => parseInt(c.replace("EQP-", ""), 10)) + .sort((a: number, b: number) => b - a)[0] || 0; + setEqForm((p) => ({ ...p, equipment_code: `EQP-${String(maxNum + 1).padStart(3, "0")}` })); + } + } catch { + // 채번 규칙 조회 실패 시 기존 수동 채번 fallback + const maxNum = + equipments + .map((e: any) => e.equipment_code || "") + .filter((c: string) => /^EQP-\d+$/.test(c)) + .map((c: string) => parseInt(c.replace("EQP-", ""), 10)) + .sort((a: number, b: number) => b - a)[0] || 0; + setEqForm((p) => ({ ...p, equipment_code: `EQP-${String(maxNum + 1).padStart(3, "0")}` })); + } + }; + const openEqEdit = (row: any) => { + setEqForm({ ...row }); + setEqEditMode(true); + setEqModalOpen(true); + }; const saveEquipment = async () => { - if (!eqForm.equipment_name) { toast.error("장비명은 필수 입력이에요"); return; } + if (!numberingRuleId && !eqForm.equipment_code) { + toast.error("장비코드는 필수예요"); + return; + } + if (!eqForm.equipment_name) { + toast.error("장비명은 필수예요"); + return; + } + if (!eqForm.equipment_type) { + toast.error("장비유형은 필수예요"); + return; + } setEqSaving(true); try { + let finalCode = eqForm.equipment_code || ""; + if (!eqEditMode && numberingRuleId) { + const allocRes = await allocateNumberingCode(numberingRuleId); + if (allocRes.success && allocRes.data?.generatedCode) { + finalCode = allocRes.data.generatedCode; + } else { + toast.error("채번 코드 할당에 실패했습니다."); + setEqSaving(false); + return; + } + } if (eqEditMode) { await apiClient.put(`/table-management/tables/${EQUIPMENT_TABLE}/edit`, { - originalData: { id: eqForm.id }, updatedData: eqForm, + originalData: { id: eqForm.id }, + updatedData: eqForm, }); toast.success("검사장비를 수정했어요"); } else { - await apiClient.post(`/table-management/tables/${EQUIPMENT_TABLE}/add`, { id: crypto.randomUUID(), ...eqForm }); + await apiClient.post(`/table-management/tables/${EQUIPMENT_TABLE}/add`, { id: crypto.randomUUID(), ...eqForm, equipment_code: finalCode }); toast.success("검사장비를 등록했어요"); } setEqModalOpen(false); fetchEquipments(); - } catch { toast.error("저장에 실패했어요"); } - finally { setEqSaving(false); } + } catch { + toast.error("저장에 실패했어요"); + } finally { + setEqSaving(false); + } }; const deleteEquipments = async () => { - if (eqChecked.length === 0) { toast.error("삭제할 항목을 선택해주세요"); return; } - const ok = await confirm("검사장비 삭제", { description: `선택한 ${eqChecked.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.` }); + if (eqChecked.length === 0) { + toast.error("삭제할 항목을 선택해주세요"); + return; + } + const ok = await confirm("검사장비 삭제", { + description: `선택한 ${eqChecked.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.`, + }); if (!ok) return; try { await apiClient.delete(`/table-management/tables/${EQUIPMENT_TABLE}/delete`, { - data: eqChecked.map(id => ({ id })), + data: eqChecked.map((id) => ({ id })), }); toast.success(`${eqChecked.length}건을 삭제했어요`); setEqChecked([]); fetchEquipments(); - } catch { toast.error("삭제에 실패했어요"); } + } catch { + toast.error("삭제에 실패했어요"); + } }; /* ═══════════════════ JSX ═══════════════════ */ @@ -296,39 +601,45 @@ export default function InspectionManagementPage() {
{ConfirmDialogComponent} -
+
- + - + 검사기준 - {inspCount} + + {inspCount} + - + 불량관리 - {defCount} + + {defCount} + - + 검사장비 - {eqCount} + + {eqCount} +
{/* ──── 검사기준 탭 ──── */} - +
- - - + + + @@ -352,186 +676,365 @@ export default function InspectionManagementPage() { } />
-
- - - - - 0 && inspChecked.length === inspections.length} - onCheckedChange={(v) => setInspChecked(v ? inspections.map(r => r.id) : [])} - /> - - {ts.visibleColumns.map((col) => ( - {col.label} - ))} - - - - {inspLoading ? ( - - ) : inspections.length === 0 ? ( -

등록된 검사기준이 없어요

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

등록된 불량유형이 없어요

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

등록된 불량유형이 없어요

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

등록된 검사장비가 없어요

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

등록된 검사장비가 없어요

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