From b783d3a33f9b2d415948bba80e5659be83f0b3d9 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 6 Apr 2026 09:25:22 +0900 Subject: [PATCH] feat: Implement numbering system for logistics, warehouse, mold, and inspection management - Integrated a numbering system to automatically generate codes for various entities including logistics carriers, warehouses, molds, and inspections. - Added functionality to preview generated codes based on defined numbering rules. - Enhanced modal handling to fetch and allocate numbering codes during entity creation and editing processes. - Implemented validation to ensure required fields are populated, considering the automatic code generation. These changes aim to streamline the management of logistics, warehouse, mold, and inspection processes by automating code generation and improving user experience. --- .../(main)/COMPANY_16/logistics/info/page.tsx | 75 ++++++- .../COMPANY_16/logistics/warehouse/page.tsx | 80 ++++++- .../app/(main)/COMPANY_16/mold/info/page.tsx | 59 ++++- .../outsourcing/subcontractor/page.tsx | 50 ++++- .../COMPANY_16/quality/inspection/page.tsx | 204 +++++++++++++++--- 5 files changed, 407 insertions(+), 61 deletions(-) diff --git a/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx index 29be86d7..1e60f516 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx @@ -50,6 +50,7 @@ import { apiClient } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { exportToExcel } from "@/lib/utils/excelExport"; +import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; @@ -241,6 +242,13 @@ function flattenCategories(items: any[]): { value: string; label: string }[] { return result; } +// 채번 대상 필드 매핑: tableName → 코드 필드 key +const NUMBERING_FIELD_MAP: Record = { + carrier_mng: "carrier_code", + delivery_route_mng: "route_code", + carrier_vehicle_mng: "vehicle_code", +}; + // ========== 메인 컴포넌트 ========== export default function LogisticsInfoPage() { const { user } = useAuth(); @@ -277,6 +285,10 @@ export default function LogisticsInfoPage() { const [editId, setEditId] = useState(null); const [formData, setFormData] = useState>({}); + // 채번 시스템 + const [numberingRuleId, setNumberingRuleId] = useState(null); + const [previewCode, setPreviewCode] = useState(null); + // 테이블 설정 (탭별) const tsCarrier = useTableSettings("c16-logistics-carrier", TAB_CONFIGS[0].tableName, TAB_CONFIGS[0].columns); const tsCost = useTableSettings("c16-logistics-cost", TAB_CONFIGS[1].tableName, TAB_CONFIGS[1].columns); @@ -407,12 +419,37 @@ export default function LogisticsInfoPage() { }, []); // 등록 모달 열기 - const handleOpenAdd = useCallback(() => { + const handleOpenAdd = useCallback(async () => { setEditMode(false); setEditId(null); setFormData({}); + setPreviewCode(null); + setNumberingRuleId(null); setFormOpen(true); - }, []); + + // 현재 탭의 채번 규칙 조회 + const config = TAB_CONFIGS.find((c) => c.key === activeTab); + if (!config) return; + const codeField = NUMBERING_FIELD_MAP[config.tableName]; + if (!codeField) return; // 채번 대상이 아닌 탭 + + try { + const ruleRes = await apiClient.get( + `/numbering-rules/by-column/${config.tableName}/${codeField}` + ); + const ruleData = ruleRes.data; + if (ruleData?.success && ruleData?.data?.ruleId) { + const ruleId = ruleData.data.ruleId; + setNumberingRuleId(ruleId); + const previewRes = await previewNumberingCode(ruleId); + if (previewRes.success && previewRes.data?.generatedCode) { + setPreviewCode(previewRes.data.generatedCode); + } + } + } catch { + // 채번 규칙 없으면 무시 — 사용자가 직접 입력 + } + }, [activeTab]); // 수정 모달 열기 const handleOpenEdit = useCallback((row: any) => { @@ -427,8 +464,10 @@ export default function LogisticsInfoPage() { const config = TAB_CONFIGS.find((c) => c.key === activeTab); if (!config) return; - // 필수값 검증 + // 필수값 검증 (등록 모드에서 채번 대상 코드 필드는 자동 할당이므로 스킵) + const numberingCodeField = NUMBERING_FIELD_MAP[config.tableName]; for (const field of config.formFields) { + if (!editMode && numberingRuleId && field.key === numberingCodeField) continue; if (field.required && !formData[field.key]?.toString().trim()) { toast.error(`${field.label}은(는) 필수 입력이에요.`); return; @@ -449,6 +488,18 @@ export default function LogisticsInfoPage() { }); toast.success("수정이 완료되었어요."); } else { + // 채번 규칙이 있으면 allocate로 실제 코드 할당 + const codeField = NUMBERING_FIELD_MAP[config.tableName]; + if (codeField && numberingRuleId) { + const allocRes = await allocateNumberingCode(numberingRuleId); + if (allocRes.success && allocRes.data?.generatedCode) { + saveData[codeField] = allocRes.data.generatedCode; + } else { + toast.error("채번 코드 할당에 실패했습니다."); + return; + } + } + await apiClient.post( `/table-management/tables/${config.tableName}/add`, { id: crypto.randomUUID(), ...saveData } @@ -464,7 +515,7 @@ export default function LogisticsInfoPage() { } catch (err: any) { toast.error(err?.response?.data?.message || "저장에 실패했어요."); } - }, [activeTab, editMode, editId, formData, fetchTabData, loadReferences]); + }, [activeTab, editMode, editId, formData, fetchTabData, loadReferences, numberingRuleId]); // 삭제 const handleDelete = useCallback(async () => { @@ -543,6 +594,9 @@ export default function LogisticsInfoPage() { const renderFormField = useCallback( (field: FormFieldDef) => { const value = formData[field.key] ?? ""; + // 현재 탭의 채번 대상 코드 필드인지 확인 + const numberingCodeField = NUMBERING_FIELD_MAP[activeConfig.tableName]; + const isNumberingTarget = !editMode && numberingRuleId && field.key === numberingCodeField; // 수정 모드에서 코드/번호 필드는 읽기전용 const isCodeField = editMode && @@ -551,6 +605,17 @@ export default function LogisticsInfoPage() { switch (field.type) { case "text": + // 등록 모드 + 채번 대상 필드: readOnly로 미리보기 코드 표시 + if (isNumberingTarget) { + return ( + + ); + } return ( >({}); const [locationSaving, setLocationSaving] = useState(false); + // 위치코드 자동 채번 + const [locNumberingRuleId, setLocNumberingRuleId] = useState(null); + const [locPreviewCode, setLocPreviewCode] = useState(null); + const [whNumberingRuleId, setWhNumberingRuleId] = useState(null); + const [whPreviewCode, setWhPreviewCode] = useState(null); + // 모달: 랙 구조 일괄 등록 const [rackModalOpen, setRackModalOpen] = useState(false); const [rackFloor, setRackFloor] = useState(""); @@ -314,10 +321,24 @@ export default function WarehouseManagementPage() { // ─── 창고 CRUD ─── - const openWarehouseCreateModal = () => { + const openWarehouseCreateModal = async () => { setWarehouseEditMode(false); setWarehouseForm({}); + setWhNumberingRuleId(null); + setWhPreviewCode(null); setWarehouseModalOpen(true); + try { + const ruleRes = await apiClient.get(`/numbering-rules/by-column/warehouse_info/warehouse_code`); + const ruleData = ruleRes.data; + if (ruleData?.success && ruleData?.data?.ruleId) { + const ruleId = ruleData.data.ruleId; + setWhNumberingRuleId(ruleId); + const previewRes = await previewNumberingCode(ruleId); + if (previewRes.success && previewRes.data?.generatedCode) { + setWhPreviewCode(previewRes.data.generatedCode); + } + } + } catch { /* 채번 규칙 없으면 무시 */ } }; const openWarehouseEditModal = (row: any) => { @@ -327,7 +348,7 @@ export default function WarehouseManagementPage() { }; const handleWarehouseSave = async () => { - if (!warehouseForm.warehouse_code?.trim()) { + if (!whNumberingRuleId && !warehouseForm.warehouse_code?.trim()) { toast.error("창고코드를 입력해주세요"); return; } @@ -337,8 +358,19 @@ export default function WarehouseManagementPage() { } setWarehouseSaving(true); try { + let finalWarehouseCode = warehouseForm.warehouse_code?.trim() || ""; + if (!warehouseEditMode && whNumberingRuleId) { + const allocRes = await allocateNumberingCode(whNumberingRuleId); + if (allocRes.success && allocRes.data?.generatedCode) { + finalWarehouseCode = allocRes.data.generatedCode; + } else { + toast.error("채번 코드 할당에 실패했습니다."); + setWarehouseSaving(false); + return; + } + } const fields = { - warehouse_code: warehouseForm.warehouse_code?.trim(), + warehouse_code: finalWarehouseCode, warehouse_name: warehouseForm.warehouse_name?.trim(), warehouse_type: warehouseForm.warehouse_type || "", manager: warehouseForm.manager || "", @@ -407,10 +439,24 @@ export default function WarehouseManagementPage() { // ─── 위치 CRUD ─── - const openLocationCreateModal = () => { + const openLocationCreateModal = async () => { setLocationEditMode(false); setLocationForm({ warehouse_code: selectedWarehouse?.warehouse_code || "" }); + setLocNumberingRuleId(null); + setLocPreviewCode(null); setLocationModalOpen(true); + try { + const ruleRes = await apiClient.get(`/numbering-rules/by-column/warehouse_location/location_code`); + const ruleData = ruleRes.data; + if (ruleData?.success && ruleData?.data?.ruleId) { + const ruleId = ruleData.data.ruleId; + setLocNumberingRuleId(ruleId); + const previewRes = await previewNumberingCode(ruleId); + if (previewRes.success && previewRes.data?.generatedCode) { + setLocPreviewCode(previewRes.data.generatedCode); + } + } + } catch { /* 채번 규칙 없으면 무시 */ } }; const openLocationEditModal = (row: any) => { @@ -420,7 +466,7 @@ export default function WarehouseManagementPage() { }; const handleLocationSave = async () => { - if (!locationForm.location_code?.trim()) { + if (!locNumberingRuleId && !locationForm.location_code?.trim()) { toast.error("위치코드를 입력해주세요"); return; } @@ -430,9 +476,21 @@ export default function WarehouseManagementPage() { } setLocationSaving(true); try { + let finalLocationCode = locationForm.location_code?.trim() || ""; + if (!locationEditMode && locNumberingRuleId) { + const allocRes = await allocateNumberingCode(locNumberingRuleId); + if (allocRes.success && allocRes.data?.generatedCode) { + finalLocationCode = allocRes.data.generatedCode; + } else { + toast.error("채번 코드 할당에 실패했습니다."); + setLocationSaving(false); + return; + } + } + const fields = { warehouse_code: locationForm.warehouse_code || selectedWarehouse?.warehouse_code || "", - location_code: locationForm.location_code?.trim(), + location_code: finalLocationCode, location_name: locationForm.location_name?.trim(), floor: locationForm.floor || "", zone: locationForm.zone || "", @@ -959,12 +1017,13 @@ export default function WarehouseManagementPage() { 창고코드 * setWarehouseForm((prev) => ({ ...prev, warehouse_code: e.target.value })) } - placeholder="창고코드를 입력해주세요" + placeholder={warehouseEditMode ? "" : (whNumberingRuleId ? "채번 조회 중..." : "자동 생성돼요")} disabled={warehouseEditMode} + readOnly={!warehouseEditMode && !!whNumberingRuleId} /> {/* 창고명 */} @@ -1097,12 +1156,13 @@ export default function WarehouseManagementPage() { 위치코드 * setLocationForm((prev) => ({ ...prev, location_code: e.target.value })) } - placeholder="위치코드를 입력해주세요" + placeholder={locationEditMode ? "" : (locNumberingRuleId ? "채번 조회 중..." : "자동 생성돼요")} disabled={locationEditMode} + readOnly={!locationEditMode && !!locNumberingRuleId} /> {/* 위치명 */} diff --git a/frontend/app/(main)/COMPANY_16/mold/info/page.tsx b/frontend/app/(main)/COMPANY_16/mold/info/page.tsx index 332b3fbc..dcd92eac 100644 --- a/frontend/app/(main)/COMPANY_16/mold/info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/mold/info/page.tsx @@ -25,6 +25,7 @@ import { } from "lucide-react"; 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"; @@ -98,6 +99,8 @@ export default function MoldInfoPage() { const [moldModalOpen, setMoldModalOpen] = useState(false); const [moldEditMode, setMoldEditMode] = useState(false); const [moldForm, setMoldForm] = useState>({}); + const [numberingRuleId, setNumberingRuleId] = useState(null); + const [previewCode, setPreviewCode] = useState(null); const [saving, setSaving] = useState(false); const [moldImagePreview, setMoldImagePreview] = useState(null); const moldImageRef = React.useRef(null); @@ -204,11 +207,29 @@ export default function MoldInfoPage() { }, [rightTab, selectedMoldCode, fetchSerials, fetchInspections, fetchParts]); // ─── 금형 CRUD ─── - const handleOpenRegister = () => { + const handleOpenRegister = async () => { setMoldEditMode(false); setMoldForm({}); setMoldImagePreview(null); + setNumberingRuleId(null); + setPreviewCode(null); setMoldModalOpen(true); + + // 채번 규칙 조회 (mold_mng.mold_code) + try { + const ruleRes = await apiClient.get(`/numbering-rules/by-column/mold_mng/mold_code`); + const ruleData = ruleRes.data; + if (ruleData?.success && ruleData?.data?.ruleId) { + const ruleId = ruleData.data.ruleId; + setNumberingRuleId(ruleId); + const previewRes = await previewNumberingCode(ruleId); + if (previewRes.success && previewRes.data?.generatedCode) { + setPreviewCode(previewRes.data.generatedCode); + } + } + } catch { + // 채번 규칙 없으면 무시 — 수동 입력 모드 + } }; const handleOpenEdit = () => { @@ -232,17 +253,37 @@ export default function MoldInfoPage() { }; const handleSaveMold = async () => { - if (!moldForm.mold_code || !moldForm.mold_name) { + // 등록 모드에서 채번이 없으면 수동 입력 필수 + if (!moldEditMode && !numberingRuleId && !moldForm.mold_code) { toast.error("금형코드와 금형명은 필수예요."); return; } + if (!moldForm.mold_name) { + toast.error("금형명은 필수예요."); + return; + } + if (moldEditMode && !moldForm.mold_code) { + toast.error("금형코드가 없어요."); + return; + } setSaving(true); try { if (moldEditMode) { await apiClient.put(`${API}/${moldForm.mold_code}`, moldForm); toast.success("금형이 수정되었어요."); } else { - await apiClient.post(API, moldForm); + // 채번 규칙이 있으면 allocate로 실제 코드 할당 + let saveData = { ...moldForm }; + if (numberingRuleId) { + const allocRes = await allocateNumberingCode(numberingRuleId); + if (allocRes.success && allocRes.data?.generatedCode) { + saveData.mold_code = allocRes.data.generatedCode; + } else { + toast.error("채번 코드 할당에 실패했어요."); + return; + } + } + await apiClient.post(API, saveData); toast.success("금형이 등록되었어요."); } setMoldModalOpen(false); @@ -980,10 +1021,14 @@ export default function MoldInfoPage() { setMoldForm({ ...moldForm, mold_code: e.target.value })} - readOnly={moldEditMode} - placeholder="금형코드를 입력해주세요" + value={moldEditMode ? (moldForm.mold_code || "") : (numberingRuleId ? (previewCode || "") : (moldForm.mold_code || ""))} + onChange={(e) => { + if (!moldEditMode && !numberingRuleId) { + setMoldForm({ ...moldForm, mold_code: e.target.value }); + } + }} + readOnly={moldEditMode || !!numberingRuleId} + placeholder={moldEditMode ? "" : (numberingRuleId ? (previewCode ? "" : "자동 생성 중...") : "금형코드를 입력해주세요")} />
diff --git a/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx b/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx index 82b9c7d6..24c3caee 100644 --- a/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx +++ b/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx @@ -28,6 +28,7 @@ import { MultiTableExcelUploadModal } from "@/components/common/MultiTableExcelU import { autoDetectMultiTableConfig, TableChainConfig } from "@/lib/api/multiTableExcel"; import { exportToExcel } from "@/lib/utils/excelExport"; import { validateField, validateForm, formatField } from "@/lib/utils/validation"; +import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; @@ -84,6 +85,10 @@ export default function SubcontractorManagementPage() { const [formErrors, setFormErrors] = useState>({}); const [saving, setSaving] = useState(false); + // 채번 시스템 + const [numberingRuleId, setNumberingRuleId] = useState(null); + const [previewCode, setPreviewCode] = useState(null); + // 품목 추가 모달 (1단계: 검색/선택) const [itemSelectOpen, setItemSelectOpen] = useState(false); const [itemSearchKeyword, setItemSearchKeyword] = useState(""); @@ -282,11 +287,29 @@ export default function SubcontractorManagementPage() { }, [selectedSubcontractor?.subcontractor_code, priceCategoryOptions]); // 외주업체 등록 모달 열기 - const openSubcontractorRegister = () => { + const openSubcontractorRegister = async () => { setSubcontractorForm({}); setFormErrors({}); setSubcontractorEditMode(false); + setNumberingRuleId(null); + setPreviewCode(null); setSubcontractorModalOpen(true); + + // 채번 규칙 조회 (subcontractor_mng.subcontractor_code) + try { + const ruleRes = await apiClient.get(`/numbering-rules/by-column/subcontractor_mng/subcontractor_code`); + const ruleData = ruleRes.data; + if (ruleData?.success && ruleData?.data?.ruleId) { + const ruleId = ruleData.data.ruleId; + setNumberingRuleId(ruleId); + const previewRes = await previewNumberingCode(ruleId); + if (previewRes.success && previewRes.data?.generatedCode) { + setPreviewCode(previewRes.data.generatedCode); + } + } + } catch { + // 채번 규칙 없으면 무시 + } }; const openSubcontractorEdit = () => { @@ -326,7 +349,19 @@ export default function SubcontractorManagementPage() { }); toast.success("수정되었어요"); } else { - await apiClient.post(`/table-management/tables/${SUBCONTRACTOR_TABLE}/add`, { id: crypto.randomUUID(), ...fields }); + // 채번 규칙이 있으면 allocate로 실제 코드 할당 + let allocatedCode: string | undefined; + if (numberingRuleId) { + const allocRes = await allocateNumberingCode(numberingRuleId); + if (allocRes.success && allocRes.data?.generatedCode) { + allocatedCode = allocRes.data.generatedCode; + } else { + toast.error("채번 코드 할당에 실패했습니다."); + return; + } + } + const saveFields = allocatedCode ? { ...fields, subcontractor_code: allocatedCode } : fields; + await apiClient.post(`/table-management/tables/${SUBCONTRACTOR_TABLE}/add`, { id: crypto.randomUUID(), ...saveFields }); toast.success("등록되었어요"); } setSubcontractorModalOpen(false); @@ -1003,10 +1038,15 @@ export default function SubcontractorManagementPage() {
setSubcontractorForm((p) => ({ ...p, subcontractor_code: e.target.value }))} - placeholder="외주업체 코드" + value={subcontractorEditMode ? (subcontractorForm.subcontractor_code || "") : (previewCode || subcontractorForm.subcontractor_code || "")} + onChange={(e) => { + if (!subcontractorEditMode && !numberingRuleId) { + setSubcontractorForm((p) => ({ ...p, subcontractor_code: e.target.value })); + } + }} + placeholder={numberingRuleId && !subcontractorEditMode ? "자동 생성돼요" : "외주업체 코드"} className="h-9 text-sm" + readOnly={!!numberingRuleId && !subcontractorEditMode} disabled={subcontractorEditMode} />
diff --git a/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx b/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx index 56226c40..41eab9c1 100644 --- a/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx @@ -33,6 +33,7 @@ import { 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"; @@ -106,6 +107,10 @@ 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 }[]>([]); @@ -266,10 +271,24 @@ export default function InspectionManagementPage() { : equipments; /* ═══════════════════ 검사기준 CRUD ═══════════════════ */ - const openInspCreate = () => { + 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 }); @@ -277,7 +296,7 @@ export default function InspectionManagementPage() { setInspModalOpen(true); }; const saveInspection = async () => { - if (!inspForm.inspection_code) { + if (!numberingRuleId && !inspForm.inspection_code) { toast.error("검사코드는 필수예요"); return; } @@ -299,6 +318,17 @@ export default function InspectionManagementPage() { } 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 }, @@ -309,6 +339,7 @@ export default function InspectionManagementPage() { await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/add`, { id: crypto.randomUUID(), ...inspForm, + inspection_code: finalCode, }); toast.success("검사기준을 등록했어요"); } @@ -342,10 +373,24 @@ export default function InspectionManagementPage() { }; /* ═══════════════════ 불량관리 CRUD ═══════════════════ */ - const openDefCreate = () => { + 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 }); @@ -353,7 +398,7 @@ export default function InspectionManagementPage() { setDefModalOpen(true); }; const saveDefect = async () => { - if (!defForm.defect_code) { + if (!numberingRuleId && !defForm.defect_code) { toast.error("불량코드는 필수예요"); return; } @@ -379,6 +424,17 @@ export default function InspectionManagementPage() { } 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 }, @@ -386,7 +442,7 @@ export default function InspectionManagementPage() { }); 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); @@ -419,20 +475,45 @@ export default function InspectionManagementPage() { }; /* ═══════════════════ 검사장비 CRUD ═══════════════════ */ - const openEqCreate = () => { - const maxNum = - equipments - .map((e: any) => e.equipment_code || "") - .filter((c: string) => /^EQP-\d+$/.test(c)) - .map((c: string) => parseInt(c.replace("EQP-", ""), 10)) - .sort((a: number, b: number) => b - a)[0] || 0; + const openEqCreate = async () => { setEqForm({ - equipment_code: `EQP-${String(maxNum + 1).padStart(3, "0")}`, 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 }); @@ -440,7 +521,7 @@ export default function InspectionManagementPage() { setEqModalOpen(true); }; const saveEquipment = async () => { - if (!eqForm.equipment_code) { + if (!numberingRuleId && !eqForm.equipment_code) { toast.error("장비코드는 필수예요"); return; } @@ -454,6 +535,17 @@ export default function InspectionManagementPage() { } 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 }, @@ -461,7 +553,7 @@ export default function InspectionManagementPage() { }); 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); @@ -1032,12 +1124,27 @@ export default function InspectionManagementPage() { - setInspForm((p) => ({ ...p, inspection_code: e.target.value }))} - placeholder="검사코드 입력" - /> + {!inspEditMode && numberingRuleId ? ( + + ) : inspEditMode ? ( + + ) : ( + setInspForm((p) => ({ ...p, inspection_code: e.target.value }))} + placeholder="검사코드 입력" + /> + )}
{/* 유형 (다중선택) */}
@@ -1238,12 +1345,27 @@ export default function InspectionManagementPage() { - setDefForm((p) => ({ ...p, defect_code: e.target.value }))} - placeholder="불량코드 입력" - /> + {!defEditMode && numberingRuleId ? ( + + ) : defEditMode ? ( + + ) : ( + setDefForm((p) => ({ ...p, defect_code: e.target.value }))} + placeholder="불량코드 입력" + /> + )}
{/* 불량유형 */}
@@ -1462,13 +1584,27 @@ export default function InspectionManagementPage() { - setEqForm((p) => ({ ...p, equipment_code: e.target.value }))} - placeholder="장비코드" - disabled={eqEditMode} - /> + {!eqEditMode && numberingRuleId ? ( + + ) : eqEditMode ? ( + + ) : ( + setEqForm((p) => ({ ...p, equipment_code: e.target.value }))} + placeholder="장비코드" + /> + )}