diff --git a/backend-node/src/controllers/numberingRuleController.ts b/backend-node/src/controllers/numberingRuleController.ts index 3764c3bc..2cd27c14 100644 --- a/backend-node/src/controllers/numberingRuleController.ts +++ b/backend-node/src/controllers/numberingRuleController.ts @@ -323,6 +323,9 @@ router.post( formData, manualInputValue ); + // TODO: 디버그용 임시 응답 (나중에 제거) + const { getPool } = require("../database/db"); + const dbPool = getPool(); return res.json({ success: true, data: { generatedCode: previewCode } }); } catch (error: any) { logger.error("코드 미리보기 실패", { error: error.message }); diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index 4a8be6bd..1179edbe 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -305,26 +305,26 @@ class NumberingRuleService { if (hasCompanyCode && companyCode !== "*") { sql = ` SELECT MAX( - CAST(SUBSTRING("${columnName}" FROM $1 FOR $2) AS INTEGER) + CAST(SUBSTRING("${columnName}" FROM ${seqStart} FOR ${seqLength}) AS INTEGER) ) as max_seq FROM "${tableName}" - WHERE "${columnName}" LIKE $3 - AND company_code = $4 - AND LENGTH("${columnName}") = $5 - AND SUBSTRING("${columnName}" FROM $1 FOR $2) ~ '^[0-9]+$' + WHERE "${columnName}" LIKE $1 + AND company_code = $2 + AND LENGTH("${columnName}") = $3 + AND SUBSTRING("${columnName}" FROM ${seqStart} FOR ${seqLength}) ~ '^[0-9]+$' `; - params = [seqStart, seqLength, likePattern, companyCode, prefixLen + seqLength + codeSuffix.length]; + params = [likePattern, companyCode, prefixLen + seqLength + codeSuffix.length]; } else { sql = ` SELECT MAX( - CAST(SUBSTRING("${columnName}" FROM $1 FOR $2) AS INTEGER) + CAST(SUBSTRING("${columnName}" FROM ${seqStart} FOR ${seqLength}) AS INTEGER) ) as max_seq FROM "${tableName}" - WHERE "${columnName}" LIKE $3 - AND LENGTH("${columnName}") = $4 - AND SUBSTRING("${columnName}" FROM $1 FOR $2) ~ '^[0-9]+$' + WHERE "${columnName}" LIKE $1 + AND LENGTH("${columnName}") = $2 + AND SUBSTRING("${columnName}" FROM ${seqStart} FOR ${seqLength}) ~ '^[0-9]+$' `; - params = [seqStart, seqLength, likePattern, prefixLen + seqLength + codeSuffix.length]; + params = [likePattern, prefixLen + seqLength + codeSuffix.length]; } const result = await client.query(sql, params); @@ -391,12 +391,18 @@ class NumberingRuleService { */ private async computeNonSequenceValues( rule: NumberingRuleConfig, - formData?: Record + formData?: Record, + manualValues?: string[] ): Promise { const sortedParts = [...rule.parts].sort((a: any, b: any) => a.order - b.order); + let manualIndex = 0; return Promise.all(sortedParts.map(async (part: any) => { if (part.partType === "sequence") return ""; - if (part.generationMethod === "manual") return ""; + if (part.generationMethod === "manual") { + const val = manualValues?.[manualIndex] || ""; + manualIndex++; + return val; + } const autoConfig = part.autoConfig || {}; @@ -486,7 +492,8 @@ class NumberingRuleService { companyCode: string, ruleId: string, prefixKey: string, - formData?: Record + formData?: Record, + manualValues?: string[] ): Promise { // 1. 현재 저장된 카운터 조회 const currentCounter = await this.getSequenceForPrefix( @@ -499,7 +506,7 @@ class NumberingRuleService { if (rule.tableName && rule.columnName) { try { const sortedParts = [...rule.parts].sort((a: any, b: any) => a.order - b.order); - const patternValues = await this.computeNonSequenceValues(rule, formData); + const patternValues = await this.computeNonSequenceValues(rule, formData, manualValues); const psInfo = this.buildCodePrefixSuffix(patternValues, sortedParts, rule.separator || ""); if (psInfo) { @@ -1420,7 +1427,7 @@ class NumberingRuleService { if (rule.tableName && rule.columnName) { try { const sortedPartsForPattern = [...rule.parts].sort((a: any, b: any) => a.order - b.order); - const patternValues = await this.computeNonSequenceValues(rule, formData); + const patternValues = await this.computeNonSequenceValues(rule, formData, manualValues); const psInfo = this.buildCodePrefixSuffix(patternValues, sortedPartsForPattern, rule.separator || ""); if (psInfo) { @@ -1429,6 +1436,7 @@ class NumberingRuleService { psInfo.prefix, psInfo.suffix, psInfo.seqLength, companyCode ); + if (maxFromTable > baseSeq) { logger.info("미리보기: 테이블 내 최대값이 카운터보다 높음", { ruleId, companyCode, currentSeq, maxFromTable, @@ -1578,7 +1586,7 @@ class NumberingRuleService { let allocatedSequence = 0; if (hasSequence) { allocatedSequence = await this.resolveNextSequence( - client, rule, companyCode, ruleId, prefixKey, formData + client, rule, companyCode, ruleId, prefixKey, formData, extractedManualValues ); } diff --git a/frontend/app/(main)/COMPANY_10/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_10/master-data/item-info/page.tsx index 375fd900..ad55d2b2 100644 --- a/frontend/app/(main)/COMPANY_10/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_10/master-data/item-info/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -156,7 +156,7 @@ const GRID_COLUMNS = [ ]; const FORM_FIELDS = [ - { key: "item_number", label: "품목코드", type: "text", required: true, disabled: true, placeholder: "자동 채번" }, + { key: "item_number", label: "품목코드", type: "numbering", required: true, placeholder: "자동 채번" }, { key: "item_name", label: "품명", type: "text", required: true }, { key: "division", label: "관리품목", type: "multi-category" }, { key: "type", label: "품목구분", type: "category" }, @@ -209,6 +209,123 @@ export default function ItemInfoPage() { // 선택된 행 const [selectedId, setSelectedId] = useState(null); + // 채번 관련 상태 + const [numberingRule, setNumberingRule] = useState(null); + const [numberingParts, setNumberingParts] = useState<{ value: string; isManual: boolean; separator: string }[]>([]); + const [manualInputValue, setManualInputValue] = useState(""); + const [isNumberingLoading, setIsNumberingLoading] = useState(false); + const numberingRuleIdRef = useRef(null); + + // 프리뷰 코드에서 각 파트별 표시값을 추출 + const parsePreviewIntoParts = (previewCode: string, rule: any) => { + if (!previewCode || !rule?.parts) return []; + const sorted = [...rule.parts].sort((a: any, b: any) => a.order - b.order); + const globalSep = rule.separator || ""; + + // 1단계: 각 파트의 "예상 값"과 "구분자" 산출 (sequence, category 제외) + const partMeta = sorted.map((part: any, idx: number) => { + const sep = idx < sorted.length - 1 + ? (part.separatorAfter ?? part.autoConfig?.separatorAfter ?? globalSep) + : ""; + const config = part.autoConfig || {}; + if (part.generationMethod === "manual") return { known: false, marker: "____", sep, isManual: true, partType: part.partType }; + switch (part.partType) { + case "text": return { known: true, value: config.textValue || "", sep, isManual: false, partType: "text" }; + case "number": return { known: true, value: String(config.numberValue || 1).padStart(config.numberLength || 3, "0"), sep, isManual: false, partType: "number" }; + case "date": { + const now = new Date(); + const y = String(now.getFullYear()), m = String(now.getMonth() + 1).padStart(2, "0"), d = String(now.getDate()).padStart(2, "0"); + const fmt = config.dateFormat || "YYYYMMDD"; + const map: Record = { YYYY: y, YY: y.slice(2), YYYYMM: y + m, YYMM: y.slice(2) + m, YYYYMMDD: y + m + d, YYMMDD: y.slice(2) + m + d }; + return { known: true, value: map[fmt] || y + m + d, sep, isManual: false, partType: "date" }; + } + // sequence, category: 알 수 없으므로 프리뷰 코드에서 추출 + default: return { known: false, sep, isManual: false, partType: part.partType }; + } + }); + + // 2단계: 프리뷰 코드를 앞에서부터 소비하면서 각 파트의 실제 값을 추출 + let remaining = previewCode; + const results: { value: string; isManual: boolean; separator: string }[] = []; + + for (let i = 0; i < partMeta.length; i++) { + const meta = partMeta[i]; + const nextMeta = i < partMeta.length - 1 ? partMeta[i + 1] : null; + + if (meta.isManual) { + // manual 파트: "____" 마커 찾아서 스킵 + const markerIdx = remaining.indexOf("____"); + if (markerIdx >= 0) { + remaining = remaining.substring(markerIdx + 4); + // 이 파트 뒤 구분자 소비 + if (meta.sep && remaining.startsWith(meta.sep)) { + remaining = remaining.substring(meta.sep.length); + } + } + results.push({ value: "", isManual: true, separator: meta.sep }); + continue; + } + + if (meta.known) { + // 알려진 값: 프리뷰 코드에서 해당 값을 소비 + const valIdx = remaining.indexOf(meta.value); + if (valIdx >= 0) { + remaining = remaining.substring(valIdx + meta.value.length); + // 구분자 소비 + if (meta.sep && remaining.startsWith(meta.sep)) { + remaining = remaining.substring(meta.sep.length); + } + } + results.push({ value: meta.value, isManual: false, separator: meta.sep }); + } else { + // 미지의 값 (sequence, category 등): 다음 파트의 값 또는 구분자 기반으로 경계 탐색 + let endIdx = remaining.length; + + if (meta.sep) { + // 이 파트 뒤 구분자로 끝나는 지점 찾기 + // 단, 다음 파트의 시작을 기준으로 역방향 탐색 + if (nextMeta) { + // 다음 파트가 known이면 그 값이 시작되는 위치 탐색 + if (nextMeta.known && nextMeta.value) { + // 구분자 + 다음 값 패턴으로 찾기 + const pattern = meta.sep + nextMeta.value; + const patIdx = remaining.indexOf(pattern); + if (patIdx >= 0) endIdx = patIdx; + } else if (nextMeta.isManual) { + // 다음이 manual이면 구분자 + "____" 패턴 + const pattern = meta.sep + "____"; + const patIdx = remaining.indexOf(pattern); + if (patIdx >= 0) endIdx = patIdx; + } else { + // 구분자로 분리 + const sepIdx = remaining.indexOf(meta.sep); + if (sepIdx >= 0) endIdx = sepIdx; + } + } + } else if (nextMeta) { + // 구분자 없이 다음 파트의 알려진 값으로 경계 탐색 + if (nextMeta.known && nextMeta.value) { + const valIdx = remaining.indexOf(nextMeta.value); + if (valIdx >= 0) endIdx = valIdx; + } else if (nextMeta.isManual) { + const markerIdx = remaining.indexOf("____"); + if (markerIdx >= 0) endIdx = markerIdx; + } + } + + const extracted = remaining.substring(0, endIdx); + remaining = remaining.substring(endIdx); + // 구분자 소비 + if (meta.sep && remaining.startsWith(meta.sep)) { + remaining = remaining.substring(meta.sep.length); + } + results.push({ value: extracted, isManual: false, separator: meta.sep }); + } + } + + return results; + }; + // 카테고리 옵션 로드 useEffect(() => { const loadCategories = async () => { @@ -288,33 +405,73 @@ export default function ItemInfoPage() { }, [fetchItems]); // 채번 미리보기 로드 - const loadNumberingPreview = async () => { + const loadNumberingPreview = async (currentFormData?: Record, currentManualValue?: string) => { try { - const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`); - const rule = ruleRes.data?.data; - if (rule?.ruleId) { - const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { formData: {} }); - return previewRes.data?.data?.generatedCode || ""; + setIsNumberingLoading(true); + + // 규칙 조회 (캐싱) + let rule = numberingRule; + if (!rule) { + const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`); + rule = ruleRes.data?.data; + if (rule) { + setNumberingRule(rule); + numberingRuleIdRef.current = rule.ruleId; + } } + + if (!rule?.ruleId) return { code: "", parts: [] as { value: string; isManual: boolean; separator: string }[] }; + + // preview 호출 (formData + manualInputValue 전달) + const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { + formData: currentFormData || {}, + manualInputValue: currentManualValue || undefined, + }); + + const generatedCode = previewRes.data?.data?.generatedCode || ""; + // 파트별 표시값 추출 + const parts = parsePreviewIntoParts(generatedCode, rule); + setNumberingParts(parts); + return { code: generatedCode, parts }; } catch { /* 채번 규칙 없으면 무시 */ } - return ""; + finally { + setIsNumberingLoading(false); + } + return { code: "", parts: [] as { value: string; isManual: boolean; separator: string }[] }; + }; + + // 파트 값으로부터 전체 코드 조합 (수동 입력값 포함) + const buildCodeFromParts = (parts: { value: string; isManual: boolean; separator: string }[], manualVal: string) => { + return parts.map((p, idx) => { + const val = p.isManual ? manualVal : p.value; + const sep = idx < parts.length - 1 ? p.separator : ""; + return val + sep; + }).join(""); }; // 등록 모달 열기 const openRegisterModal = async () => { setFormData({}); + setManualInputValue(""); + setNumberingParts([]); setIsEditMode(false); setEditId(null); setIsModalOpen(true); - // 채번 컬럼 자동 로드 - const code = await loadNumberingPreview(); - if (code) setFormData(prev => ({ ...prev, item_number: code })); + // 채번 미리보기 + const result = await loadNumberingPreview({}); + if (result.code) { + const hasManual = result.parts.some(p => p.isManual); + const displayCode = hasManual ? buildCodeFromParts(result.parts, "") : result.code; + setFormData(prev => ({ ...prev, item_number: displayCode })); + } }; // 수정 모달 열기 const openEditModal = (item: any) => { const raw = rawItems.find((r) => r.id === item.id) || item; setFormData({ ...raw }); + setManualInputValue(""); + setNumberingParts([]); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); @@ -325,13 +482,56 @@ export default function ItemInfoPage() { const raw = rawItems.find((r) => r.id === item.id) || item; const { id, item_number, created_date, updated_date, writer, ...rest } = raw; setFormData(rest); + setManualInputValue(""); + setNumberingParts([]); setIsEditMode(false); setEditId(null); - const code = await loadNumberingPreview(); - if (code) setFormData(prev => ({ ...prev, item_number: code })); setIsModalOpen(true); + // 복사된 formData 기반으로 preview + const result = await loadNumberingPreview(rest); + if (result.code) { + const hasManual = result.parts.some(p => p.isManual); + const displayCode = hasManual ? buildCodeFromParts(result.parts, "") : result.code; + setFormData(prev => ({ ...prev, item_number: displayCode })); + } }; + // 카테고리 변경 시 채번 preview 재호출 + useEffect(() => { + if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return; + + const hasCategoryPart = numberingRule?.parts?.some( + (p: any) => p.partType === "category" && p.generationMethod === "auto" + ); + if (!hasCategoryPart) return; + + const timer = setTimeout(async () => { + const result = await loadNumberingPreview(formData, manualInputValue); + if (result.parts.length > 0) { + setFormData(prev => ({ ...prev, item_number: buildCodeFromParts(result.parts, manualInputValue) })); + } + }, 300); + + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [...CATEGORY_COLUMNS.map(col => formData[col])]); + + // 수동 입력값 변경 시 preview 갱신 (순번 재계산) + useEffect(() => { + if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return; + if (!numberingParts.some(p => p.isManual)) return; + + const timer = setTimeout(async () => { + const result = await loadNumberingPreview(formData, manualInputValue); + if (result.parts.length > 0) { + setFormData(prev => ({ ...prev, item_number: buildCodeFromParts(result.parts, manualInputValue) })); + } + }, 500); + + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [manualInputValue]); + // 저장 const handleSave = async () => { if (!formData.item_name) { @@ -342,6 +542,7 @@ export default function ItemInfoPage() { setSaving(true); try { if (isEditMode && editId) { + // 수정: item_number는 변경하지 않음 const { id, created_date, updated_date, writer, company_code, ...updateFields } = formData; await apiClient.put(`/table-management/tables/${TABLE_NAME}/edit`, { originalData: { id: editId }, @@ -349,8 +550,38 @@ export default function ItemInfoPage() { }); toast.success("수정되었어요."); } else { + // 신규 등록: allocateCode 호출하여 실제 순번 확보 + let finalItemNumber = formData.item_number || ""; + + if (numberingRuleIdRef.current) { + try { + const hasManual = numberingParts.some(p => p.isManual); + const userInputCode = hasManual && manualInputValue + ? manualInputValue + : undefined; + + const allocRes = await apiClient.post( + `/numbering-rules/${numberingRuleIdRef.current}/allocate`, + { formData, userInputCode } + ); + + if (allocRes.data?.success && allocRes.data?.data?.generatedCode) { + finalItemNumber = allocRes.data.data.generatedCode; + } + } catch (err) { + console.error("채번 할당 실패:", err); + toast.error("품목코드 할당에 실패했어요. 다시 시도해주세요."); + setSaving(false); + return; + } + } + const { id, created_date, updated_date, ...insertFields } = formData; - await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, { id: crypto.randomUUID(), ...insertFields }); + await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, { + id: crypto.randomUUID(), + ...insertFields, + item_number: finalItemNumber, + }); toast.success("등록되었어요."); } setIsModalOpen(false); @@ -484,7 +715,15 @@ export default function ItemInfoPage() { /> {/* 등록/수정 모달 */} - + { + setIsModalOpen(open); + if (!open) { + setNumberingParts([]); + setManualInputValue(""); + setNumberingRule(null); + numberingRuleIdRef.current = null; + } + }}> {isEditMode ? "품목 수정" : "품목 등록"} @@ -534,6 +773,74 @@ export default function ItemInfoPage() { placeholder={field.label} rows={3} /> + ) : field.type === "numbering" ? ( + // 채번 세그먼트 UI + isEditMode ? ( + + ) : isNumberingLoading && numberingParts.length === 0 ? ( +
+ + 생성 중... +
+ ) : numberingParts.some(p => p.isManual) ? ( + // 파트별 세그먼트 렌더링 (수동 입력 파트 있음) +
+ {numberingParts.map((part, idx) => { + const isFirst = idx === 0; + const isLast = idx === numberingParts.length - 1; + if (part.isManual) { + return ( + + { + const val = e.target.value; + setManualInputValue(val); + setFormData(prev => ({ + ...prev, + item_number: buildCodeFromParts(numberingParts, val), + })); + }} + placeholder="입력" + className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none" + /> + {part.separator && !isLast && ( + {part.separator} + )} + + ); + } + // auto 파트: 회색 배경 읽기전용 + return ( + + + {part.value} + + {part.separator && !isLast && ( + {part.separator} + )} + + ); + })} +
+ ) : ( + // 전체 auto: 읽기전용 표시 + + ) ) : ["selling_price", "standard_price"].includes(field.key) ? ( setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))} - placeholder={field.placeholder || (field.disabled ? "자동 채번" : field.label)} - disabled={field.disabled && !isEditMode} + placeholder={field.placeholder || field.label} className="h-9" /> )} diff --git a/frontend/app/(main)/COMPANY_10/production/bom/page.tsx b/frontend/app/(main)/COMPANY_10/production/bom/page.tsx index 0563f47b..65e83cf2 100644 --- a/frontend/app/(main)/COMPANY_10/production/bom/page.tsx +++ b/frontend/app/(main)/COMPANY_10/production/bom/page.tsx @@ -412,21 +412,22 @@ export default function BomManagementPage() { } catch {} } - // item_info의 division 카테고리 - try { - const res = await apiClient.get(`/table-categories/item_info/division/values`); - if (res.data?.data?.length > 0) { - const flatten = (items: any[]): { code: string; label: string }[] => { - const result: { code: string; label: string }[] = []; - for (const item of items) { - result.push({ code: item.valueCode || item.value || item.code, label: item.valueLabel || item.label || item.value }); - if (item.children?.length) result.push(...flatten(item.children)); - } - return result; - }; - results["division"] = flatten(res.data.data); - } - } catch {} + for (const itemCol of ["division", "unit"]) { + try { + const res = await apiClient.get(`/table-categories/item_info/${itemCol}/values`); + if (res.data?.data?.length > 0) { + const flatten = (items: any[]): { code: string; label: string }[] => { + const result: { code: string; label: string }[] = []; + for (const item of items) { + result.push({ code: item.valueCode || item.value || item.code, label: item.valueLabel || item.label || item.value }); + if (item.children?.length) result.push(...flatten(item.children)); + } + return result; + }; + results[itemCol] = flatten(res.data.data); + } + } catch {} + } setCategoryOptions(results); } catch {} @@ -1122,12 +1123,11 @@ export default function BomManagementPage() { } // 현재 버전 ID 가져오기 - let versionId = currentVersionId; - if (!versionId && bomId) { + let versionId: string | null = null; + if (bomId) { try { const verRes = await apiClient.get(`/bom/${bomId}/versions`); - const verData = verRes.data?.data || verRes.data; - versionId = verData?.currentVersionId || null; + versionId = verRes.data?.currentVersionId || null; } catch {} } @@ -1193,7 +1193,20 @@ export default function BomManagementPage() { }); rows = res2.data?.data?.data || res2.data?.data?.rows || []; } - setItemSearchResults(rows); + const resolved = rows.map((r: any) => { + const out = { ...r }; + if (out.division) { + out.division = out.division.split(",").map((c: string) => { + const t = c.trim(); + return categoryOptions["division"]?.find((o) => o.code === t)?.label || t; + }).filter((v: string) => v && v !== "s").join(", "); + } + if (out.unit) { + out.unit = categoryOptions["unit"]?.find((o) => o.code === out.unit)?.label || out.unit; + } + return out; + }); + setItemSearchResults(resolved); } catch { toast.error("품목 검색에 실패했어요"); } finally { @@ -1201,14 +1214,20 @@ export default function BomManagementPage() { } }; + const resolveUnit = (code: string) => { + if (!code) return ""; + return categoryOptions["unit"]?.find((o) => o.code === code)?.label || code; + }; + const selectItem = (item: any) => { + const unitLabel = resolveUnit(item.unit); if (itemSearchTarget === "master") { setMasterForm((prev) => ({ ...prev, item_id: item.id, item_code: item.item_number || "", item_name: item.item_name || "", - unit: item.unit || prev.unit, + unit: unitLabel || prev.unit, })); setShowItemSearchModal(false); } else { @@ -1220,7 +1239,7 @@ export default function BomManagementPage() { item_number: item.item_number || "", item_name: item.item_name || "", quantity: "1", - unit: item.unit || "", + unit: unitLabel, process_type: "", loss_rate: "0", remark: "", diff --git a/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx index 546ed739..50be10ee 100644 --- a/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx @@ -674,7 +674,10 @@ export default function ProductionPlanManagementPage() { manager_name: modalManager, work_order_no: modalWorkOrderNo, remarks: modalRemarks, - equipment_id: modalEquipmentId ? Number(modalEquipmentId) : null, + equipment_code: modalEquipmentId && modalEquipmentId !== "none" ? modalEquipmentId : null, + equipment_name: modalEquipmentId && modalEquipmentId !== "none" + ? equipmentList.find((eq) => (eq.equipment_code || eq.id) === modalEquipmentId)?.equipment_name || null + : null, } as any); if (res.success) { toast.success("생산계획이 수정되었습니다"); diff --git a/frontend/app/(main)/COMPANY_10/production/process-info/ItemRoutingTab.tsx b/frontend/app/(main)/COMPANY_10/production/process-info/ItemRoutingTab.tsx index a5b136c2..e50e27d5 100644 --- a/frontend/app/(main)/COMPANY_10/production/process-info/ItemRoutingTab.tsx +++ b/frontend/app/(main)/COMPANY_10/production/process-info/ItemRoutingTab.tsx @@ -39,6 +39,7 @@ import { type RoutingVersion, } from "@/lib/api/processInfo"; import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; function normalizeDefaultFlag(v: RoutingVersion): boolean { const raw = v.is_default as unknown; @@ -210,6 +211,7 @@ export function ItemRoutingTab() { return; } const list = res.data.map((v) => ({ ...v, is_default: normalizeDefaultFlag(v) })); + list.sort((a, b) => (a.is_default === b.is_default ? 0 : a.is_default ? -1 : 1)); setVersions(list); const preferred = preferVersionId ? list.find((v) => v.id === preferVersionId) : undefined; const def = list.find((v) => v.is_default); @@ -399,6 +401,38 @@ export function ItemRoutingTab() { } }; + // 선택 버전이 기본인지 + const selectedVersionIsDefault = useMemo(() => { + if (!selectedVersionId) return false; + const v = versions.find((v) => v.id === selectedVersionId); + return v ? normalizeDefaultFlag(v) : false; + }, [selectedVersionId, versions]); + + // 기본 라우팅으로 설정 + const handleSetDefaultVersion = async () => { + if (!selectedVersionId || !selectedItem) return; + try { + // 기존 기본 해제 + for (const v of versions) { + if (normalizeDefaultFlag(v) && v.id !== selectedVersionId) { + await apiClient.put(`/table-management/tables/item_routing_version/edit`, { + originalData: { id: v.id }, + updatedData: { is_default: false }, + }); + } + } + // 선택 버전 기본 설정 + await apiClient.put(`/table-management/tables/item_routing_version/edit`, { + originalData: { id: selectedVersionId }, + updatedData: { is_default: true }, + }); + toast.success("기본 라우팅으로 설정했어요"); + await loadVersions(selectedItem, selectedVersionId); + } catch { + toast.error("기본 설정에 실패했어요"); + } + }; + const submitNewVersion = async () => { if (!selectedItem) return; const name = versionName.trim(); @@ -639,6 +673,12 @@ export function ItemRoutingTab() {
+ {selectedVersionId && !selectedVersionIsDefault && ( + + )}
+ {selectedVersionId && !selectedVersionIsDefault && ( + + )}
+ {selectedVersionId && !selectedVersionIsDefault && ( + + )}
+ {selectedVersionId && !selectedVersionIsDefault && ( + + )}
+ {selectedVersionId && !selectedVersionIsDefault && ( + + )}
+ {selectedVersionId && !selectedVersionIsDefault && ( + + )}
+ {selectedVersionId && !selectedVersionIsDefault && ( + + )}