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.
This commit is contained in:
@@ -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<string, string> = {
|
||||
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<string | null>(null);
|
||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||
|
||||
// 채번 시스템
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
const [previewCode, setPreviewCode] = useState<string | null>(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 (
|
||||
<Input
|
||||
value={previewCode || ""}
|
||||
readOnly
|
||||
placeholder="채번 조회 중..."
|
||||
className="h-9 text-sm bg-muted text-muted-foreground"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
value={value}
|
||||
@@ -631,7 +696,7 @@ export default function LogisticsInfoPage() {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[formData, editMode, carrierOptions, routeOptions, categoryOptions, updateFormField]
|
||||
[formData, editMode, carrierOptions, routeOptions, categoryOptions, updateFormField, activeConfig, numberingRuleId, previewCode]
|
||||
);
|
||||
|
||||
// ========== 렌더링 ==========
|
||||
|
||||
@@ -65,6 +65,7 @@ import { useTableSettings } from "@/hooks/useTableSettings";
|
||||
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
|
||||
|
||||
const WAREHOUSE_TABLE = "warehouse_info";
|
||||
|
||||
@@ -139,6 +140,12 @@ export default function WarehouseManagementPage() {
|
||||
const [locationForm, setLocationForm] = useState<Record<string, any>>({});
|
||||
const [locationSaving, setLocationSaving] = useState(false);
|
||||
|
||||
// 위치코드 자동 채번
|
||||
const [locNumberingRuleId, setLocNumberingRuleId] = useState<string | null>(null);
|
||||
const [locPreviewCode, setLocPreviewCode] = useState<string | null>(null);
|
||||
const [whNumberingRuleId, setWhNumberingRuleId] = useState<string | null>(null);
|
||||
const [whPreviewCode, setWhPreviewCode] = useState<string | null>(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() {
|
||||
창고코드 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
value={warehouseForm.warehouse_code || ""}
|
||||
value={warehouseEditMode ? (warehouseForm.warehouse_code || "") : (whPreviewCode || "")}
|
||||
onChange={(e) =>
|
||||
setWarehouseForm((prev) => ({ ...prev, warehouse_code: e.target.value }))
|
||||
}
|
||||
placeholder="창고코드를 입력해주세요"
|
||||
placeholder={warehouseEditMode ? "" : (whNumberingRuleId ? "채번 조회 중..." : "자동 생성돼요")}
|
||||
disabled={warehouseEditMode}
|
||||
readOnly={!warehouseEditMode && !!whNumberingRuleId}
|
||||
/>
|
||||
</div>
|
||||
{/* 창고명 */}
|
||||
@@ -1097,12 +1156,13 @@ export default function WarehouseManagementPage() {
|
||||
위치코드 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
value={locationForm.location_code || ""}
|
||||
value={locationEditMode ? (locationForm.location_code || "") : (locPreviewCode || "")}
|
||||
onChange={(e) =>
|
||||
setLocationForm((prev) => ({ ...prev, location_code: e.target.value }))
|
||||
}
|
||||
placeholder="위치코드를 입력해주세요"
|
||||
placeholder={locationEditMode ? "" : (locNumberingRuleId ? "채번 조회 중..." : "자동 생성돼요")}
|
||||
disabled={locationEditMode}
|
||||
readOnly={!locationEditMode && !!locNumberingRuleId}
|
||||
/>
|
||||
</div>
|
||||
{/* 위치명 */}
|
||||
|
||||
@@ -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<Record<string, any>>({});
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
const [previewCode, setPreviewCode] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null);
|
||||
const moldImageRef = React.useRef<HTMLInputElement>(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() {
|
||||
<Label className="text-xs">금형코드 <span className="text-destructive">*</span></Label>
|
||||
<Input
|
||||
className="h-9 text-sm"
|
||||
value={moldForm.mold_code || ""}
|
||||
onChange={(e) => 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 ? "" : "자동 생성 중...") : "금형코드를 입력해주세요")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
|
||||
@@ -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<Record<string, string>>({});
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// 채번 시스템
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
const [previewCode, setPreviewCode] = useState<string | null>(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() {
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">외주업체 코드</Label>
|
||||
<Input
|
||||
value={subcontractorForm.subcontractor_code || ""}
|
||||
onChange={(e) => 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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [previewCode, setPreviewCode] = useState<string | null>(null);
|
||||
|
||||
/* ───── 카테고리 옵션 ───── */
|
||||
const [catOptions, setCatOptions] = useState<Record<string, { code: string; label: string }[]>>({});
|
||||
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() {
|
||||
<Label className="text-xs font-semibold">
|
||||
검사코드 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
className="h-9"
|
||||
value={inspForm.inspection_code || ""}
|
||||
onChange={(e) => setInspForm((p) => ({ ...p, inspection_code: e.target.value }))}
|
||||
placeholder="검사코드 입력"
|
||||
/>
|
||||
{!inspEditMode && numberingRuleId ? (
|
||||
<Input
|
||||
className="h-9 bg-muted"
|
||||
value={previewCode || ""}
|
||||
readOnly
|
||||
placeholder="채번 조회 중..."
|
||||
/>
|
||||
) : inspEditMode ? (
|
||||
<Input
|
||||
className="h-9 bg-muted"
|
||||
value={inspForm.inspection_code || ""}
|
||||
disabled
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
className="h-9"
|
||||
value={inspForm.inspection_code || ""}
|
||||
onChange={(e) => setInspForm((p) => ({ ...p, inspection_code: e.target.value }))}
|
||||
placeholder="검사코드 입력"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* 유형 (다중선택) */}
|
||||
<div className="space-y-1.5">
|
||||
@@ -1238,12 +1345,27 @@ export default function InspectionManagementPage() {
|
||||
<Label className="text-xs font-semibold">
|
||||
불량코드 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
className="h-9"
|
||||
value={defForm.defect_code || ""}
|
||||
onChange={(e) => setDefForm((p) => ({ ...p, defect_code: e.target.value }))}
|
||||
placeholder="불량코드 입력"
|
||||
/>
|
||||
{!defEditMode && numberingRuleId ? (
|
||||
<Input
|
||||
className="h-9 bg-muted"
|
||||
value={previewCode || ""}
|
||||
readOnly
|
||||
placeholder="채번 조회 중..."
|
||||
/>
|
||||
) : defEditMode ? (
|
||||
<Input
|
||||
className="h-9 bg-muted"
|
||||
value={defForm.defect_code || ""}
|
||||
disabled
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
className="h-9"
|
||||
value={defForm.defect_code || ""}
|
||||
onChange={(e) => setDefForm((p) => ({ ...p, defect_code: e.target.value }))}
|
||||
placeholder="불량코드 입력"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* 불량유형 */}
|
||||
<div className="space-y-1.5">
|
||||
@@ -1462,13 +1584,27 @@ export default function InspectionManagementPage() {
|
||||
<Label className="text-xs font-semibold">
|
||||
장비코드 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
className="h-9"
|
||||
value={eqForm.equipment_code || ""}
|
||||
onChange={(e) => setEqForm((p) => ({ ...p, equipment_code: e.target.value }))}
|
||||
placeholder="장비코드"
|
||||
disabled={eqEditMode}
|
||||
/>
|
||||
{!eqEditMode && numberingRuleId ? (
|
||||
<Input
|
||||
className="h-9 bg-muted"
|
||||
value={previewCode || ""}
|
||||
readOnly
|
||||
placeholder="채번 조회 중..."
|
||||
/>
|
||||
) : eqEditMode ? (
|
||||
<Input
|
||||
className="h-9 bg-muted"
|
||||
value={eqForm.equipment_code || ""}
|
||||
disabled
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
className="h-9"
|
||||
value={eqForm.equipment_code || ""}
|
||||
onChange={(e) => setEqForm((p) => ({ ...p, equipment_code: e.target.value }))}
|
||||
placeholder="장비코드"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-semibold">
|
||||
|
||||
Reference in New Issue
Block a user