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..c208192e 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,13 @@ export default function ItemInfoPage() { // 선택된 행 const [selectedId, setSelectedId] = useState(null); + // 채번 관련 상태 + const [numberingRule, setNumberingRule] = useState(null); + const [numberingTemplate, setNumberingTemplate] = useState(""); + const [manualInputValue, setManualInputValue] = useState(""); + const [isNumberingLoading, setIsNumberingLoading] = useState(false); + const numberingRuleIdRef = useRef(null); + // 카테고리 옵션 로드 useEffect(() => { const loadCategories = async () => { @@ -288,26 +295,49 @@ 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 ""; + + // preview 호출 (formData + manualInputValue 전달) + const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { + formData: currentFormData || {}, + manualInputValue: currentManualValue || undefined, + }); + + const generatedCode = previewRes.data?.data?.generatedCode || ""; + setNumberingTemplate(generatedCode); + return generatedCode; } catch { /* 채번 규칙 없으면 무시 */ } + finally { + setIsNumberingLoading(false); + } return ""; }; // 등록 모달 열기 const openRegisterModal = async () => { setFormData({}); + setManualInputValue(""); + setNumberingTemplate(""); setIsEditMode(false); setEditId(null); setIsModalOpen(true); - // 채번 컬럼 자동 로드 - const code = await loadNumberingPreview(); + // 채번 미리보기 + const code = await loadNumberingPreview({}); if (code) setFormData(prev => ({ ...prev, item_number: code })); }; @@ -315,6 +345,8 @@ export default function ItemInfoPage() { const openEditModal = (item: any) => { const raw = rawItems.find((r) => r.id === item.id) || item; setFormData({ ...raw }); + setManualInputValue(""); + setNumberingTemplate(""); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); @@ -325,13 +357,72 @@ 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(""); + setNumberingTemplate(""); setIsEditMode(false); setEditId(null); - const code = await loadNumberingPreview(); - if (code) setFormData(prev => ({ ...prev, item_number: code })); setIsModalOpen(true); + // 복사된 formData 기반으로 preview + const code = await loadNumberingPreview(rest); + if (code) setFormData(prev => ({ ...prev, item_number: code })); }; + // 카테고리 변경 시 채번 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 code = await loadNumberingPreview(formData, manualInputValue); + if (code) { + if (code.includes("____")) { + setNumberingTemplate(code); + const parts = code.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix })); + } else { + setFormData(prev => ({ ...prev, item_number: code })); + } + } + }, 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 (!numberingTemplate.includes("____")) return; + + const timer = setTimeout(async () => { + try { + const previewRes = await apiClient.post( + `/numbering-rules/${numberingRuleIdRef.current}/preview`, + { formData, manualInputValue: manualInputValue || undefined } + ); + const newCode = previewRes.data?.data?.generatedCode || ""; + if (newCode) { + setNumberingTemplate(newCode); + if (newCode.includes("____")) { + const parts = newCode.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix })); + } + } + } catch { /* ignore */ } + }, 500); + + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [manualInputValue]); + // 저장 const handleSave = async () => { if (!formData.item_name) { @@ -342,6 +433,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 +441,42 @@ export default function ItemInfoPage() { }); toast.success("수정되었어요."); } else { + // 신규 등록: allocateCode 호출하여 실제 순번 확보 + let finalItemNumber = formData.item_number || ""; + + if (numberingRuleIdRef.current) { + try { + const userInputCode = numberingTemplate.includes("____") + ? (() => { + const parts = numberingTemplate.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + return prefix + manualInputValue + suffix; + })() + : 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 +610,15 @@ export default function ItemInfoPage() { /> {/* 등록/수정 모달 */} - + { + setIsModalOpen(open); + if (!open) { + setNumberingTemplate(""); + setManualInputValue(""); + setNumberingRule(null); + numberingRuleIdRef.current = null; + } + }}> {isEditMode ? "품목 수정" : "품목 등록"} @@ -534,6 +668,61 @@ export default function ItemInfoPage() { placeholder={field.label} rows={3} /> + ) : field.type === "numbering" ? ( + // 채번 세그먼트 UI + isEditMode ? ( + + ) : isNumberingLoading ? ( +
+ + 생성 중... +
+ ) : numberingTemplate.includes("____") ? ( + (() => { + const tplParts = numberingTemplate.split("____"); + const prefix = tplParts[0] || ""; + const suffix = tplParts.slice(1).join("") || ""; + return ( +
+ {prefix && ( + + {prefix} + + )} + { + const val = e.target.value; + setManualInputValue(val); + setFormData(prev => ({ + ...prev, + item_number: prefix + val + suffix, + })); + }} + placeholder="입력" + className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none" + /> + {suffix && ( + + {suffix} + + )} +
+ ); + })() + ) : ( + + ) ) : ["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/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/sales/order/page.tsx b/frontend/app/(main)/COMPANY_10/sales/order/page.tsx index f9a358d4..1cb49efb 100644 --- a/frontend/app/(main)/COMPANY_10/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_10/sales/order/page.tsx @@ -249,7 +249,7 @@ export default function SalesOrderPage() { page: 1, size: 500, autoFilter: true, }); const custs = custRes.data?.data?.data || custRes.data?.data?.rows || []; - optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: `${c.customer_name} (${c.customer_code})` })); + optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: c.customer_name })); } catch { /* skip */ } // 사용자 목록 try { @@ -608,6 +608,24 @@ export default function SalesOrderPage() { try { const filters: any[] = []; if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword }); + + // 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링 + const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI"; + const partnerId = masterForm.partner_id; + let customerItemIds: Set | null = null; + + if (isCustomerPrice && partnerId) { + try { + const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, { + page: 1, size: 500, + dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] }, + autoFilter: true, + }); + const mappings = mappingRes.data?.data?.data || mappingRes.data?.data?.rows || []; + customerItemIds = new Set(mappings.map((m: any) => m.item_id).filter(Boolean)); + } catch { /* skip */ } + } + const res = await apiClient.post(`/table-management/tables/item_info/data`, { page: 1, size: 500, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, @@ -615,6 +633,12 @@ export default function SalesOrderPage() { }); const resData = res.data?.data; let allRows = resData?.data || resData?.rows || []; + + // 거래처우선일 때 연결된 품목만 표시 + if (customerItemIds) { + allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); + } + // 관리품목 필터 (코드/라벨 혼재 대응) if (itemSearchDivision !== "all") { const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; @@ -678,7 +702,8 @@ export default function SalesOrderPage() { autoFilter: true, }); const prices = res.data?.data?.data || res.data?.data?.rows || []; - const today = new Date().toISOString().slice(0, 10); + const _n = new Date(); + const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`; for (const p of prices) { const start = p.start_date || ""; const end = p.end_date || ""; @@ -768,7 +793,8 @@ export default function SalesOrderPage() { autoFilter: true, }); const prices = res.data?.data?.data || res.data?.data?.rows || []; - const today = new Date().toISOString().slice(0, 10); + const _n = new Date(); + const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`; const priceMap: Record = {}; for (const p of prices) { if (p.start_date && p.start_date > today) continue; diff --git a/frontend/app/(main)/COMPANY_16/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_16/master-data/item-info/page.tsx index 375fd900..c208192e 100644 --- a/frontend/app/(main)/COMPANY_16/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/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,13 @@ export default function ItemInfoPage() { // 선택된 행 const [selectedId, setSelectedId] = useState(null); + // 채번 관련 상태 + const [numberingRule, setNumberingRule] = useState(null); + const [numberingTemplate, setNumberingTemplate] = useState(""); + const [manualInputValue, setManualInputValue] = useState(""); + const [isNumberingLoading, setIsNumberingLoading] = useState(false); + const numberingRuleIdRef = useRef(null); + // 카테고리 옵션 로드 useEffect(() => { const loadCategories = async () => { @@ -288,26 +295,49 @@ 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 ""; + + // preview 호출 (formData + manualInputValue 전달) + const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { + formData: currentFormData || {}, + manualInputValue: currentManualValue || undefined, + }); + + const generatedCode = previewRes.data?.data?.generatedCode || ""; + setNumberingTemplate(generatedCode); + return generatedCode; } catch { /* 채번 규칙 없으면 무시 */ } + finally { + setIsNumberingLoading(false); + } return ""; }; // 등록 모달 열기 const openRegisterModal = async () => { setFormData({}); + setManualInputValue(""); + setNumberingTemplate(""); setIsEditMode(false); setEditId(null); setIsModalOpen(true); - // 채번 컬럼 자동 로드 - const code = await loadNumberingPreview(); + // 채번 미리보기 + const code = await loadNumberingPreview({}); if (code) setFormData(prev => ({ ...prev, item_number: code })); }; @@ -315,6 +345,8 @@ export default function ItemInfoPage() { const openEditModal = (item: any) => { const raw = rawItems.find((r) => r.id === item.id) || item; setFormData({ ...raw }); + setManualInputValue(""); + setNumberingTemplate(""); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); @@ -325,13 +357,72 @@ 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(""); + setNumberingTemplate(""); setIsEditMode(false); setEditId(null); - const code = await loadNumberingPreview(); - if (code) setFormData(prev => ({ ...prev, item_number: code })); setIsModalOpen(true); + // 복사된 formData 기반으로 preview + const code = await loadNumberingPreview(rest); + if (code) setFormData(prev => ({ ...prev, item_number: code })); }; + // 카테고리 변경 시 채번 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 code = await loadNumberingPreview(formData, manualInputValue); + if (code) { + if (code.includes("____")) { + setNumberingTemplate(code); + const parts = code.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix })); + } else { + setFormData(prev => ({ ...prev, item_number: code })); + } + } + }, 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 (!numberingTemplate.includes("____")) return; + + const timer = setTimeout(async () => { + try { + const previewRes = await apiClient.post( + `/numbering-rules/${numberingRuleIdRef.current}/preview`, + { formData, manualInputValue: manualInputValue || undefined } + ); + const newCode = previewRes.data?.data?.generatedCode || ""; + if (newCode) { + setNumberingTemplate(newCode); + if (newCode.includes("____")) { + const parts = newCode.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix })); + } + } + } catch { /* ignore */ } + }, 500); + + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [manualInputValue]); + // 저장 const handleSave = async () => { if (!formData.item_name) { @@ -342,6 +433,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 +441,42 @@ export default function ItemInfoPage() { }); toast.success("수정되었어요."); } else { + // 신규 등록: allocateCode 호출하여 실제 순번 확보 + let finalItemNumber = formData.item_number || ""; + + if (numberingRuleIdRef.current) { + try { + const userInputCode = numberingTemplate.includes("____") + ? (() => { + const parts = numberingTemplate.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + return prefix + manualInputValue + suffix; + })() + : 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 +610,15 @@ export default function ItemInfoPage() { /> {/* 등록/수정 모달 */} - + { + setIsModalOpen(open); + if (!open) { + setNumberingTemplate(""); + setManualInputValue(""); + setNumberingRule(null); + numberingRuleIdRef.current = null; + } + }}> {isEditMode ? "품목 수정" : "품목 등록"} @@ -534,6 +668,61 @@ export default function ItemInfoPage() { placeholder={field.label} rows={3} /> + ) : field.type === "numbering" ? ( + // 채번 세그먼트 UI + isEditMode ? ( + + ) : isNumberingLoading ? ( +
+ + 생성 중... +
+ ) : numberingTemplate.includes("____") ? ( + (() => { + const tplParts = numberingTemplate.split("____"); + const prefix = tplParts[0] || ""; + const suffix = tplParts.slice(1).join("") || ""; + return ( +
+ {prefix && ( + + {prefix} + + )} + { + const val = e.target.value; + setManualInputValue(val); + setFormData(prev => ({ + ...prev, + item_number: prefix + val + suffix, + })); + }} + placeholder="입력" + className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none" + /> + {suffix && ( + + {suffix} + + )} +
+ ); + })() + ) : ( + + ) ) : ["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_16/sales/order/page.tsx b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx index f9a358d4..1cb49efb 100644 --- a/frontend/app/(main)/COMPANY_16/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx @@ -249,7 +249,7 @@ export default function SalesOrderPage() { page: 1, size: 500, autoFilter: true, }); const custs = custRes.data?.data?.data || custRes.data?.data?.rows || []; - optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: `${c.customer_name} (${c.customer_code})` })); + optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: c.customer_name })); } catch { /* skip */ } // 사용자 목록 try { @@ -608,6 +608,24 @@ export default function SalesOrderPage() { try { const filters: any[] = []; if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword }); + + // 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링 + const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI"; + const partnerId = masterForm.partner_id; + let customerItemIds: Set | null = null; + + if (isCustomerPrice && partnerId) { + try { + const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, { + page: 1, size: 500, + dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] }, + autoFilter: true, + }); + const mappings = mappingRes.data?.data?.data || mappingRes.data?.data?.rows || []; + customerItemIds = new Set(mappings.map((m: any) => m.item_id).filter(Boolean)); + } catch { /* skip */ } + } + const res = await apiClient.post(`/table-management/tables/item_info/data`, { page: 1, size: 500, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, @@ -615,6 +633,12 @@ export default function SalesOrderPage() { }); const resData = res.data?.data; let allRows = resData?.data || resData?.rows || []; + + // 거래처우선일 때 연결된 품목만 표시 + if (customerItemIds) { + allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); + } + // 관리품목 필터 (코드/라벨 혼재 대응) if (itemSearchDivision !== "all") { const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; @@ -678,7 +702,8 @@ export default function SalesOrderPage() { autoFilter: true, }); const prices = res.data?.data?.data || res.data?.data?.rows || []; - const today = new Date().toISOString().slice(0, 10); + const _n = new Date(); + const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`; for (const p of prices) { const start = p.start_date || ""; const end = p.end_date || ""; @@ -768,7 +793,8 @@ export default function SalesOrderPage() { autoFilter: true, }); const prices = res.data?.data?.data || res.data?.data?.rows || []; - const today = new Date().toISOString().slice(0, 10); + const _n = new Date(); + const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`; const priceMap: Record = {}; for (const p of prices) { if (p.start_date && p.start_date > today) continue; diff --git a/frontend/app/(main)/COMPANY_29/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_29/master-data/item-info/page.tsx index 375fd900..c208192e 100644 --- a/frontend/app/(main)/COMPANY_29/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_29/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,13 @@ export default function ItemInfoPage() { // 선택된 행 const [selectedId, setSelectedId] = useState(null); + // 채번 관련 상태 + const [numberingRule, setNumberingRule] = useState(null); + const [numberingTemplate, setNumberingTemplate] = useState(""); + const [manualInputValue, setManualInputValue] = useState(""); + const [isNumberingLoading, setIsNumberingLoading] = useState(false); + const numberingRuleIdRef = useRef(null); + // 카테고리 옵션 로드 useEffect(() => { const loadCategories = async () => { @@ -288,26 +295,49 @@ 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 ""; + + // preview 호출 (formData + manualInputValue 전달) + const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { + formData: currentFormData || {}, + manualInputValue: currentManualValue || undefined, + }); + + const generatedCode = previewRes.data?.data?.generatedCode || ""; + setNumberingTemplate(generatedCode); + return generatedCode; } catch { /* 채번 규칙 없으면 무시 */ } + finally { + setIsNumberingLoading(false); + } return ""; }; // 등록 모달 열기 const openRegisterModal = async () => { setFormData({}); + setManualInputValue(""); + setNumberingTemplate(""); setIsEditMode(false); setEditId(null); setIsModalOpen(true); - // 채번 컬럼 자동 로드 - const code = await loadNumberingPreview(); + // 채번 미리보기 + const code = await loadNumberingPreview({}); if (code) setFormData(prev => ({ ...prev, item_number: code })); }; @@ -315,6 +345,8 @@ export default function ItemInfoPage() { const openEditModal = (item: any) => { const raw = rawItems.find((r) => r.id === item.id) || item; setFormData({ ...raw }); + setManualInputValue(""); + setNumberingTemplate(""); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); @@ -325,13 +357,72 @@ 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(""); + setNumberingTemplate(""); setIsEditMode(false); setEditId(null); - const code = await loadNumberingPreview(); - if (code) setFormData(prev => ({ ...prev, item_number: code })); setIsModalOpen(true); + // 복사된 formData 기반으로 preview + const code = await loadNumberingPreview(rest); + if (code) setFormData(prev => ({ ...prev, item_number: code })); }; + // 카테고리 변경 시 채번 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 code = await loadNumberingPreview(formData, manualInputValue); + if (code) { + if (code.includes("____")) { + setNumberingTemplate(code); + const parts = code.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix })); + } else { + setFormData(prev => ({ ...prev, item_number: code })); + } + } + }, 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 (!numberingTemplate.includes("____")) return; + + const timer = setTimeout(async () => { + try { + const previewRes = await apiClient.post( + `/numbering-rules/${numberingRuleIdRef.current}/preview`, + { formData, manualInputValue: manualInputValue || undefined } + ); + const newCode = previewRes.data?.data?.generatedCode || ""; + if (newCode) { + setNumberingTemplate(newCode); + if (newCode.includes("____")) { + const parts = newCode.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix })); + } + } + } catch { /* ignore */ } + }, 500); + + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [manualInputValue]); + // 저장 const handleSave = async () => { if (!formData.item_name) { @@ -342,6 +433,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 +441,42 @@ export default function ItemInfoPage() { }); toast.success("수정되었어요."); } else { + // 신규 등록: allocateCode 호출하여 실제 순번 확보 + let finalItemNumber = formData.item_number || ""; + + if (numberingRuleIdRef.current) { + try { + const userInputCode = numberingTemplate.includes("____") + ? (() => { + const parts = numberingTemplate.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + return prefix + manualInputValue + suffix; + })() + : 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 +610,15 @@ export default function ItemInfoPage() { /> {/* 등록/수정 모달 */} - + { + setIsModalOpen(open); + if (!open) { + setNumberingTemplate(""); + setManualInputValue(""); + setNumberingRule(null); + numberingRuleIdRef.current = null; + } + }}> {isEditMode ? "품목 수정" : "품목 등록"} @@ -534,6 +668,61 @@ export default function ItemInfoPage() { placeholder={field.label} rows={3} /> + ) : field.type === "numbering" ? ( + // 채번 세그먼트 UI + isEditMode ? ( + + ) : isNumberingLoading ? ( +
+ + 생성 중... +
+ ) : numberingTemplate.includes("____") ? ( + (() => { + const tplParts = numberingTemplate.split("____"); + const prefix = tplParts[0] || ""; + const suffix = tplParts.slice(1).join("") || ""; + return ( +
+ {prefix && ( + + {prefix} + + )} + { + const val = e.target.value; + setManualInputValue(val); + setFormData(prev => ({ + ...prev, + item_number: prefix + val + suffix, + })); + }} + placeholder="입력" + className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none" + /> + {suffix && ( + + {suffix} + + )} +
+ ); + })() + ) : ( + + ) ) : ["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_29/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx index 546ed739..50be10ee 100644 --- a/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_29/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_29/sales/order/page.tsx b/frontend/app/(main)/COMPANY_29/sales/order/page.tsx index f9a358d4..1cb49efb 100644 --- a/frontend/app/(main)/COMPANY_29/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_29/sales/order/page.tsx @@ -249,7 +249,7 @@ export default function SalesOrderPage() { page: 1, size: 500, autoFilter: true, }); const custs = custRes.data?.data?.data || custRes.data?.data?.rows || []; - optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: `${c.customer_name} (${c.customer_code})` })); + optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: c.customer_name })); } catch { /* skip */ } // 사용자 목록 try { @@ -608,6 +608,24 @@ export default function SalesOrderPage() { try { const filters: any[] = []; if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword }); + + // 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링 + const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI"; + const partnerId = masterForm.partner_id; + let customerItemIds: Set | null = null; + + if (isCustomerPrice && partnerId) { + try { + const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, { + page: 1, size: 500, + dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] }, + autoFilter: true, + }); + const mappings = mappingRes.data?.data?.data || mappingRes.data?.data?.rows || []; + customerItemIds = new Set(mappings.map((m: any) => m.item_id).filter(Boolean)); + } catch { /* skip */ } + } + const res = await apiClient.post(`/table-management/tables/item_info/data`, { page: 1, size: 500, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, @@ -615,6 +633,12 @@ export default function SalesOrderPage() { }); const resData = res.data?.data; let allRows = resData?.data || resData?.rows || []; + + // 거래처우선일 때 연결된 품목만 표시 + if (customerItemIds) { + allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); + } + // 관리품목 필터 (코드/라벨 혼재 대응) if (itemSearchDivision !== "all") { const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; @@ -678,7 +702,8 @@ export default function SalesOrderPage() { autoFilter: true, }); const prices = res.data?.data?.data || res.data?.data?.rows || []; - const today = new Date().toISOString().slice(0, 10); + const _n = new Date(); + const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`; for (const p of prices) { const start = p.start_date || ""; const end = p.end_date || ""; @@ -768,7 +793,8 @@ export default function SalesOrderPage() { autoFilter: true, }); const prices = res.data?.data?.data || res.data?.data?.rows || []; - const today = new Date().toISOString().slice(0, 10); + const _n = new Date(); + const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`; const priceMap: Record = {}; for (const p of prices) { if (p.start_date && p.start_date > today) continue; diff --git a/frontend/app/(main)/COMPANY_30/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_30/master-data/item-info/page.tsx index 375fd900..c208192e 100644 --- a/frontend/app/(main)/COMPANY_30/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_30/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,13 @@ export default function ItemInfoPage() { // 선택된 행 const [selectedId, setSelectedId] = useState(null); + // 채번 관련 상태 + const [numberingRule, setNumberingRule] = useState(null); + const [numberingTemplate, setNumberingTemplate] = useState(""); + const [manualInputValue, setManualInputValue] = useState(""); + const [isNumberingLoading, setIsNumberingLoading] = useState(false); + const numberingRuleIdRef = useRef(null); + // 카테고리 옵션 로드 useEffect(() => { const loadCategories = async () => { @@ -288,26 +295,49 @@ 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 ""; + + // preview 호출 (formData + manualInputValue 전달) + const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { + formData: currentFormData || {}, + manualInputValue: currentManualValue || undefined, + }); + + const generatedCode = previewRes.data?.data?.generatedCode || ""; + setNumberingTemplate(generatedCode); + return generatedCode; } catch { /* 채번 규칙 없으면 무시 */ } + finally { + setIsNumberingLoading(false); + } return ""; }; // 등록 모달 열기 const openRegisterModal = async () => { setFormData({}); + setManualInputValue(""); + setNumberingTemplate(""); setIsEditMode(false); setEditId(null); setIsModalOpen(true); - // 채번 컬럼 자동 로드 - const code = await loadNumberingPreview(); + // 채번 미리보기 + const code = await loadNumberingPreview({}); if (code) setFormData(prev => ({ ...prev, item_number: code })); }; @@ -315,6 +345,8 @@ export default function ItemInfoPage() { const openEditModal = (item: any) => { const raw = rawItems.find((r) => r.id === item.id) || item; setFormData({ ...raw }); + setManualInputValue(""); + setNumberingTemplate(""); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); @@ -325,13 +357,72 @@ 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(""); + setNumberingTemplate(""); setIsEditMode(false); setEditId(null); - const code = await loadNumberingPreview(); - if (code) setFormData(prev => ({ ...prev, item_number: code })); setIsModalOpen(true); + // 복사된 formData 기반으로 preview + const code = await loadNumberingPreview(rest); + if (code) setFormData(prev => ({ ...prev, item_number: code })); }; + // 카테고리 변경 시 채번 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 code = await loadNumberingPreview(formData, manualInputValue); + if (code) { + if (code.includes("____")) { + setNumberingTemplate(code); + const parts = code.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix })); + } else { + setFormData(prev => ({ ...prev, item_number: code })); + } + } + }, 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 (!numberingTemplate.includes("____")) return; + + const timer = setTimeout(async () => { + try { + const previewRes = await apiClient.post( + `/numbering-rules/${numberingRuleIdRef.current}/preview`, + { formData, manualInputValue: manualInputValue || undefined } + ); + const newCode = previewRes.data?.data?.generatedCode || ""; + if (newCode) { + setNumberingTemplate(newCode); + if (newCode.includes("____")) { + const parts = newCode.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix })); + } + } + } catch { /* ignore */ } + }, 500); + + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [manualInputValue]); + // 저장 const handleSave = async () => { if (!formData.item_name) { @@ -342,6 +433,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 +441,42 @@ export default function ItemInfoPage() { }); toast.success("수정되었어요."); } else { + // 신규 등록: allocateCode 호출하여 실제 순번 확보 + let finalItemNumber = formData.item_number || ""; + + if (numberingRuleIdRef.current) { + try { + const userInputCode = numberingTemplate.includes("____") + ? (() => { + const parts = numberingTemplate.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + return prefix + manualInputValue + suffix; + })() + : 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 +610,15 @@ export default function ItemInfoPage() { /> {/* 등록/수정 모달 */} - + { + setIsModalOpen(open); + if (!open) { + setNumberingTemplate(""); + setManualInputValue(""); + setNumberingRule(null); + numberingRuleIdRef.current = null; + } + }}> {isEditMode ? "품목 수정" : "품목 등록"} @@ -534,6 +668,61 @@ export default function ItemInfoPage() { placeholder={field.label} rows={3} /> + ) : field.type === "numbering" ? ( + // 채번 세그먼트 UI + isEditMode ? ( + + ) : isNumberingLoading ? ( +
+ + 생성 중... +
+ ) : numberingTemplate.includes("____") ? ( + (() => { + const tplParts = numberingTemplate.split("____"); + const prefix = tplParts[0] || ""; + const suffix = tplParts.slice(1).join("") || ""; + return ( +
+ {prefix && ( + + {prefix} + + )} + { + const val = e.target.value; + setManualInputValue(val); + setFormData(prev => ({ + ...prev, + item_number: prefix + val + suffix, + })); + }} + placeholder="입력" + className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none" + /> + {suffix && ( + + {suffix} + + )} +
+ ); + })() + ) : ( + + ) ) : ["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_30/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_30/production/plan-management/page.tsx index 546ed739..50be10ee 100644 --- a/frontend/app/(main)/COMPANY_30/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_30/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_30/sales/order/page.tsx b/frontend/app/(main)/COMPANY_30/sales/order/page.tsx index 6f4502bd..6fcbbe8f 100644 --- a/frontend/app/(main)/COMPANY_30/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_30/sales/order/page.tsx @@ -172,7 +172,7 @@ export default function JeilGlassOrderPage() { const custs = res.data?.data?.data || res.data?.data?.rows || []; optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, - label: `${c.customer_name} (${c.customer_code})`, + label: c.customer_name, })); } catch { /* skip */ } // 담당자 diff --git a/frontend/app/(main)/COMPANY_7/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_7/master-data/item-info/page.tsx index 375fd900..c208192e 100644 --- a/frontend/app/(main)/COMPANY_7/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_7/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,13 @@ export default function ItemInfoPage() { // 선택된 행 const [selectedId, setSelectedId] = useState(null); + // 채번 관련 상태 + const [numberingRule, setNumberingRule] = useState(null); + const [numberingTemplate, setNumberingTemplate] = useState(""); + const [manualInputValue, setManualInputValue] = useState(""); + const [isNumberingLoading, setIsNumberingLoading] = useState(false); + const numberingRuleIdRef = useRef(null); + // 카테고리 옵션 로드 useEffect(() => { const loadCategories = async () => { @@ -288,26 +295,49 @@ 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 ""; + + // preview 호출 (formData + manualInputValue 전달) + const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { + formData: currentFormData || {}, + manualInputValue: currentManualValue || undefined, + }); + + const generatedCode = previewRes.data?.data?.generatedCode || ""; + setNumberingTemplate(generatedCode); + return generatedCode; } catch { /* 채번 규칙 없으면 무시 */ } + finally { + setIsNumberingLoading(false); + } return ""; }; // 등록 모달 열기 const openRegisterModal = async () => { setFormData({}); + setManualInputValue(""); + setNumberingTemplate(""); setIsEditMode(false); setEditId(null); setIsModalOpen(true); - // 채번 컬럼 자동 로드 - const code = await loadNumberingPreview(); + // 채번 미리보기 + const code = await loadNumberingPreview({}); if (code) setFormData(prev => ({ ...prev, item_number: code })); }; @@ -315,6 +345,8 @@ export default function ItemInfoPage() { const openEditModal = (item: any) => { const raw = rawItems.find((r) => r.id === item.id) || item; setFormData({ ...raw }); + setManualInputValue(""); + setNumberingTemplate(""); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); @@ -325,13 +357,72 @@ 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(""); + setNumberingTemplate(""); setIsEditMode(false); setEditId(null); - const code = await loadNumberingPreview(); - if (code) setFormData(prev => ({ ...prev, item_number: code })); setIsModalOpen(true); + // 복사된 formData 기반으로 preview + const code = await loadNumberingPreview(rest); + if (code) setFormData(prev => ({ ...prev, item_number: code })); }; + // 카테고리 변경 시 채번 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 code = await loadNumberingPreview(formData, manualInputValue); + if (code) { + if (code.includes("____")) { + setNumberingTemplate(code); + const parts = code.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix })); + } else { + setFormData(prev => ({ ...prev, item_number: code })); + } + } + }, 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 (!numberingTemplate.includes("____")) return; + + const timer = setTimeout(async () => { + try { + const previewRes = await apiClient.post( + `/numbering-rules/${numberingRuleIdRef.current}/preview`, + { formData, manualInputValue: manualInputValue || undefined } + ); + const newCode = previewRes.data?.data?.generatedCode || ""; + if (newCode) { + setNumberingTemplate(newCode); + if (newCode.includes("____")) { + const parts = newCode.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix })); + } + } + } catch { /* ignore */ } + }, 500); + + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [manualInputValue]); + // 저장 const handleSave = async () => { if (!formData.item_name) { @@ -342,6 +433,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 +441,42 @@ export default function ItemInfoPage() { }); toast.success("수정되었어요."); } else { + // 신규 등록: allocateCode 호출하여 실제 순번 확보 + let finalItemNumber = formData.item_number || ""; + + if (numberingRuleIdRef.current) { + try { + const userInputCode = numberingTemplate.includes("____") + ? (() => { + const parts = numberingTemplate.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + return prefix + manualInputValue + suffix; + })() + : 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 +610,15 @@ export default function ItemInfoPage() { /> {/* 등록/수정 모달 */} - + { + setIsModalOpen(open); + if (!open) { + setNumberingTemplate(""); + setManualInputValue(""); + setNumberingRule(null); + numberingRuleIdRef.current = null; + } + }}> {isEditMode ? "품목 수정" : "품목 등록"} @@ -534,6 +668,61 @@ export default function ItemInfoPage() { placeholder={field.label} rows={3} /> + ) : field.type === "numbering" ? ( + // 채번 세그먼트 UI + isEditMode ? ( + + ) : isNumberingLoading ? ( +
+ + 생성 중... +
+ ) : numberingTemplate.includes("____") ? ( + (() => { + const tplParts = numberingTemplate.split("____"); + const prefix = tplParts[0] || ""; + const suffix = tplParts.slice(1).join("") || ""; + return ( +
+ {prefix && ( + + {prefix} + + )} + { + const val = e.target.value; + setManualInputValue(val); + setFormData(prev => ({ + ...prev, + item_number: prefix + val + suffix, + })); + }} + placeholder="입력" + className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none" + /> + {suffix && ( + + {suffix} + + )} +
+ ); + })() + ) : ( + + ) ) : ["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_7/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_7/production/plan-management/page.tsx index 546ed739..50be10ee 100644 --- a/frontend/app/(main)/COMPANY_7/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_7/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_7/sales/order/page.tsx b/frontend/app/(main)/COMPANY_7/sales/order/page.tsx index f9a358d4..1cb49efb 100644 --- a/frontend/app/(main)/COMPANY_7/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_7/sales/order/page.tsx @@ -249,7 +249,7 @@ export default function SalesOrderPage() { page: 1, size: 500, autoFilter: true, }); const custs = custRes.data?.data?.data || custRes.data?.data?.rows || []; - optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: `${c.customer_name} (${c.customer_code})` })); + optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: c.customer_name })); } catch { /* skip */ } // 사용자 목록 try { @@ -608,6 +608,24 @@ export default function SalesOrderPage() { try { const filters: any[] = []; if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword }); + + // 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링 + const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI"; + const partnerId = masterForm.partner_id; + let customerItemIds: Set | null = null; + + if (isCustomerPrice && partnerId) { + try { + const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, { + page: 1, size: 500, + dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] }, + autoFilter: true, + }); + const mappings = mappingRes.data?.data?.data || mappingRes.data?.data?.rows || []; + customerItemIds = new Set(mappings.map((m: any) => m.item_id).filter(Boolean)); + } catch { /* skip */ } + } + const res = await apiClient.post(`/table-management/tables/item_info/data`, { page: 1, size: 500, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, @@ -615,6 +633,12 @@ export default function SalesOrderPage() { }); const resData = res.data?.data; let allRows = resData?.data || resData?.rows || []; + + // 거래처우선일 때 연결된 품목만 표시 + if (customerItemIds) { + allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); + } + // 관리품목 필터 (코드/라벨 혼재 대응) if (itemSearchDivision !== "all") { const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; @@ -678,7 +702,8 @@ export default function SalesOrderPage() { autoFilter: true, }); const prices = res.data?.data?.data || res.data?.data?.rows || []; - const today = new Date().toISOString().slice(0, 10); + const _n = new Date(); + const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`; for (const p of prices) { const start = p.start_date || ""; const end = p.end_date || ""; @@ -768,7 +793,8 @@ export default function SalesOrderPage() { autoFilter: true, }); const prices = res.data?.data?.data || res.data?.data?.rows || []; - const today = new Date().toISOString().slice(0, 10); + const _n = new Date(); + const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`; const priceMap: Record = {}; for (const p of prices) { if (p.start_date && p.start_date > today) continue; diff --git a/frontend/app/(main)/COMPANY_8/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_8/master-data/item-info/page.tsx index 375fd900..c208192e 100644 --- a/frontend/app/(main)/COMPANY_8/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_8/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,13 @@ export default function ItemInfoPage() { // 선택된 행 const [selectedId, setSelectedId] = useState(null); + // 채번 관련 상태 + const [numberingRule, setNumberingRule] = useState(null); + const [numberingTemplate, setNumberingTemplate] = useState(""); + const [manualInputValue, setManualInputValue] = useState(""); + const [isNumberingLoading, setIsNumberingLoading] = useState(false); + const numberingRuleIdRef = useRef(null); + // 카테고리 옵션 로드 useEffect(() => { const loadCategories = async () => { @@ -288,26 +295,49 @@ 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 ""; + + // preview 호출 (formData + manualInputValue 전달) + const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { + formData: currentFormData || {}, + manualInputValue: currentManualValue || undefined, + }); + + const generatedCode = previewRes.data?.data?.generatedCode || ""; + setNumberingTemplate(generatedCode); + return generatedCode; } catch { /* 채번 규칙 없으면 무시 */ } + finally { + setIsNumberingLoading(false); + } return ""; }; // 등록 모달 열기 const openRegisterModal = async () => { setFormData({}); + setManualInputValue(""); + setNumberingTemplate(""); setIsEditMode(false); setEditId(null); setIsModalOpen(true); - // 채번 컬럼 자동 로드 - const code = await loadNumberingPreview(); + // 채번 미리보기 + const code = await loadNumberingPreview({}); if (code) setFormData(prev => ({ ...prev, item_number: code })); }; @@ -315,6 +345,8 @@ export default function ItemInfoPage() { const openEditModal = (item: any) => { const raw = rawItems.find((r) => r.id === item.id) || item; setFormData({ ...raw }); + setManualInputValue(""); + setNumberingTemplate(""); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); @@ -325,13 +357,72 @@ 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(""); + setNumberingTemplate(""); setIsEditMode(false); setEditId(null); - const code = await loadNumberingPreview(); - if (code) setFormData(prev => ({ ...prev, item_number: code })); setIsModalOpen(true); + // 복사된 formData 기반으로 preview + const code = await loadNumberingPreview(rest); + if (code) setFormData(prev => ({ ...prev, item_number: code })); }; + // 카테고리 변경 시 채번 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 code = await loadNumberingPreview(formData, manualInputValue); + if (code) { + if (code.includes("____")) { + setNumberingTemplate(code); + const parts = code.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix })); + } else { + setFormData(prev => ({ ...prev, item_number: code })); + } + } + }, 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 (!numberingTemplate.includes("____")) return; + + const timer = setTimeout(async () => { + try { + const previewRes = await apiClient.post( + `/numbering-rules/${numberingRuleIdRef.current}/preview`, + { formData, manualInputValue: manualInputValue || undefined } + ); + const newCode = previewRes.data?.data?.generatedCode || ""; + if (newCode) { + setNumberingTemplate(newCode); + if (newCode.includes("____")) { + const parts = newCode.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix })); + } + } + } catch { /* ignore */ } + }, 500); + + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [manualInputValue]); + // 저장 const handleSave = async () => { if (!formData.item_name) { @@ -342,6 +433,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 +441,42 @@ export default function ItemInfoPage() { }); toast.success("수정되었어요."); } else { + // 신규 등록: allocateCode 호출하여 실제 순번 확보 + let finalItemNumber = formData.item_number || ""; + + if (numberingRuleIdRef.current) { + try { + const userInputCode = numberingTemplate.includes("____") + ? (() => { + const parts = numberingTemplate.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + return prefix + manualInputValue + suffix; + })() + : 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 +610,15 @@ export default function ItemInfoPage() { /> {/* 등록/수정 모달 */} - + { + setIsModalOpen(open); + if (!open) { + setNumberingTemplate(""); + setManualInputValue(""); + setNumberingRule(null); + numberingRuleIdRef.current = null; + } + }}> {isEditMode ? "품목 수정" : "품목 등록"} @@ -534,6 +668,61 @@ export default function ItemInfoPage() { placeholder={field.label} rows={3} /> + ) : field.type === "numbering" ? ( + // 채번 세그먼트 UI + isEditMode ? ( + + ) : isNumberingLoading ? ( +
+ + 생성 중... +
+ ) : numberingTemplate.includes("____") ? ( + (() => { + const tplParts = numberingTemplate.split("____"); + const prefix = tplParts[0] || ""; + const suffix = tplParts.slice(1).join("") || ""; + return ( +
+ {prefix && ( + + {prefix} + + )} + { + const val = e.target.value; + setManualInputValue(val); + setFormData(prev => ({ + ...prev, + item_number: prefix + val + suffix, + })); + }} + placeholder="입력" + className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none" + /> + {suffix && ( + + {suffix} + + )} +
+ ); + })() + ) : ( + + ) ) : ["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_8/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx index 546ed739..50be10ee 100644 --- a/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_8/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_8/sales/order/page.tsx b/frontend/app/(main)/COMPANY_8/sales/order/page.tsx index f9a358d4..1cb49efb 100644 --- a/frontend/app/(main)/COMPANY_8/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_8/sales/order/page.tsx @@ -249,7 +249,7 @@ export default function SalesOrderPage() { page: 1, size: 500, autoFilter: true, }); const custs = custRes.data?.data?.data || custRes.data?.data?.rows || []; - optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: `${c.customer_name} (${c.customer_code})` })); + optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: c.customer_name })); } catch { /* skip */ } // 사용자 목록 try { @@ -608,6 +608,24 @@ export default function SalesOrderPage() { try { const filters: any[] = []; if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword }); + + // 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링 + const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI"; + const partnerId = masterForm.partner_id; + let customerItemIds: Set | null = null; + + if (isCustomerPrice && partnerId) { + try { + const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, { + page: 1, size: 500, + dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] }, + autoFilter: true, + }); + const mappings = mappingRes.data?.data?.data || mappingRes.data?.data?.rows || []; + customerItemIds = new Set(mappings.map((m: any) => m.item_id).filter(Boolean)); + } catch { /* skip */ } + } + const res = await apiClient.post(`/table-management/tables/item_info/data`, { page: 1, size: 500, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, @@ -615,6 +633,12 @@ export default function SalesOrderPage() { }); const resData = res.data?.data; let allRows = resData?.data || resData?.rows || []; + + // 거래처우선일 때 연결된 품목만 표시 + if (customerItemIds) { + allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); + } + // 관리품목 필터 (코드/라벨 혼재 대응) if (itemSearchDivision !== "all") { const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; @@ -678,7 +702,8 @@ export default function SalesOrderPage() { autoFilter: true, }); const prices = res.data?.data?.data || res.data?.data?.rows || []; - const today = new Date().toISOString().slice(0, 10); + const _n = new Date(); + const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`; for (const p of prices) { const start = p.start_date || ""; const end = p.end_date || ""; @@ -768,7 +793,8 @@ export default function SalesOrderPage() { autoFilter: true, }); const prices = res.data?.data?.data || res.data?.data?.rows || []; - const today = new Date().toISOString().slice(0, 10); + const _n = new Date(); + const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`; const priceMap: Record = {}; for (const p of prices) { if (p.start_date && p.start_date > today) continue; diff --git a/frontend/app/(main)/COMPANY_9/master-data/item-info/page.tsx b/frontend/app/(main)/COMPANY_9/master-data/item-info/page.tsx index 375fd900..c208192e 100644 --- a/frontend/app/(main)/COMPANY_9/master-data/item-info/page.tsx +++ b/frontend/app/(main)/COMPANY_9/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,13 @@ export default function ItemInfoPage() { // 선택된 행 const [selectedId, setSelectedId] = useState(null); + // 채번 관련 상태 + const [numberingRule, setNumberingRule] = useState(null); + const [numberingTemplate, setNumberingTemplate] = useState(""); + const [manualInputValue, setManualInputValue] = useState(""); + const [isNumberingLoading, setIsNumberingLoading] = useState(false); + const numberingRuleIdRef = useRef(null); + // 카테고리 옵션 로드 useEffect(() => { const loadCategories = async () => { @@ -288,26 +295,49 @@ 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 ""; + + // preview 호출 (formData + manualInputValue 전달) + const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { + formData: currentFormData || {}, + manualInputValue: currentManualValue || undefined, + }); + + const generatedCode = previewRes.data?.data?.generatedCode || ""; + setNumberingTemplate(generatedCode); + return generatedCode; } catch { /* 채번 규칙 없으면 무시 */ } + finally { + setIsNumberingLoading(false); + } return ""; }; // 등록 모달 열기 const openRegisterModal = async () => { setFormData({}); + setManualInputValue(""); + setNumberingTemplate(""); setIsEditMode(false); setEditId(null); setIsModalOpen(true); - // 채번 컬럼 자동 로드 - const code = await loadNumberingPreview(); + // 채번 미리보기 + const code = await loadNumberingPreview({}); if (code) setFormData(prev => ({ ...prev, item_number: code })); }; @@ -315,6 +345,8 @@ export default function ItemInfoPage() { const openEditModal = (item: any) => { const raw = rawItems.find((r) => r.id === item.id) || item; setFormData({ ...raw }); + setManualInputValue(""); + setNumberingTemplate(""); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); @@ -325,13 +357,72 @@ 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(""); + setNumberingTemplate(""); setIsEditMode(false); setEditId(null); - const code = await loadNumberingPreview(); - if (code) setFormData(prev => ({ ...prev, item_number: code })); setIsModalOpen(true); + // 복사된 formData 기반으로 preview + const code = await loadNumberingPreview(rest); + if (code) setFormData(prev => ({ ...prev, item_number: code })); }; + // 카테고리 변경 시 채번 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 code = await loadNumberingPreview(formData, manualInputValue); + if (code) { + if (code.includes("____")) { + setNumberingTemplate(code); + const parts = code.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix })); + } else { + setFormData(prev => ({ ...prev, item_number: code })); + } + } + }, 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 (!numberingTemplate.includes("____")) return; + + const timer = setTimeout(async () => { + try { + const previewRes = await apiClient.post( + `/numbering-rules/${numberingRuleIdRef.current}/preview`, + { formData, manualInputValue: manualInputValue || undefined } + ); + const newCode = previewRes.data?.data?.generatedCode || ""; + if (newCode) { + setNumberingTemplate(newCode); + if (newCode.includes("____")) { + const parts = newCode.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix })); + } + } + } catch { /* ignore */ } + }, 500); + + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [manualInputValue]); + // 저장 const handleSave = async () => { if (!formData.item_name) { @@ -342,6 +433,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 +441,42 @@ export default function ItemInfoPage() { }); toast.success("수정되었어요."); } else { + // 신규 등록: allocateCode 호출하여 실제 순번 확보 + let finalItemNumber = formData.item_number || ""; + + if (numberingRuleIdRef.current) { + try { + const userInputCode = numberingTemplate.includes("____") + ? (() => { + const parts = numberingTemplate.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.slice(1).join("") || ""; + return prefix + manualInputValue + suffix; + })() + : 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 +610,15 @@ export default function ItemInfoPage() { /> {/* 등록/수정 모달 */} - + { + setIsModalOpen(open); + if (!open) { + setNumberingTemplate(""); + setManualInputValue(""); + setNumberingRule(null); + numberingRuleIdRef.current = null; + } + }}> {isEditMode ? "품목 수정" : "품목 등록"} @@ -534,6 +668,61 @@ export default function ItemInfoPage() { placeholder={field.label} rows={3} /> + ) : field.type === "numbering" ? ( + // 채번 세그먼트 UI + isEditMode ? ( + + ) : isNumberingLoading ? ( +
+ + 생성 중... +
+ ) : numberingTemplate.includes("____") ? ( + (() => { + const tplParts = numberingTemplate.split("____"); + const prefix = tplParts[0] || ""; + const suffix = tplParts.slice(1).join("") || ""; + return ( +
+ {prefix && ( + + {prefix} + + )} + { + const val = e.target.value; + setManualInputValue(val); + setFormData(prev => ({ + ...prev, + item_number: prefix + val + suffix, + })); + }} + placeholder="입력" + className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none" + /> + {suffix && ( + + {suffix} + + )} +
+ ); + })() + ) : ( + + ) ) : ["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_9/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx index 546ed739..50be10ee 100644 --- a/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_9/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_9/sales/order/page.tsx b/frontend/app/(main)/COMPANY_9/sales/order/page.tsx index 6f4502bd..6fcbbe8f 100644 --- a/frontend/app/(main)/COMPANY_9/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_9/sales/order/page.tsx @@ -172,7 +172,7 @@ export default function JeilGlassOrderPage() { const custs = res.data?.data?.data || res.data?.data?.rows || []; optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, - label: `${c.customer_name} (${c.customer_code})`, + label: c.customer_name, })); } catch { /* skip */ } // 담당자 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 78c7c6fd..4cdf2d22 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -270,7 +270,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -312,7 +311,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -346,7 +344,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -3062,7 +3059,6 @@ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz", "integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.17.8", "@types/react-reconciler": "^0.32.0", @@ -3722,7 +3718,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.6.tgz", "integrity": "sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "5.90.6" }, @@ -3817,7 +3812,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.2.tgz", "integrity": "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -4161,7 +4155,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.2.tgz", "integrity": "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", @@ -6662,7 +6655,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -6673,7 +6665,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6716,7 +6707,6 @@ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz", "integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==", "license": "MIT", - "peer": true, "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", @@ -6799,7 +6789,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -7432,7 +7421,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8583,8 +8571,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/d3": { "version": "7.9.0", @@ -8906,7 +8893,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -9688,7 +9674,6 @@ "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9777,7 +9762,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9879,7 +9863,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -11036,7 +11019,6 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -11817,8 +11799,7 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/levn": { "version": "0.4.1", @@ -13137,7 +13118,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -13431,7 +13411,6 @@ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "license": "MIT", - "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -13461,7 +13440,6 @@ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -13510,7 +13488,6 @@ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz", "integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -13714,7 +13691,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13784,7 +13760,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -13835,7 +13810,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -13877,8 +13851,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-leaflet": { "version": "5.0.0", @@ -14186,7 +14159,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -14209,8 +14181,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/recharts/node_modules/redux-thunk": { "version": "3.1.0", @@ -15268,8 +15239,7 @@ "version": "0.180.0", "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", "integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/three-mesh-bvh": { "version": "0.8.3", @@ -15357,7 +15327,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15706,7 +15675,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver"