From 29c42cbb7909ca04ec3755fd92d33a5ae5b135e6 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Fri, 10 Apr 2026 10:43:12 +0900 Subject: [PATCH 1/9] 123 --- .../purchase/purchase-item/page.tsx | 463 +++++++++++++++--- .../COMPANY_16/sales/sales-item/page.tsx | 398 +++++++++++++-- frontend/components/layout/AppLayout.tsx | 138 +++++- 3 files changed, 883 insertions(+), 116 deletions(-) diff --git a/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx index 55f87c4a..2ce10b78 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx @@ -9,7 +9,7 @@ * 공급업체관리와 양방향 연동 (같은 supplier_item_mapping 테이블) */ -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"; @@ -137,7 +137,7 @@ function MultiCategoryCombobox({ options, value, onChange, placeholder }: { } 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" }, @@ -217,6 +217,13 @@ export default function PurchaseItemPage() { const [editId, setEditId] = useState(null); const [formData, setFormData] = useState>({}); + // 채번 관련 상태 + const [numberingRule, setNumberingRule] = useState(null); + const [numberingParts, setNumberingParts] = useState<{ value: string; isManual: boolean; separator: string }[]>([]); + const [manualInputValue, setManualInputValue] = useState(""); + const [isNumberingLoading, setIsNumberingLoading] = useState(false); + const numberingRuleIdRef = useRef(null); + // 검색 필터 (DynamicSearchFilter에서 관리) const [searchFilters, setSearchFilters] = useState([]); @@ -340,38 +347,194 @@ export default function PurchaseItemPage() { useEffect(() => { fetchItems(); }, [fetchItems]); - // 채번 미리보기 - const loadNumberingPreview = async () => { - try { - const ruleRes = await apiClient.get(`/numbering-rules/by-column/${ITEM_TABLE}/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 || ""; + // 프리뷰 코드에서 각 파트별 표시값을 추출 + const parsePreviewIntoParts = (previewCode: string, rule: any) => { + if (!previewCode || !rule?.parts) return []; + const sorted = [...rule.parts].sort((a: any, b: any) => a.order - b.order); + const globalSep = rule.separator || ""; + + const partMeta = sorted.map((part: any, idx: number) => { + const sep = idx < sorted.length - 1 + ? (part.separatorAfter ?? part.autoConfig?.separatorAfter ?? globalSep) + : ""; + const config = part.autoConfig || {}; + if (part.generationMethod === "manual") return { known: false, marker: "____", sep, isManual: true, partType: part.partType }; + switch (part.partType) { + case "text": return { known: true, value: config.textValue || "", sep, isManual: false, partType: "text" }; + case "number": return { known: true, value: String(config.numberValue || 1).padStart(config.numberLength || 3, "0"), sep, isManual: false, partType: "number" }; + case "date": { + const now = new Date(); + const y = String(now.getFullYear()), m = String(now.getMonth() + 1).padStart(2, "0"), d = String(now.getDate()).padStart(2, "0"); + const fmt = config.dateFormat || "YYYYMMDD"; + const map: Record = { YYYY: y, YY: y.slice(2), YYYYMM: y + m, YYMM: y.slice(2) + m, YYYYMMDD: y + m + d, YYMMDD: y.slice(2) + m + d }; + return { known: true, value: map[fmt] || y + m + d, sep, isManual: false, partType: "date" }; + } + default: return { known: false, sep, isManual: false, partType: part.partType }; } + }); + + let remaining = previewCode; + const results: { value: string; isManual: boolean; separator: string }[] = []; + + for (let i = 0; i < partMeta.length; i++) { + const meta = partMeta[i]; + const nextMeta = i < partMeta.length - 1 ? partMeta[i + 1] : null; + + if (meta.isManual) { + const markerIdx = remaining.indexOf("____"); + if (markerIdx >= 0) { + remaining = remaining.substring(markerIdx + 4); + if (meta.sep && remaining.startsWith(meta.sep)) remaining = remaining.substring(meta.sep.length); + } + results.push({ value: "", isManual: true, separator: meta.sep }); + continue; + } + + if (meta.known) { + const valIdx = remaining.indexOf(meta.value); + if (valIdx >= 0) { + remaining = remaining.substring(valIdx + meta.value.length); + if (meta.sep && remaining.startsWith(meta.sep)) remaining = remaining.substring(meta.sep.length); + } + results.push({ value: meta.value, isManual: false, separator: meta.sep }); + } else { + let endIdx = remaining.length; + if (meta.sep) { + if (nextMeta) { + if (nextMeta.known && nextMeta.value) { + const patIdx = remaining.indexOf(meta.sep + nextMeta.value); + if (patIdx >= 0) endIdx = patIdx; + } else if (nextMeta.isManual) { + const patIdx = remaining.indexOf(meta.sep + "____"); + if (patIdx >= 0) endIdx = patIdx; + } else { + const sepIdx = remaining.indexOf(meta.sep); + if (sepIdx >= 0) endIdx = sepIdx; + } + } + } else if (nextMeta) { + if (nextMeta.known && nextMeta.value) { + const valIdx = remaining.indexOf(nextMeta.value); + if (valIdx >= 0) endIdx = valIdx; + } else if (nextMeta.isManual) { + const markerIdx = remaining.indexOf("____"); + if (markerIdx >= 0) endIdx = markerIdx; + } + } + const extracted = remaining.substring(0, endIdx); + remaining = remaining.substring(endIdx); + if (meta.sep && remaining.startsWith(meta.sep)) remaining = remaining.substring(meta.sep.length); + results.push({ value: extracted, isManual: false, separator: meta.sep }); + } + } + return results; + }; + + // 파트 값으로부터 전체 코드 조합 + const buildCodeFromParts = (parts: { value: string; isManual: boolean; separator: string }[], manualVal: string) => { + return parts.map((p, idx) => { + const val = p.isManual ? manualVal : p.value; + const sep = idx < parts.length - 1 ? p.separator : ""; + return val + sep; + }).join(""); + }; + + // 채번 미리보기 + const loadNumberingPreview = async (currentFormData?: Record, currentManualValue?: string) => { + try { + setIsNumberingLoading(true); + + let rule = numberingRule; + if (!rule) { + const ruleRes = await apiClient.get(`/numbering-rules/by-column/${ITEM_TABLE}/item_number`); + rule = ruleRes.data?.data; + if (rule) { + setNumberingRule(rule); + numberingRuleIdRef.current = rule.ruleId; + } + } + + if (!rule?.ruleId) return { code: "", parts: [] as { value: string; isManual: boolean; separator: string }[] }; + + const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { + formData: currentFormData || {}, + manualInputValue: currentManualValue || undefined, + }); + + const generatedCode = previewRes.data?.data?.generatedCode || ""; + const parts = parsePreviewIntoParts(generatedCode, rule); + setNumberingParts(parts); + return { code: generatedCode, parts }; } catch { /* 채번 규칙 없으면 무시 */ } - return ""; + finally { + setIsNumberingLoading(false); + } + return { code: "", parts: [] as { value: string; isManual: boolean; separator: string }[] }; }; // 등록 모달 열기 const openRegisterModal = async () => { setFormData({}); + setManualInputValue(""); + setNumberingParts([]); setIsEditMode(false); setEditId(null); setIsModalOpen(true); - const code = await loadNumberingPreview(); - if (code) setFormData((prev) => ({ ...prev, item_number: code })); + const result = await loadNumberingPreview({}); + if (result.code) { + const hasManual = result.parts.some(p => p.isManual); + const displayCode = hasManual ? buildCodeFromParts(result.parts, "") : result.code; + setFormData(prev => ({ ...prev, item_number: displayCode })); + } }; // 수정 모달 열기 const openEditModal = (item: any) => { const raw = rawItems.find((r) => r.id === item.id) || item; setFormData({ ...raw }); + setManualInputValue(""); + setNumberingParts([]); setIsEditMode(true); setEditId(item.id); setIsModalOpen(true); }; + // 카테고리 변경 시 채번 preview 재호출 + useEffect(() => { + if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return; + + const hasCategoryPart = numberingRule?.parts?.some( + (p: any) => p.partType === "category" && p.generationMethod === "auto" + ); + if (!hasCategoryPart) return; + + const timer = setTimeout(async () => { + const result = await loadNumberingPreview(formData, manualInputValue); + if (result.parts.length > 0) { + setFormData(prev => ({ ...prev, item_number: buildCodeFromParts(result.parts, manualInputValue) })); + } + }, 300); + + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [...CATEGORY_COLUMNS.map(col => formData[col])]); + + // 수동 입력값 변경 시 preview 갱신 + useEffect(() => { + if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return; + if (!numberingParts.some(p => p.isManual)) return; + + const timer = setTimeout(async () => { + const result = await loadNumberingPreview(formData, manualInputValue); + if (result.parts.length > 0) { + setFormData(prev => ({ ...prev, item_number: buildCodeFromParts(result.parts, manualInputValue) })); + } + }, 500); + + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [manualInputValue]); + // 저장 (등록 또는 수정) const handleSave = async () => { if (!formData.item_name) { @@ -388,8 +551,38 @@ export default function PurchaseItemPage() { }); toast.success("수정되었어요."); } else { + // 신규 등록: allocateCode 호출하여 실제 순번 확보 + let finalItemNumber = formData.item_number || ""; + + if (numberingRuleIdRef.current) { + try { + const hasManual = numberingParts.some(p => p.isManual); + const userInputCode = hasManual && manualInputValue + ? manualInputValue + : undefined; + + const allocRes = await apiClient.post( + `/numbering-rules/${numberingRuleIdRef.current}/allocate`, + { formData, userInputCode } + ); + + if (allocRes.data?.success && allocRes.data?.data?.generatedCode) { + finalItemNumber = allocRes.data.data.generatedCode; + } + } catch (err) { + console.error("채번 할당 실패:", err); + toast.error("품목코드 할당에 실패했어요. 다시 시도해주세요."); + setSaving(false); + return; + } + } + const { id, created_date, updated_date, ...insertFields } = formData; - await apiClient.post(`/table-management/tables/${ITEM_TABLE}/add`, { id: crypto.randomUUID(), ...insertFields }); + await apiClient.post(`/table-management/tables/${ITEM_TABLE}/add`, { + id: crypto.randomUUID(), + ...insertFields, + item_number: finalItemNumber, + }); toast.success("등록되었어요."); } setIsModalOpen(false); @@ -664,25 +857,51 @@ export default function PurchaseItemPage() { if (found) custInfo = found; } catch { /* skip */ } - const mappingRows = [{ - _id: `m_existing_${row.id}`, - supplier_item_code: row.supplier_item_code || "", - supplier_item_name: row.supplier_item_name || "", - }].filter((m) => m.supplier_item_code || m.supplier_item_name); + // 매핑 전체 조회 + let mappingRows: any[] = []; + try { + const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, { + page: 1, size: 100, + dataFilter: { enabled: true, filters: [ + { columnName: "supplier_id", operator: "equals", value: custKey }, + { columnName: "item_id", operator: "equals", value: selectedItem!.item_number }, + ]}, autoFilter: true, + }); + const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || []; + mappingRows = allMappings + .filter((m: any) => m.supplier_item_code || m.supplier_item_name) + .map((m: any) => ({ + _id: `m_existing_${m.id}`, + supplier_item_code: m.supplier_item_code || "", + supplier_item_name: m.supplier_item_name || "", + })); + } catch { /* skip */ } - const priceRows = [{ - _id: `p_existing_${row.id}`, - start_date: row.start_date || "", - end_date: row.end_date || "", - currency_code: row.currency_code || "CAT_MLAMDKVN_PZJI", - base_price_type: row.base_price_type || "CAT_MLAMFGFT_4RZW", - base_price: row.base_price ? String(row.base_price) : "", - discount_type: row.discount_type || "", - discount_value: row.discount_value ? String(row.discount_value) : "", - rounding_type: row.rounding_type || "", - rounding_unit_value: row.rounding_unit_value || "", - calculated_price: row.calculated_price ? String(row.calculated_price) : "", - }].filter((p) => p.base_price || p.start_date); + // 단가 전체 조회 + let priceRows: any[] = []; + try { + const priceRes = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { + page: 1, size: 100, + dataFilter: { enabled: true, filters: [ + { columnName: "supplier_id", operator: "equals", value: custKey }, + { columnName: "item_id", operator: "equals", value: selectedItem!.item_number }, + ]}, autoFilter: true, + }); + const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || []; + priceRows = allPriceData.map((p: any) => ({ + _id: `p_existing_${p.id}`, + start_date: p.start_date ? String(p.start_date).split("T")[0] : "", + end_date: p.end_date ? String(p.end_date).split("T")[0] : "", + currency_code: p.currency_code || "CAT_MLAMDKVN_PZJI", + base_price_type: p.base_price_type || "CAT_MLAMFGFT_4RZW", + base_price: p.base_price ? String(p.base_price) : "", + discount_type: p.discount_type || "", + discount_value: p.discount_value ? String(p.discount_value) : "", + rounding_type: p.rounding_type || "", + rounding_unit_value: p.rounding_unit_value || "", + calculated_price: p.calculated_price ? String(p.calculated_price) : "", + })); + } catch { /* skip */ } if (priceRows.length === 0) { priceRows.push({ @@ -709,47 +928,104 @@ export default function PurchaseItemPage() { const mappingRows = suppMappings[custKey] || []; if (isEditingExisting && editSuppData?.id) { - await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, { - originalData: { id: editSuppData.id }, - updatedData: { - supplier_item_code: mappingRows[0]?.supplier_item_code || "", - supplier_item_name: mappingRows[0]?.supplier_item_name || "", - }, - }); - - // 기존 prices 삭제 후 재등록 + // 매핑: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE + const keptIds = new Set(); + let firstMappingId: string | null = null; + for (let mi = 0; mi < mappingRows.length; mi++) { + const m = mappingRows[mi]; + if (m._id?.startsWith("m_existing_")) { + const realId = m._id.replace("m_existing_", ""); + keptIds.add(realId); + if (mi === 0) firstMappingId = realId; + await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, { + originalData: { id: realId }, + updatedData: { + supplier_item_code: m.supplier_item_code || "", + supplier_item_name: m.supplier_item_name || "", + }, + }); + } else { + const res = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, { + id: crypto.randomUUID(), + supplier_id: custKey, item_id: selectedItem.item_number, + supplier_item_code: m.supplier_item_code || "", + supplier_item_name: m.supplier_item_name || "", + }); + if (mi === 0) firstMappingId = res.data?.data?.id || null; + } + } + // 폼에서 삭제된 매핑 → DB 삭제 try { - const existingPrices = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { + const dbMappings = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, { page: 1, size: 100, dataFilter: { enabled: true, filters: [ - { columnName: "mapping_id", operator: "equals", value: editSuppData.id }, + { columnName: "supplier_id", operator: "equals", value: custKey }, + { columnName: "item_id", operator: "equals", value: selectedItem.item_number }, ]}, autoFilter: true, }); - const existing = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || []; - if (existing.length > 0) { - await apiClient.delete(`/table-management/tables/supplier_item_prices/delete`, { - data: existing.map((p: any) => ({ id: p.id })), + const toDelete = (dbMappings.data?.data?.data || dbMappings.data?.data?.rows || []) + .filter((m: any) => !keptIds.has(m.id)); + if (toDelete.length > 0) { + await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, { + data: toDelete.map((m: any) => ({ id: m.id })), }); } } catch { /* skip */ } + if (!firstMappingId) firstMappingId = editSuppData.id; + + // 단가: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE + const keptPriceIds = new Set(); const priceRows = (suppPrices[custKey] || []).filter((p) => p.base_price || p.start_date || p.currency_code || p.base_price_type ); for (const price of priceRows) { - await apiClient.post(`/table-management/tables/supplier_item_prices/add`, { - id: crypto.randomUUID(), - mapping_id: editSuppData.id, - supplier_id: custKey, - item_id: selectedItem.item_number, - start_date: price.start_date || null, end_date: price.end_date || null, - currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, - base_price: price.base_price ? Number(price.base_price) : null, - discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, - rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, - calculated_price: price.calculated_price ? Number(price.calculated_price) : null, - }); + if (price._id?.startsWith("p_existing_")) { + const realId = price._id.replace("p_existing_", ""); + keptPriceIds.add(realId); + await apiClient.put(`/table-management/tables/supplier_item_prices/edit`, { + originalData: { id: realId }, + updatedData: { + mapping_id: firstMappingId || "", + start_date: price.start_date || null, end_date: price.end_date || null, + currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, + base_price: price.base_price ? Number(price.base_price) : null, + discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, + rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, + calculated_price: price.calculated_price ? Number(price.calculated_price) : null, + }, + }); + } else { + await apiClient.post(`/table-management/tables/supplier_item_prices/add`, { + id: crypto.randomUUID(), + mapping_id: firstMappingId || "", + supplier_id: custKey, item_id: selectedItem.item_number, + start_date: price.start_date || null, end_date: price.end_date || null, + currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, + base_price: price.base_price ? Number(price.base_price) : null, + discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, + rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, + calculated_price: price.calculated_price ? Number(price.calculated_price) : null, + }); + } } + // 폼에서 삭제된 단가 → DB 삭제 + try { + const dbPrices = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { + page: 1, size: 100, + dataFilter: { enabled: true, filters: [ + { columnName: "supplier_id", operator: "equals", value: custKey }, + { columnName: "item_id", operator: "equals", value: selectedItem.item_number }, + ]}, autoFilter: true, + }); + const toDeletePrices = (dbPrices.data?.data?.data || dbPrices.data?.data?.rows || []) + .filter((p: any) => !keptPriceIds.has(p.id)); + if (toDeletePrices.length > 0) { + await apiClient.delete(`/table-management/tables/supplier_item_prices/delete`, { + data: toDeletePrices.map((p: any) => ({ id: p.id })), + }); + } + } catch { /* skip */ } } else { // 신규 등록 const mappingRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, { @@ -1216,7 +1492,71 @@ export default function PurchaseItemPage() { {field.label} {"required" in field && field.required && *} - {field.type === "image" ? ( + {field.type === "numbering" ? ( + isEditMode ? ( + + ) : isNumberingLoading && numberingParts.length === 0 ? ( +
+ + 생성 중... +
+ ) : numberingParts.some(p => p.isManual) ? ( +
+ {numberingParts.map((part, idx) => { + const isFirst = idx === 0; + const isLast = idx === numberingParts.length - 1; + if (part.isManual) { + return ( + + { + const val = e.target.value; + setManualInputValue(val); + setFormData(prev => ({ + ...prev, + item_number: buildCodeFromParts(numberingParts, val), + })); + }} + placeholder="입력" + className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none" + /> + {part.separator && !isLast && ( + {part.separator} + )} + + ); + } + return ( + + + {part.value} + + {part.separator && !isLast && ( + {part.separator} + )} + + ); + })} +
+ ) : ( + + ) + ) : field.type === "image" ? ( setFormData((prev) => ({ ...prev, [field.key]: v }))} @@ -1260,8 +1600,7 @@ export default function PurchaseItemPage() { setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))} - placeholder={"placeholder" in field ? field.placeholder : ("disabled" in field && field.disabled ? "자동 채번" : field.label)} - disabled={"disabled" in field && field.disabled && !isEditMode} + placeholder={"placeholder" in field ? field.placeholder : field.label} className="h-9" /> )} diff --git a/frontend/app/(main)/COMPANY_16/sales/sales-item/page.tsx b/frontend/app/(main)/COMPANY_16/sales/sales-item/page.tsx index de4c1950..de794d28 100644 --- a/frontend/app/(main)/COMPANY_16/sales/sales-item/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/sales-item/page.tsx @@ -9,7 +9,7 @@ * 거래처관리와 양방향 연동 (같은 customer_item_mapping 테이블) */ -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"; @@ -157,7 +157,7 @@ const ITEM_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" }, @@ -241,6 +241,13 @@ export default function SalesItemPage() { const [editId, setEditId] = useState(null); const [saving, setSaving] = useState(false); + // 채번 관련 상태 + const [numberingRule, setNumberingRule] = useState(null); + const [numberingParts, setNumberingParts] = useState<{ value: string; isManual: boolean; separator: string }[]>([]); + const [manualInputValue, setManualInputValue] = useState(""); + const [isNumberingLoading, setIsNumberingLoading] = useState(false); + const numberingRuleIdRef = useRef(null); + // 엑셀 const [excelUploadOpen, setExcelUploadOpen] = useState(false); const [excelChainConfig, setExcelChainConfig] = useState(null); @@ -673,47 +680,104 @@ export default function SalesItemPage() { const mappingRows = custMappings[custKey] || []; if (isEditingExisting && editCustData?.id) { - await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, { - originalData: { id: editCustData.id }, - updatedData: { - customer_item_code: mappingRows[0]?.customer_item_code || "", - customer_item_name: mappingRows[0]?.customer_item_name || "", - }, - }); - - // 기존 prices 삭제 후 재등록 + // 매핑: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE + const keptIds = new Set(); + let firstMappingId: string | null = null; + for (let mi = 0; mi < mappingRows.length; mi++) { + const m = mappingRows[mi]; + if (m._id?.startsWith("m_existing_")) { + const realId = m._id.replace("m_existing_", ""); + keptIds.add(realId); + if (mi === 0) firstMappingId = realId; + await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, { + originalData: { id: realId }, + updatedData: { + customer_item_code: m.customer_item_code || "", + customer_item_name: m.customer_item_name || "", + }, + }); + } else { + const res = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, { + id: crypto.randomUUID(), + customer_id: custKey, item_id: selectedItem.item_number, + customer_item_code: m.customer_item_code || "", + customer_item_name: m.customer_item_name || "", + }); + if (mi === 0) firstMappingId = res.data?.data?.id || null; + } + } + // 폼에서 삭제된 매핑 → DB 삭제 try { - const existingPrices = await apiClient.post(`/table-management/tables/customer_item_prices/data`, { + const dbMappings = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, { page: 1, size: 100, dataFilter: { enabled: true, filters: [ - { columnName: "mapping_id", operator: "equals", value: editCustData.id }, + { columnName: "customer_id", operator: "equals", value: custKey }, + { columnName: "item_id", operator: "equals", value: selectedItem.item_number }, ]}, autoFilter: true, }); - const existing = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || []; - if (existing.length > 0) { - await apiClient.delete(`/table-management/tables/customer_item_prices/delete`, { - data: existing.map((p: any) => ({ id: p.id })), + const toDelete = (dbMappings.data?.data?.data || dbMappings.data?.data?.rows || []) + .filter((m: any) => !keptIds.has(m.id)); + if (toDelete.length > 0) { + await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, { + data: toDelete.map((m: any) => ({ id: m.id })), }); } } catch { /* skip */ } + if (!firstMappingId) firstMappingId = editCustData.id; + + // 단가: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE + const keptPriceIds = new Set(); const priceRows = (custPrices[custKey] || []).filter((p) => p.base_price || p.start_date || p.currency_code || p.base_price_type ); for (const price of priceRows) { - await apiClient.post(`/table-management/tables/customer_item_prices/add`, { - id: crypto.randomUUID(), - mapping_id: editCustData.id, - customer_id: custKey, - item_id: selectedItem.item_number, - start_date: price.start_date || null, end_date: price.end_date || null, - currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, - base_price: price.base_price ? Number(price.base_price) : null, - discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, - rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, - calculated_price: price.calculated_price ? Number(price.calculated_price) : null, - }); + if (price._id?.startsWith("p_existing_")) { + const realId = price._id.replace("p_existing_", ""); + keptPriceIds.add(realId); + await apiClient.put(`/table-management/tables/customer_item_prices/edit`, { + originalData: { id: realId }, + updatedData: { + mapping_id: firstMappingId || "", + start_date: price.start_date || null, end_date: price.end_date || null, + currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, + base_price: price.base_price ? Number(price.base_price) : null, + discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, + rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, + calculated_price: price.calculated_price ? Number(price.calculated_price) : null, + }, + }); + } else { + await apiClient.post(`/table-management/tables/customer_item_prices/add`, { + id: crypto.randomUUID(), + mapping_id: firstMappingId || "", + customer_id: custKey, item_id: selectedItem.item_number, + start_date: price.start_date || null, end_date: price.end_date || null, + currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, + base_price: price.base_price ? Number(price.base_price) : null, + discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, + rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, + calculated_price: price.calculated_price ? Number(price.calculated_price) : null, + }); + } } + // 폼에서 삭제된 단가 → DB 삭제 + try { + const dbPrices = await apiClient.post(`/table-management/tables/customer_item_prices/data`, { + page: 1, size: 100, + dataFilter: { enabled: true, filters: [ + { columnName: "customer_id", operator: "equals", value: custKey }, + { columnName: "item_id", operator: "equals", value: selectedItem.item_number }, + ]}, autoFilter: true, + }); + const toDeletePrices = (dbPrices.data?.data?.data || dbPrices.data?.data?.rows || []) + .filter((p: any) => !keptPriceIds.has(p.id)); + if (toDeletePrices.length > 0) { + await apiClient.delete(`/table-management/tables/customer_item_prices/delete`, { + data: toDeletePrices.map((p: any) => ({ id: p.id })), + }); + } + } catch { /* skip */ } } else { // 신규 등록 const mappingRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, { @@ -765,27 +829,145 @@ export default function SalesItemPage() { } }; - // 채번 미리보기 로드 - const loadNumberingPreview = async () => { - try { - const ruleRes = await apiClient.get(`/numbering-rules/by-column/${ITEM_TABLE}/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 || ""; + // 프리뷰 코드에서 각 파트별 표시값을 추출 + const parsePreviewIntoParts = (previewCode: string, rule: any) => { + if (!previewCode || !rule?.parts) return []; + const sorted = [...rule.parts].sort((a: any, b: any) => a.order - b.order); + const globalSep = rule.separator || ""; + + const partMeta = sorted.map((part: any, idx: number) => { + const sep = idx < sorted.length - 1 + ? (part.separatorAfter ?? part.autoConfig?.separatorAfter ?? globalSep) + : ""; + const config = part.autoConfig || {}; + if (part.generationMethod === "manual") return { known: false, marker: "____", sep, isManual: true, partType: part.partType }; + switch (part.partType) { + case "text": return { known: true, value: config.textValue || "", sep, isManual: false, partType: "text" }; + case "number": return { known: true, value: String(config.numberValue || 1).padStart(config.numberLength || 3, "0"), sep, isManual: false, partType: "number" }; + case "date": { + const now = new Date(); + const y = String(now.getFullYear()), m = String(now.getMonth() + 1).padStart(2, "0"), d = String(now.getDate()).padStart(2, "0"); + const fmt = config.dateFormat || "YYYYMMDD"; + const map: Record = { YYYY: y, YY: y.slice(2), YYYYMM: y + m, YYMM: y.slice(2) + m, YYYYMMDD: y + m + d, YYMMDD: y.slice(2) + m + d }; + return { known: true, value: map[fmt] || y + m + d, sep, isManual: false, partType: "date" }; + } + default: return { known: false, sep, isManual: false, partType: part.partType }; } + }); + + let remaining = previewCode; + const results: { value: string; isManual: boolean; separator: string }[] = []; + + for (let i = 0; i < partMeta.length; i++) { + const meta = partMeta[i]; + const nextMeta = i < partMeta.length - 1 ? partMeta[i + 1] : null; + + if (meta.isManual) { + const markerIdx = remaining.indexOf("____"); + if (markerIdx >= 0) { + remaining = remaining.substring(markerIdx + 4); + if (meta.sep && remaining.startsWith(meta.sep)) remaining = remaining.substring(meta.sep.length); + } + results.push({ value: "", isManual: true, separator: meta.sep }); + continue; + } + + if (meta.known) { + const valIdx = remaining.indexOf(meta.value); + if (valIdx >= 0) { + remaining = remaining.substring(valIdx + meta.value.length); + if (meta.sep && remaining.startsWith(meta.sep)) remaining = remaining.substring(meta.sep.length); + } + results.push({ value: meta.value, isManual: false, separator: meta.sep }); + } else { + let endIdx = remaining.length; + if (meta.sep) { + if (nextMeta) { + if (nextMeta.known && nextMeta.value) { + const patIdx = remaining.indexOf(meta.sep + nextMeta.value); + if (patIdx >= 0) endIdx = patIdx; + } else if (nextMeta.isManual) { + const patIdx = remaining.indexOf(meta.sep + "____"); + if (patIdx >= 0) endIdx = patIdx; + } else { + const sepIdx = remaining.indexOf(meta.sep); + if (sepIdx >= 0) endIdx = sepIdx; + } + } + } else if (nextMeta) { + if (nextMeta.known && nextMeta.value) { + const valIdx = remaining.indexOf(nextMeta.value); + if (valIdx >= 0) endIdx = valIdx; + } else if (nextMeta.isManual) { + const markerIdx = remaining.indexOf("____"); + if (markerIdx >= 0) endIdx = markerIdx; + } + } + const extracted = remaining.substring(0, endIdx); + remaining = remaining.substring(endIdx); + if (meta.sep && remaining.startsWith(meta.sep)) remaining = remaining.substring(meta.sep.length); + results.push({ value: extracted, isManual: false, separator: meta.sep }); + } + } + return results; + }; + + // 파트 값으로부터 전체 코드 조합 + const buildCodeFromParts = (parts: { value: string; isManual: boolean; separator: string }[], manualVal: string) => { + return parts.map((p, idx) => { + const val = p.isManual ? manualVal : p.value; + const sep = idx < parts.length - 1 ? p.separator : ""; + return val + sep; + }).join(""); + }; + + // 채번 미리보기 로드 + const loadNumberingPreview = async (currentFormData?: Record, currentManualValue?: string) => { + try { + setIsNumberingLoading(true); + + let rule = numberingRule; + if (!rule) { + const ruleRes = await apiClient.get(`/numbering-rules/by-column/${ITEM_TABLE}/item_number`); + rule = ruleRes.data?.data; + if (rule) { + setNumberingRule(rule); + numberingRuleIdRef.current = rule.ruleId; + } + } + + if (!rule?.ruleId) return { code: "", parts: [] as { value: string; isManual: boolean; separator: string }[] }; + + const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { + formData: currentFormData || {}, + manualInputValue: currentManualValue || undefined, + }); + + const generatedCode = previewRes.data?.data?.generatedCode || ""; + const parts = parsePreviewIntoParts(generatedCode, rule); + setNumberingParts(parts); + return { code: generatedCode, parts }; } catch { /* 채번 규칙 없으면 무시 */ } - return ""; + finally { + setIsNumberingLoading(false); + } + return { code: "", parts: [] as { value: string; isManual: boolean; separator: string }[] }; }; // 품목 등록 모달 열기 const openRegisterModal = async () => { setEditItemForm({}); + setManualInputValue(""); + setNumberingParts([]); setIsEditMode(false); setEditId(null); setEditItemOpen(true); - const code = await loadNumberingPreview(); - if (code) setEditItemForm(prev => ({ ...prev, item_number: code })); + const result = await loadNumberingPreview({}); + if (result.code) { + const hasManual = result.parts.some(p => p.isManual); + const displayCode = hasManual ? buildCodeFromParts(result.parts, "") : result.code; + setEditItemForm(prev => ({ ...prev, item_number: displayCode })); + } }; // 품목 수정 모달 열기 @@ -794,11 +976,49 @@ export default function SalesItemPage() { if (!target) return; const raw = rawItems.find((r) => r.id === target.id) || target; setEditItemForm({ ...raw }); + setManualInputValue(""); + setNumberingParts([]); setIsEditMode(true); setEditId(target.id); setEditItemOpen(true); }; + // 카테고리 변경 시 채번 preview 재호출 + useEffect(() => { + if (isEditMode || !editItemOpen || !numberingRuleIdRef.current) return; + + const hasCategoryPart = numberingRule?.parts?.some( + (p: any) => p.partType === "category" && p.generationMethod === "auto" + ); + if (!hasCategoryPart) return; + + const timer = setTimeout(async () => { + const result = await loadNumberingPreview(editItemForm, manualInputValue); + if (result.parts.length > 0) { + setEditItemForm(prev => ({ ...prev, item_number: buildCodeFromParts(result.parts, manualInputValue) })); + } + }, 300); + + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [...CATEGORY_COLUMNS_FOR_MODAL.map(col => editItemForm[col])]); + + // 수동 입력값 변경 시 preview 갱신 + useEffect(() => { + if (isEditMode || !editItemOpen || !numberingRuleIdRef.current) return; + if (!numberingParts.some(p => p.isManual)) return; + + const timer = setTimeout(async () => { + const result = await loadNumberingPreview(editItemForm, manualInputValue); + if (result.parts.length > 0) { + setEditItemForm(prev => ({ ...prev, item_number: buildCodeFromParts(result.parts, manualInputValue) })); + } + }, 500); + + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [manualInputValue]); + // 품목 저장 (등록 + 수정 통합) const handleEditSave = async () => { if (!editItemForm.item_name) { @@ -815,8 +1035,38 @@ export default function SalesItemPage() { }); toast.success("수정되었어요."); } else { + // 신규 등록: allocateCode 호출하여 실제 순번 확보 + let finalItemNumber = editItemForm.item_number || ""; + + if (numberingRuleIdRef.current) { + try { + const hasManual = numberingParts.some(p => p.isManual); + const userInputCode = hasManual && manualInputValue + ? manualInputValue + : undefined; + + const allocRes = await apiClient.post( + `/numbering-rules/${numberingRuleIdRef.current}/allocate`, + { formData: editItemForm, 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 } = editItemForm; - await apiClient.post(`/table-management/tables/${ITEM_TABLE}/add`, { id: crypto.randomUUID(), ...insertFields }); + await apiClient.post(`/table-management/tables/${ITEM_TABLE}/add`, { + id: crypto.randomUUID(), + ...insertFields, + item_number: finalItemNumber, + }); toast.success("등록되었어요."); } setEditItemOpen(false); @@ -1238,7 +1488,71 @@ export default function SalesItemPage() { {field.label} {field.required && *} - {field.type === "image" ? ( + {field.type === "numbering" ? ( + isEditMode ? ( + + ) : isNumberingLoading && numberingParts.length === 0 ? ( +
+ + 생성 중... +
+ ) : numberingParts.some(p => p.isManual) ? ( +
+ {numberingParts.map((part, idx) => { + const isFirst = idx === 0; + const isLast = idx === numberingParts.length - 1; + if (part.isManual) { + return ( + + { + const val = e.target.value; + setManualInputValue(val); + setEditItemForm(prev => ({ + ...prev, + item_number: buildCodeFromParts(numberingParts, val), + })); + }} + placeholder="입력" + className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none" + /> + {part.separator && !isLast && ( + {part.separator} + )} + + ); + } + return ( + + + {part.value} + + {part.separator && !isLast && ( + {part.separator} + )} + + ); + })} +
+ ) : ( + + ) + ) : field.type === "image" ? ( setEditItemForm((prev) => ({ ...prev, [field.key]: v }))} diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index 857dc07b..46921770 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -1,6 +1,7 @@ "use client"; -import React, { useState, Suspense, useEffect, useCallback, useRef } from "react"; +import React, { useState, Suspense, useEffect, useCallback, useRef, useMemo } from "react"; +import { createPortal } from "react-dom"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { @@ -262,6 +263,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { return false; }); const [hasPopMenus, setHasPopMenus] = useState(false); + const [hoveredCollapsedMenu, setHoveredCollapsedMenu] = useState(null); const toggleSidebarCollapse = () => { setSidebarCollapsed((prev) => { @@ -623,22 +625,73 @@ function AppLayoutInner({ children }: AppLayoutProps) { }; // 축소 상태 메뉴 렌더링 (아이콘만, hover 시 오버레이 메뉴로 조작) + const collapsedMenuRefs = useRef>({}); + const hoverTimeoutRef = useRef | null>(null); + + const handleCollapsedMouseEnter = useCallback((menuId: string) => { + if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current); + setHoveredCollapsedMenu(menuId); + }, []); + + const handleCollapsedMouseLeave = useCallback(() => { + hoverTimeoutRef.current = setTimeout(() => setHoveredCollapsedMenu(null), 150); + }, []); + const renderCollapsedMenu = (menu: any) => { const isActive = isMenuActive(menu); const hasActiveChild = menu.hasChildren && menu.children?.some((child: any) => isMenuActive(child)); + const isHovered = hoveredCollapsedMenu === menu.id; return ( - + + + {isHovered && typeof document !== "undefined" && createPortal( +
handleCollapsedMouseEnter(menu.id)} + onMouseLeave={handleCollapsedMouseLeave} + > +
+ {menu.name} +
+ {menu.hasChildren && menu.children?.map((child: any) => ( + + ))} +
, + document.body + )} + ); }; @@ -794,8 +847,69 @@ function AppLayoutInner({ children }: AppLayoutProps) { )} - {/* 확장 상태 전용: 관리 회사, 모드 전환 버튼 */} - {(!isMobile && sidebarCollapsed) ? null : ( + {/* 관리 회사, 모드 전환 버튼 */} + {(!isMobile && sidebarCollapsed) ? ( + /* 축소 상태: 관리자 아이콘 + hover 팝오버 */ + ((user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" || + (user as ExtendedUserInfo)?.userType === "COMPANY_ADMIN" || + (user as ExtendedUserInfo)?.userType === "admin") ? ( +
{ collapsedMenuRefs.current["__admin__"] = el; }} + onMouseEnter={() => handleCollapsedMouseEnter("__admin__")} + onMouseLeave={handleCollapsedMouseLeave} + > + + + {hoveredCollapsedMenu === "__admin__" && typeof document !== "undefined" && createPortal( +
handleCollapsedMouseEnter("__admin__")} + onMouseLeave={handleCollapsedMouseLeave} + > + {(user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" && ( +
+

현재 관리 회사

+

{currentCompanyName || "로딩 중..."}

+
+ )} + + {(user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" && ( + + )} +
, + document.body + )} +
+ ) : null + ) : ( <> {(user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" && (
From f4162417884acdcae635c3556a6b5647616626db Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 10 Apr 2026 11:12:49 +0900 Subject: [PATCH 2/9] fix: Reset related fields on input mode change in Sales Order page - Updated the input mode selection logic to clear associated fields (partner_id, delivery_partner_id, delivery_address) when the input mode changes. - This change ensures that the form state is correctly managed and prevents stale data from being retained, enhancing the user experience across multiple companies. These modifications aim to improve the clarity and functionality of the Sales Order page by ensuring that changes in input mode reflect accurately in the form state. --- frontend/app/(main)/COMPANY_10/sales/order/page.tsx | 11 ++++++++++- frontend/app/(main)/COMPANY_16/sales/order/page.tsx | 11 ++++++++++- frontend/app/(main)/COMPANY_29/sales/order/page.tsx | 11 ++++++++++- frontend/app/(main)/COMPANY_7/sales/order/page.tsx | 12 +++++++++++- frontend/app/(main)/COMPANY_8/sales/order/page.tsx | 11 ++++++++++- 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/frontend/app/(main)/COMPANY_10/sales/order/page.tsx b/frontend/app/(main)/COMPANY_10/sales/order/page.tsx index 1cb49efb..949e7e79 100644 --- a/frontend/app/(main)/COMPANY_10/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_10/sales/order/page.tsx @@ -1104,7 +1104,16 @@ export default function SalesOrderPage() {
- { + setMasterForm((p) => { + const next = { ...p, input_mode: v }; + delete next.partner_id; + delete next.delivery_partner_id; + delete next.delivery_address; + return next; + }); + setDeliveryOptions([]); + }}> {(categoryOptions["input_mode"] || []).map((o) => ( diff --git a/frontend/app/(main)/COMPANY_16/sales/order/page.tsx b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx index 1cb49efb..949e7e79 100644 --- a/frontend/app/(main)/COMPANY_16/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx @@ -1104,7 +1104,16 @@ export default function SalesOrderPage() {
- { + setMasterForm((p) => { + const next = { ...p, input_mode: v }; + delete next.partner_id; + delete next.delivery_partner_id; + delete next.delivery_address; + return next; + }); + setDeliveryOptions([]); + }}> {(categoryOptions["input_mode"] || []).map((o) => ( diff --git a/frontend/app/(main)/COMPANY_29/sales/order/page.tsx b/frontend/app/(main)/COMPANY_29/sales/order/page.tsx index 1cb49efb..949e7e79 100644 --- a/frontend/app/(main)/COMPANY_29/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_29/sales/order/page.tsx @@ -1104,7 +1104,16 @@ export default function SalesOrderPage() {
- { + setMasterForm((p) => { + const next = { ...p, input_mode: v }; + delete next.partner_id; + delete next.delivery_partner_id; + delete next.delivery_address; + return next; + }); + setDeliveryOptions([]); + }}> {(categoryOptions["input_mode"] || []).map((o) => ( diff --git a/frontend/app/(main)/COMPANY_7/sales/order/page.tsx b/frontend/app/(main)/COMPANY_7/sales/order/page.tsx index 984e359c..81211602 100644 --- a/frontend/app/(main)/COMPANY_7/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_7/sales/order/page.tsx @@ -1104,7 +1104,17 @@ export default function SalesOrderPage() {
- { + setMasterForm((p) => { + const next = { ...p, input_mode: v }; + // 입력방식 변경 시 거래처 관련 값 초기화 + delete next.partner_id; + delete next.delivery_partner_id; + delete next.delivery_address; + return next; + }); + setDeliveryOptions([]); + }}> {(categoryOptions["input_mode"] || []).map((o) => ( diff --git a/frontend/app/(main)/COMPANY_8/sales/order/page.tsx b/frontend/app/(main)/COMPANY_8/sales/order/page.tsx index 1cb49efb..949e7e79 100644 --- a/frontend/app/(main)/COMPANY_8/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_8/sales/order/page.tsx @@ -1104,7 +1104,16 @@ export default function SalesOrderPage() {
- { + setMasterForm((p) => { + const next = { ...p, input_mode: v }; + delete next.partner_id; + delete next.delivery_partner_id; + delete next.delivery_address; + return next; + }); + setDeliveryOptions([]); + }}> {(categoryOptions["input_mode"] || []).map((o) => ( From c8b1b1b742253301c1f7a5ce8d2fb8dc3660248d Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Fri, 10 Apr 2026 13:34:01 +0900 Subject: [PATCH 3/9] 123 --- .../purchase/purchase-item/page.tsx | 144 +++++++------ .../COMPANY_16/sales/sales-item/page.tsx | 202 +++++++++++------- frontend/components/layout/AppLayout.tsx | 6 +- frontend/components/layout/ThemeToggle.tsx | 4 +- 4 files changed, 200 insertions(+), 156 deletions(-) diff --git a/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx index 2ce10b78..1e44c7cd 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx @@ -616,6 +616,7 @@ export default function PurchaseItemPage() { page: 1, size: 500, dataFilter: { enabled: true, filters: [{ columnName: "item_id", operator: "equals", value: itemKey }] }, autoFilter: true, + sort: { columnName: "created_date", order: "asc" }, }); const mappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || []; @@ -866,6 +867,7 @@ export default function PurchaseItemPage() { { columnName: "supplier_id", operator: "equals", value: custKey }, { columnName: "item_id", operator: "equals", value: selectedItem!.item_number }, ]}, autoFilter: true, + sort: { columnName: "created_date", order: "asc" }, }); const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || []; mappingRows = allMappings @@ -887,7 +889,8 @@ export default function PurchaseItemPage() { { columnName: "item_id", operator: "equals", value: selectedItem!.item_number }, ]}, autoFilter: true, }); - const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || []; + const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || []) + .sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || "")); priceRows = allPriceData.map((p: any) => ({ _id: `p_existing_${p.id}`, start_date: p.start_date ? String(p.start_date).split("T")[0] : "", @@ -928,104 +931,104 @@ export default function PurchaseItemPage() { const mappingRows = suppMappings[custKey] || []; if (isEditingExisting && editSuppData?.id) { - // 매핑: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE - const keptIds = new Set(); - let firstMappingId: string | null = null; + // 기존 매핑 조회 + let existingMaps: any[] = []; + try { + const existingMappings = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, { + page: 1, size: 100, + dataFilter: { enabled: true, filters: [ + { columnName: "supplier_id", operator: "equals", value: custKey }, + { columnName: "item_id", operator: "equals", value: selectedItem.item_number }, + ]}, autoFilter: true, + sort: { columnName: "created_date", order: "asc" }, + }); + existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || []; + } catch { /* skip */ } + + // 매핑 upsert: 인덱스 기반 + const usedExistingIds = new Set(); + let firstMappingId: string | null = editSuppData.id; for (let mi = 0; mi < mappingRows.length; mi++) { - const m = mappingRows[mi]; - if (m._id?.startsWith("m_existing_")) { - const realId = m._id.replace("m_existing_", ""); - keptIds.add(realId); - if (mi === 0) firstMappingId = realId; + const existMap = existingMaps[mi]; + if (existMap) { await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, { - originalData: { id: realId }, + originalData: { id: existMap.id }, updatedData: { - supplier_item_code: m.supplier_item_code || "", - supplier_item_name: m.supplier_item_name || "", + supplier_item_code: mappingRows[mi].supplier_item_code || "", + supplier_item_name: mappingRows[mi].supplier_item_name || "", }, }); + usedExistingIds.add(existMap.id); + if (mi === 0) firstMappingId = existMap.id; } else { - const res = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, { + const mRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, { id: crypto.randomUUID(), supplier_id: custKey, item_id: selectedItem.item_number, - supplier_item_code: m.supplier_item_code || "", - supplier_item_name: m.supplier_item_name || "", + supplier_item_code: mappingRows[mi].supplier_item_code || "", + supplier_item_name: mappingRows[mi].supplier_item_name || "", }); - if (mi === 0) firstMappingId = res.data?.data?.id || null; + if (mi === 0 && !firstMappingId) firstMappingId = mRes.data?.data?.id || null; } } - // 폼에서 삭제된 매핑 → DB 삭제 + // 초과분 delete + const toDeleteMaps = existingMaps.filter((m) => !usedExistingIds.has(m.id)); + if (toDeleteMaps.length > 0) { + await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, { + data: toDeleteMaps.map((m: any) => ({ id: m.id })), + }); + } + + // 기존 단가 조회 + let existingPriceRows: any[] = []; try { - const dbMappings = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, { + const existingPrices = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { page: 1, size: 100, dataFilter: { enabled: true, filters: [ { columnName: "supplier_id", operator: "equals", value: custKey }, { columnName: "item_id", operator: "equals", value: selectedItem.item_number }, ]}, autoFilter: true, }); - const toDelete = (dbMappings.data?.data?.data || dbMappings.data?.data?.rows || []) - .filter((m: any) => !keptIds.has(m.id)); - if (toDelete.length > 0) { - await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, { - data: toDelete.map((m: any) => ({ id: m.id })), - }); - } + existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || []; } catch { /* skip */ } - if (!firstMappingId) firstMappingId = editSuppData.id; - - // 단가: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE - const keptPriceIds = new Set(); + // 단가 upsert: 인덱스 기반 const priceRows = (suppPrices[custKey] || []).filter((p) => p.base_price || p.start_date || p.currency_code || p.base_price_type ); - for (const price of priceRows) { - if (price._id?.startsWith("p_existing_")) { - const realId = price._id.replace("p_existing_", ""); - keptPriceIds.add(realId); + const usedPriceIds = new Set(); + for (let pi = 0; pi < priceRows.length; pi++) { + const price = priceRows[pi]; + const priceData = { + mapping_id: firstMappingId || editSuppData.id, + supplier_id: custKey, item_id: selectedItem.item_number, + start_date: price.start_date || null, end_date: price.end_date || null, + currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, + base_price: price.base_price ? Number(price.base_price) : null, + unit_price: price.calculated_price ? Number(price.calculated_price) : (price.base_price ? Number(price.base_price) : null), + discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, + rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, + calculated_price: price.calculated_price ? Number(price.calculated_price) : null, + }; + const existPrice = existingPriceRows[pi]; + if (existPrice) { await apiClient.put(`/table-management/tables/supplier_item_prices/edit`, { - originalData: { id: realId }, - updatedData: { - mapping_id: firstMappingId || "", - start_date: price.start_date || null, end_date: price.end_date || null, - currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, - base_price: price.base_price ? Number(price.base_price) : null, - discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, - rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, - calculated_price: price.calculated_price ? Number(price.calculated_price) : null, - }, + originalData: { id: existPrice.id }, + updatedData: priceData, }); + usedPriceIds.add(existPrice.id); } else { await apiClient.post(`/table-management/tables/supplier_item_prices/add`, { - id: crypto.randomUUID(), - mapping_id: firstMappingId || "", - supplier_id: custKey, item_id: selectedItem.item_number, - start_date: price.start_date || null, end_date: price.end_date || null, - currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, - base_price: price.base_price ? Number(price.base_price) : null, - discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, - rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, - calculated_price: price.calculated_price ? Number(price.calculated_price) : null, + id: crypto.randomUUID(), ...priceData, }); } } - // 폼에서 삭제된 단가 → DB 삭제 - try { - const dbPrices = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, { - page: 1, size: 100, - dataFilter: { enabled: true, filters: [ - { columnName: "supplier_id", operator: "equals", value: custKey }, - { columnName: "item_id", operator: "equals", value: selectedItem.item_number }, - ]}, autoFilter: true, + // 초과분 delete + const toDeletePrices = existingPriceRows.filter((p) => !usedPriceIds.has(p.id)); + if (toDeletePrices.length > 0) { + await apiClient.delete(`/table-management/tables/supplier_item_prices/delete`, { + data: toDeletePrices.map((p: any) => ({ id: p.id })), }); - const toDeletePrices = (dbPrices.data?.data?.data || dbPrices.data?.data?.rows || []) - .filter((p: any) => !keptPriceIds.has(p.id)); - if (toDeletePrices.length > 0) { - await apiClient.delete(`/table-management/tables/supplier_item_prices/delete`, { - data: toDeletePrices.map((p: any) => ({ id: p.id })), - }); - } - } catch { /* skip */ } + } } else { // 신규 등록 const mappingRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, { @@ -1055,6 +1058,7 @@ export default function PurchaseItemPage() { start_date: price.start_date || null, end_date: price.end_date || null, currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, base_price: price.base_price ? Number(price.base_price) : null, + unit_price: price.calculated_price ? Number(price.calculated_price) : (price.base_price ? Number(price.base_price) : null), discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, calculated_price: price.calculated_price ? Number(price.calculated_price) : null, @@ -1719,7 +1723,7 @@ export default function PurchaseItemPage() { {/* ── 공급업체 상세 입력/수정 모달 ── */} - + 공급업체 상세정보 {editSuppData ? "수정" : "입력"} — {selectedItem?.item_name || ""} @@ -1729,7 +1733,7 @@ export default function PurchaseItemPage() { -
+
{selectedSuppsForDetail.map((cust, idx) => { const custKey = cust.supplier_code || cust.id; const mappingRows = suppMappings[custKey] || []; diff --git a/frontend/app/(main)/COMPANY_16/sales/sales-item/page.tsx b/frontend/app/(main)/COMPANY_16/sales/sales-item/page.tsx index de794d28..7b9c09c6 100644 --- a/frontend/app/(main)/COMPANY_16/sales/sales-item/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/sales-item/page.tsx @@ -264,6 +264,7 @@ export default function SalesItemPage() { calculated_price: string; }>>>({}); const [editCustData, setEditCustData] = useState(null); + const [collapsedPriceCards, setCollapsedPriceCards] = useState>(new Set()); // 카테고리 로드 @@ -368,6 +369,7 @@ export default function SalesItemPage() { page: 1, size: 500, dataFilter: { enabled: true, filters: [{ columnName: "item_id", operator: "equals", value: itemKey }] }, autoFilter: true, + sort: { columnName: "created_date", order: "asc" }, }); const mappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || []; @@ -618,6 +620,7 @@ export default function SalesItemPage() { { columnName: "customer_id", operator: "equals", value: custKey }, { columnName: "item_id", operator: "equals", value: selectedItem!.item_number }, ]}, autoFilter: true, + sort: { columnName: "created_date", order: "asc" }, }); const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || []; mappingRows = allMappings @@ -639,7 +642,8 @@ export default function SalesItemPage() { { columnName: "item_id", operator: "equals", value: selectedItem!.item_number }, ]}, autoFilter: true, }); - const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || []; + const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || []) + .sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || "")); priceRows = allPriceData.map((p: any) => ({ _id: `p_existing_${p.id}`, start_date: p.start_date ? String(p.start_date).split("T")[0] : "", @@ -680,104 +684,104 @@ export default function SalesItemPage() { const mappingRows = custMappings[custKey] || []; if (isEditingExisting && editCustData?.id) { - // 매핑: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE - const keptIds = new Set(); - let firstMappingId: string | null = null; + // 기존 매핑 조회 + let existingMaps: any[] = []; + try { + const existingMappings = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, { + page: 1, size: 100, + dataFilter: { enabled: true, filters: [ + { columnName: "customer_id", operator: "equals", value: custKey }, + { columnName: "item_id", operator: "equals", value: selectedItem.item_number }, + ]}, autoFilter: true, + sort: { columnName: "created_date", order: "asc" }, + }); + existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || []; + } catch { /* skip */ } + + // 매핑 upsert: 인덱스 기반 + const usedExistingIds = new Set(); + let firstMappingId: string | null = editCustData.id; for (let mi = 0; mi < mappingRows.length; mi++) { - const m = mappingRows[mi]; - if (m._id?.startsWith("m_existing_")) { - const realId = m._id.replace("m_existing_", ""); - keptIds.add(realId); - if (mi === 0) firstMappingId = realId; + const existMap = existingMaps[mi]; + if (existMap) { await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, { - originalData: { id: realId }, + originalData: { id: existMap.id }, updatedData: { - customer_item_code: m.customer_item_code || "", - customer_item_name: m.customer_item_name || "", + customer_item_code: mappingRows[mi].customer_item_code || "", + customer_item_name: mappingRows[mi].customer_item_name || "", }, }); + usedExistingIds.add(existMap.id); + if (mi === 0) firstMappingId = existMap.id; } else { - const res = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, { + const mRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, { id: crypto.randomUUID(), customer_id: custKey, item_id: selectedItem.item_number, - customer_item_code: m.customer_item_code || "", - customer_item_name: m.customer_item_name || "", + customer_item_code: mappingRows[mi].customer_item_code || "", + customer_item_name: mappingRows[mi].customer_item_name || "", }); - if (mi === 0) firstMappingId = res.data?.data?.id || null; + if (mi === 0 && !firstMappingId) firstMappingId = mRes.data?.data?.id || null; } } - // 폼에서 삭제된 매핑 → DB 삭제 + // 초과분 delete + const toDeleteMaps = existingMaps.filter((m) => !usedExistingIds.has(m.id)); + if (toDeleteMaps.length > 0) { + await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, { + data: toDeleteMaps.map((m: any) => ({ id: m.id })), + }); + } + + // 기존 단가 조회 + let existingPriceRows: any[] = []; try { - const dbMappings = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, { + const existingPrices = await apiClient.post(`/table-management/tables/customer_item_prices/data`, { page: 1, size: 100, dataFilter: { enabled: true, filters: [ { columnName: "customer_id", operator: "equals", value: custKey }, { columnName: "item_id", operator: "equals", value: selectedItem.item_number }, ]}, autoFilter: true, }); - const toDelete = (dbMappings.data?.data?.data || dbMappings.data?.data?.rows || []) - .filter((m: any) => !keptIds.has(m.id)); - if (toDelete.length > 0) { - await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, { - data: toDelete.map((m: any) => ({ id: m.id })), - }); - } + existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || []; } catch { /* skip */ } - if (!firstMappingId) firstMappingId = editCustData.id; - - // 단가: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE - const keptPriceIds = new Set(); + // 단가 upsert: 인덱스 기반 const priceRows = (custPrices[custKey] || []).filter((p) => p.base_price || p.start_date || p.currency_code || p.base_price_type ); - for (const price of priceRows) { - if (price._id?.startsWith("p_existing_")) { - const realId = price._id.replace("p_existing_", ""); - keptPriceIds.add(realId); + const usedPriceIds = new Set(); + for (let pi = 0; pi < priceRows.length; pi++) { + const price = priceRows[pi]; + const priceData = { + mapping_id: firstMappingId || editCustData.id, + customer_id: custKey, item_id: selectedItem.item_number, + start_date: price.start_date || null, end_date: price.end_date || null, + currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, + base_price: price.base_price ? Number(price.base_price) : null, + unit_price: price.calculated_price ? Number(price.calculated_price) : (price.base_price ? Number(price.base_price) : null), + discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, + rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, + calculated_price: price.calculated_price ? Number(price.calculated_price) : null, + }; + const existPrice = existingPriceRows[pi]; + if (existPrice) { await apiClient.put(`/table-management/tables/customer_item_prices/edit`, { - originalData: { id: realId }, - updatedData: { - mapping_id: firstMappingId || "", - start_date: price.start_date || null, end_date: price.end_date || null, - currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, - base_price: price.base_price ? Number(price.base_price) : null, - discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, - rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, - calculated_price: price.calculated_price ? Number(price.calculated_price) : null, - }, + originalData: { id: existPrice.id }, + updatedData: priceData, }); + usedPriceIds.add(existPrice.id); } else { await apiClient.post(`/table-management/tables/customer_item_prices/add`, { - id: crypto.randomUUID(), - mapping_id: firstMappingId || "", - customer_id: custKey, item_id: selectedItem.item_number, - start_date: price.start_date || null, end_date: price.end_date || null, - currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, - base_price: price.base_price ? Number(price.base_price) : null, - discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, - rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, - calculated_price: price.calculated_price ? Number(price.calculated_price) : null, + id: crypto.randomUUID(), ...priceData, }); } } - // 폼에서 삭제된 단가 → DB 삭제 - try { - const dbPrices = await apiClient.post(`/table-management/tables/customer_item_prices/data`, { - page: 1, size: 100, - dataFilter: { enabled: true, filters: [ - { columnName: "customer_id", operator: "equals", value: custKey }, - { columnName: "item_id", operator: "equals", value: selectedItem.item_number }, - ]}, autoFilter: true, + // 초과분 delete + const toDeletePrices = existingPriceRows.filter((p) => !usedPriceIds.has(p.id)); + if (toDeletePrices.length > 0) { + await apiClient.delete(`/table-management/tables/customer_item_prices/delete`, { + data: toDeletePrices.map((p: any) => ({ id: p.id })), }); - const toDeletePrices = (dbPrices.data?.data?.data || dbPrices.data?.data?.rows || []) - .filter((p: any) => !keptPriceIds.has(p.id)); - if (toDeletePrices.length > 0) { - await apiClient.delete(`/table-management/tables/customer_item_prices/delete`, { - data: toDeletePrices.map((p: any) => ({ id: p.id })), - }); - } - } catch { /* skip */ } + } } else { // 신규 등록 const mappingRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, { @@ -807,6 +811,7 @@ export default function SalesItemPage() { start_date: price.start_date || null, end_date: price.end_date || null, currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, base_price: price.base_price ? Number(price.base_price) : null, + unit_price: price.calculated_price ? Number(price.calculated_price) : (price.base_price ? Number(price.base_price) : null), discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, calculated_price: price.calculated_price ? Number(price.calculated_price) : null, @@ -823,7 +828,10 @@ export default function SalesItemPage() { setSelectedItemId(null); setTimeout(() => setSelectedItemId(sid), 50); } catch (err: any) { - toast.error(err.response?.data?.message || "저장에 실패했습니다."); + console.error("거래처 상세 저장 실패:", err.response?.data); + const detail = err.response?.data?.error?.details; + const msg = err.response?.data?.message || "저장에 실패했습니다."; + toast.error(detail ? `${msg} (${typeof detail === "string" ? detail : JSON.stringify(detail)})` : msg); } finally { setSaving(false); } @@ -1716,7 +1724,7 @@ export default function SalesItemPage() { {/* ── 거래처 상세 입력/수정 모달 ── */} - + 거래처 상세정보 {editCustData ? "수정" : "입력"} — {selectedItem?.item_name || ""} @@ -1726,7 +1734,7 @@ export default function SalesItemPage() { -
+
{selectedCustsForDetail.map((cust, idx) => { const custKey = cust.customer_code || cust.id; const mappingRows = custMappings[custKey] || []; @@ -1742,17 +1750,17 @@ export default function SalesItemPage() {
-
+
{/* 좌: 거래처 품번/품명 */} -
-
+
+
거래처 품번/품명 관리
-
+
{mappingRows.length === 0 ? (
입력된 거래처 품번이 없어요
) : ( @@ -1792,35 +1800,61 @@ export default function SalesItemPage() {
{/* 우: 기간별 단가 */} -
-
+
+
기간별 단가 설정
-
+
{prices.map((price, pIdx) => ( -
-
- 단가 {pIdx + 1} +
+
setCollapsedPriceCards((prev) => { + const next = new Set(prev); + if (next.has(price._id)) next.delete(price._id); else next.add(price._id); + return next; + })} + > +
+ {collapsedPriceCards.has(price._id) + ? + : + } + 단가 {pIdx + 1} + {collapsedPriceCards.has(price._id) && price.calculated_price && ( + + {price.start_date || "—"} ~ {price.end_date || "—"} · {Number(price.calculated_price).toLocaleString()} {priceCategoryOptions["currency_code"]?.find((o) => o.code === price.currency_code)?.label || ""} + + )} +
{prices.length > 1 && ( )}
+ {!collapsedPriceCards.has(price._id) &&
{/* 기간 + 통화 */}
updatePriceRow(custKey, price._id, "start_date", e.target.value)} + onChange={(e) => { + const v = e.target.value; + updatePriceRow(custKey, price._id, "start_date", v); + if (price.end_date && v > price.end_date) { + updatePriceRow(custKey, price._id, "end_date", v); + } + }} + max={price.end_date || undefined} className="h-8 text-xs flex-1" /> ~ @@ -1828,6 +1862,7 @@ export default function SalesItemPage() { type="date" value={price.end_date} onChange={(e) => updatePriceRow(custKey, price._id, "end_date", e.target.value)} + min={price.start_date || undefined} className="h-8 text-xs flex-1" />
@@ -1922,6 +1957,7 @@ export default function SalesItemPage() { {price.calculated_price ? Number(price.calculated_price).toLocaleString() : "-"}
+
}
))}
diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index 46921770..f9fdc1e1 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -999,7 +999,11 @@ function AppLayoutInner({ children }: AppLayoutProps) {
{/* 테마 토글 */} - {(!isMobile && sidebarCollapsed) ? null : ( + {(!isMobile && sidebarCollapsed) ? ( +
+ +
+ ) : (
diff --git a/frontend/components/layout/ThemeToggle.tsx b/frontend/components/layout/ThemeToggle.tsx index b6ea71be..fa6ff2e0 100644 --- a/frontend/components/layout/ThemeToggle.tsx +++ b/frontend/components/layout/ThemeToggle.tsx @@ -32,10 +32,10 @@ export function ThemeToggle({ collapsed = false }: ThemeToggleProps) { variant="ghost" size={collapsed ? "icon" : "default"} onClick={() => setTheme(isDark ? "light" : "dark")} - className="w-full justify-start gap-2 text-sm" + className={collapsed ? "h-10 w-10 justify-center" : "w-full justify-start gap-2 text-sm"} title={isDark ? "라이트 모드" : "다크 모드"} > - {isDark ? : } + {isDark ? : } {!collapsed && (isDark ? "라이트 모드" : "다크 모드")} ); From 5842a91c7f92a1927a1ad75cf85e1c48c0f9cdbd Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 10 Apr 2026 13:58:59 +0900 Subject: [PATCH 4/9] refactor: Update table layout styles in Production Plan Management page - Changed the table layout from fixed to a minimum width of 900px to enhance responsiveness. - Adjusted TableHead components to ensure consistent width and minimum width settings for better alignment and usability. - These modifications aim to improve the overall user experience by providing a more flexible and visually appealing table layout across multiple companies. --- .../COMPANY_10/production/plan-management/page.tsx | 8 ++++---- .../COMPANY_16/production/plan-management/page.tsx | 8 ++++---- .../COMPANY_29/production/plan-management/page.tsx | 8 ++++---- .../COMPANY_30/production/plan-management/page.tsx | 8 ++++---- .../COMPANY_7/production/plan-management/page.tsx | 8 ++++---- .../COMPANY_8/production/plan-management/page.tsx | 8 ++++---- .../COMPANY_9/production/plan-management/page.tsx | 8 ++++---- .../report/designer/renderers/CardRenderer.tsx | 5 ++--- .../report/designer/renderers/TableRenderer.tsx | 11 +++++++---- 9 files changed, 37 insertions(+), 35 deletions(-) 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 50be10ee..38b01658 100644 --- a/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx @@ -1052,17 +1052,17 @@ export default function ProductionPlanManagementPage() { }; return ( - +
- + 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" /> - + 품목코드 품목명 {ts.visibleColumns.map((col) => ( - + {col.label} ))} diff --git a/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx index 50be10ee..38b01658 100644 --- a/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx @@ -1052,17 +1052,17 @@ export default function ProductionPlanManagementPage() { }; return ( -
+
- + 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" /> - + 품목코드 품목명 {ts.visibleColumns.map((col) => ( - + {col.label} ))} 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 50be10ee..38b01658 100644 --- a/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx @@ -1052,17 +1052,17 @@ export default function ProductionPlanManagementPage() { }; return ( -
+
- + 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" /> - + 품목코드 품목명 {ts.visibleColumns.map((col) => ( - + {col.label} ))} 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 50be10ee..38b01658 100644 --- a/frontend/app/(main)/COMPANY_30/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_30/production/plan-management/page.tsx @@ -1052,17 +1052,17 @@ export default function ProductionPlanManagementPage() { }; return ( -
+
- + 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" /> - + 품목코드 품목명 {ts.visibleColumns.map((col) => ( - + {col.label} ))} 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 50be10ee..38b01658 100644 --- a/frontend/app/(main)/COMPANY_7/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_7/production/plan-management/page.tsx @@ -1052,17 +1052,17 @@ export default function ProductionPlanManagementPage() { }; return ( -
+
- + 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" /> - + 품목코드 품목명 {ts.visibleColumns.map((col) => ( - + {col.label} ))} 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 50be10ee..38b01658 100644 --- a/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx @@ -1052,17 +1052,17 @@ export default function ProductionPlanManagementPage() { }; return ( -
+
- + 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" /> - + 품목코드 품목명 {ts.visibleColumns.map((col) => ( - + {col.label} ))} 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 50be10ee..38b01658 100644 --- a/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx @@ -1052,17 +1052,17 @@ export default function ProductionPlanManagementPage() { }; return ( -
+
- + 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" /> - + 품목코드 품목명 {ts.visibleColumns.map((col) => ( - + {col.label} ))} diff --git a/frontend/components/report/designer/renderers/CardRenderer.tsx b/frontend/components/report/designer/renderers/CardRenderer.tsx index 65b14b3a..23310059 100644 --- a/frontend/components/report/designer/renderers/CardRenderer.tsx +++ b/frontend/components/report/designer/renderers/CardRenderer.tsx @@ -43,9 +43,8 @@ function CardListRenderer({ component, getQueryResult }: CardRendererProps) { const queryResult = getQueryResult(component.queryId); if (queryResult && queryResult.rows && queryResult.rows.length > 0) { const row = queryResult.rows[0]; - return row[item.fieldName] !== undefined - ? String(row[item.fieldName]) - : item.value; + const val = row[item.fieldName]; + return val !== undefined && val !== null ? String(val) : ""; } } return item.value; diff --git a/frontend/components/report/designer/renderers/TableRenderer.tsx b/frontend/components/report/designer/renderers/TableRenderer.tsx index cf211c0b..7b23ae1e 100644 --- a/frontend/components/report/designer/renderers/TableRenderer.tsx +++ b/frontend/components/report/designer/renderers/TableRenderer.tsx @@ -54,9 +54,12 @@ function getGridCellValue( ): string { if (cell.cellType === "static") return cell.value ?? ""; - if (cell.cellType === "field" && cell.field && row) { - const raw = String(row[cell.field] ?? ""); - return applyNumberFormat(raw, cell.numberFormat, cell.currencySuffix); + if (cell.cellType === "field") { + if (cell.field && row) { + const raw = String(row[cell.field] ?? ""); + return applyNumberFormat(raw, cell.numberFormat, cell.currencySuffix); + } + return ""; // 데이터 없으면 플레이스홀더 숨김 } return cell.value ?? ""; @@ -277,7 +280,7 @@ function ClassicTableRenderer({ component, getQueryResult }: TableRendererProps) color: "#d1d5db", }} > - {col.field ? `{${col.field}}` : "—"} + {""} ))} From b8860e56e5b8df0f700a96ec23474eab53e24f7e Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 10 Apr 2026 14:17:35 +0900 Subject: [PATCH 5/9] refactor: Update category value handling to make menuObjid optional - Modified the addCategoryValue function to allow menuObjid to be optional, accommodating scenarios where it may not be provided, such as in global management screens. - Adjusted related service and controller logic to handle the absence of menuObjid gracefully, ensuring that the application remains robust and user-friendly. - Enhanced the frontend components to reflect these changes, improving the overall user experience when adding category values across multiple companies. --- .../tableCategoryValueController.ts | 9 +--- .../src/services/tableCategoryValueService.ts | 32 ++++++++------ .../table-category/CategoryColumnList.tsx | 44 ++++++++++--------- .../table-category/CategoryValueManager.tsx | 11 +---- frontend/lib/api/tableCategoryValue.ts | 4 +- 5 files changed, 47 insertions(+), 53 deletions(-) diff --git a/backend-node/src/controllers/tableCategoryValueController.ts b/backend-node/src/controllers/tableCategoryValueController.ts index 49fe6e72..cff7ccfa 100644 --- a/backend-node/src/controllers/tableCategoryValueController.ts +++ b/backend-node/src/controllers/tableCategoryValueController.ts @@ -116,12 +116,7 @@ export const addCategoryValue = async (req: AuthenticatedRequest, res: Response) const userId = req.user!.userId; const { menuObjid, ...value } = req.body; - if (!menuObjid) { - return res.status(400).json({ - success: false, - message: "menuObjid는 필수입니다", - }); - } + // menuObjid는 선택사항 — 옵션설정 등 전역 관리 화면에서는 없을 수 있음 logger.info("카테고리 값 추가 요청", { tableName: value.tableName, @@ -134,7 +129,7 @@ export const addCategoryValue = async (req: AuthenticatedRequest, res: Response) value, companyCode, userId, - Number(menuObjid) // ← menuObjid 전달 + menuObjid ? Number(menuObjid) : null ); return res.status(201).json({ diff --git a/backend-node/src/services/tableCategoryValueService.ts b/backend-node/src/services/tableCategoryValueService.ts index 6fc07d39..16bc75a2 100644 --- a/backend-node/src/services/tableCategoryValueService.ts +++ b/backend-node/src/services/tableCategoryValueService.ts @@ -269,7 +269,7 @@ class TableCategoryValueService { value: TableCategoryValue, companyCode: string, userId: string, - menuObjid: number + menuObjid: number | null ): Promise { const pool = getPool(); @@ -286,29 +286,35 @@ class TableCategoryValueService { let duplicateQuery: string; let duplicateParams: any[]; + const menuCondition = menuObjid + ? "AND menu_objid = $4" + : "AND menu_objid IS NULL"; + const baseParams = menuObjid + ? [value.tableName, value.columnName, value.valueCode, menuObjid] + : [value.tableName, value.columnName, value.valueCode]; + if (companyCode === "*") { - // 최고 관리자: 모든 회사에서 중복 체크 duplicateQuery = ` - SELECT value_id + SELECT value_id FROM category_values WHERE table_name = $1 AND column_name = $2 AND value_code = $3 - AND menu_objid = $4 + ${menuCondition} `; - duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid]; + duplicateParams = baseParams; } else { - // 일반 회사: 자신의 회사에서만 중복 체크 + const companyIdx = menuObjid ? "$5" : "$4"; duplicateQuery = ` - SELECT value_id + SELECT value_id FROM category_values WHERE table_name = $1 AND column_name = $2 AND value_code = $3 - AND menu_objid = $4 - AND company_code = $5 + ${menuCondition} + AND company_code = ${companyIdx} `; - duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid, companyCode]; + duplicateParams = [...baseParams, companyCode]; } const duplicateResult = await pool.query(duplicateQuery, duplicateParams); @@ -352,11 +358,11 @@ class TableCategoryValueService { const insertQuery = ` INSERT INTO category_values ( - table_name, column_name, value_code, value_label, value_order, + value_id, table_name, column_name, value_code, value_label, value_order, parent_value_id, depth, description, color, icon, is_active, is_default, company_code, menu_objid, created_by - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) - RETURNING + ) VALUES ((SELECT COALESCE(MAX(value_id), 0) + 1 FROM category_values), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) + RETURNING value_id AS "valueId", table_name AS "tableName", column_name AS "columnName", diff --git a/frontend/components/table-category/CategoryColumnList.tsx b/frontend/components/table-category/CategoryColumnList.tsx index 1b1bf0a5..fada949f 100644 --- a/frontend/components/table-category/CategoryColumnList.tsx +++ b/frontend/components/table-category/CategoryColumnList.tsx @@ -372,33 +372,35 @@ export function CategoryColumnList({ } return ( -
-
+
+

카테고리 컬럼

관리할 카테고리 컬럼을 선택하세요

-
- - setSearchQuery(e.target.value)} - className="h-8 pl-8 pr-8 text-xs" - /> - {searchQuery && ( - - )} +
+
+ + setSearchQuery(e.target.value)} + className="h-8 pl-8 pr-8 text-xs" + /> + {searchQuery && ( + + )} +
-
+
{filteredColumns.length === 0 && searchQuery ? (
'{searchQuery}'에 대한 검색 결과가 없습니다 diff --git a/frontend/components/table-category/CategoryValueManager.tsx b/frontend/components/table-category/CategoryValueManager.tsx index 75ddbce8..cdfdd3d5 100644 --- a/frontend/components/table-category/CategoryValueManager.tsx +++ b/frontend/components/table-category/CategoryValueManager.tsx @@ -106,22 +106,13 @@ export const CategoryValueManager: React.FC = ({ const handleAddValue = async (newValue: TableCategoryValue) => { try { - if (!menuObjid) { - toast({ - title: "오류", - description: "메뉴 정보가 없습니다. 카테고리 값을 추가할 수 없습니다.", - variant: "destructive", - }); - return; - } - const response = await addCategoryValue( { ...newValue, tableName, columnName, }, - menuObjid + menuObjid || 0 ); if (response.success && response.data) { diff --git a/frontend/lib/api/tableCategoryValue.ts b/frontend/lib/api/tableCategoryValue.ts index 253e66d0..b2da31ff 100644 --- a/frontend/lib/api/tableCategoryValue.ts +++ b/frontend/lib/api/tableCategoryValue.ts @@ -80,7 +80,7 @@ export async function getCategoryValues( */ export async function addCategoryValue( value: TableCategoryValue, - menuObjid: number + menuObjid?: number ) { try { const response = await apiClient.post<{ @@ -88,7 +88,7 @@ export async function addCategoryValue( data: TableCategoryValue; }>("/table-categories/values", { ...value, - menuObjid, // ← menuObjid 포함 + menuObjid: menuObjid || undefined, }); return response.data; } catch (error: any) { From 2f50d7d80922eafc23d712500c6bd4026ed8b970 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 10 Apr 2026 15:59:38 +0900 Subject: [PATCH 6/9] fix: Enhance file handling and inspection method mapping - Updated fileController to include Cross-Origin-Resource-Policy headers for improved security and file handling. - Added error handling for file streams to ensure robust responses in case of read errors. - Modified materialStatusController to correctly map material IDs to their respective codes for inventory stock queries. - Enhanced moldController to include warranty shot count in mold creation and update processes. - Improved item inspection page by adding inspection method category loading and mapping, ensuring accurate display of method labels in the UI. These changes aim to enhance the overall functionality and user experience across multiple companies by ensuring proper file handling, data mapping, and error management. --- .../src/controllers/fileController.ts | 38 ++++++++++++- .../controllers/materialStatusController.ts | 7 +-- .../src/controllers/moldController.ts | 16 +++--- .../(main)/COMPANY_10/logistics/info/page.tsx | 9 ++-- .../COMPANY_10/logistics/receiving/page.tsx | 6 ++- .../quality/item-inspection/page.tsx | 53 +++++++++++++++---- .../(main)/COMPANY_16/logistics/info/page.tsx | 9 ++-- .../COMPANY_16/logistics/receiving/page.tsx | 6 ++- .../quality/item-inspection/page.tsx | 53 +++++++++++++++---- .../(main)/COMPANY_29/logistics/info/page.tsx | 9 ++-- .../COMPANY_29/logistics/receiving/page.tsx | 6 ++- .../quality/item-inspection/page.tsx | 53 +++++++++++++++---- .../(main)/COMPANY_30/logistics/info/page.tsx | 9 ++-- .../COMPANY_30/logistics/receiving/page.tsx | 6 ++- .../quality/item-inspection/page.tsx | 53 +++++++++++++++---- .../(main)/COMPANY_7/logistics/info/page.tsx | 9 ++-- .../COMPANY_7/logistics/receiving/page.tsx | 6 ++- .../quality/item-inspection/page.tsx | 53 +++++++++++++++---- .../(main)/COMPANY_8/logistics/info/page.tsx | 9 ++-- .../COMPANY_8/logistics/receiving/page.tsx | 6 ++- .../quality/item-inspection/page.tsx | 53 +++++++++++++++---- .../(main)/COMPANY_9/logistics/info/page.tsx | 9 ++-- .../COMPANY_9/logistics/receiving/page.tsx | 6 ++- .../quality/item-inspection/page.tsx | 53 +++++++++++++++---- 24 files changed, 414 insertions(+), 123 deletions(-) diff --git a/backend-node/src/controllers/fileController.ts b/backend-node/src/controllers/fileController.ts index ea9bd7a9..03048d6d 100644 --- a/backend-node/src/controllers/fileController.ts +++ b/backend-node/src/controllers/fileController.ts @@ -924,12 +924,26 @@ export const previewFile = async ( ); res.setHeader("Access-Control-Allow-Credentials", "true"); - // 캐시 헤더 설정 + // Cross-Origin-Resource-Policy: cross-origin 설정 + // helmet 기본값(same-origin)을 오버라이드하여 v1.vexplor.com에서 api.vexplor.com 이미지 로드 허용 + res.setHeader("Cross-Origin-Resource-Policy", "cross-origin"); + + // 파일 크기 및 캐시 헤더 설정 + const stat = fs.statSync(finalPath); + res.setHeader("Content-Length", stat.size); res.setHeader("Cache-Control", "public, max-age=3600"); res.setHeader("Content-Type", mimeType); // 파일 스트림으로 전송 const fileStream = fs.createReadStream(finalPath); + fileStream.on("error", (err) => { + console.error("파일 스트림 오류:", err); + if (!res.headersSent) { + res.status(500).json({ success: false, message: "파일 읽기 오류" }); + } else { + res.end(); + } + }); fileStream.pipe(res); } catch (error) { console.error("파일 미리보기 오류:", error); @@ -1031,9 +1045,20 @@ export const downloadFile = async ( `attachment; filename="${encodeURIComponent(fileRecord.real_file_name!)}"` ); res.setHeader("Content-Type", "application/octet-stream"); + res.setHeader("Cross-Origin-Resource-Policy", "cross-origin"); + const stat = fs.statSync(filePath); + res.setHeader("Content-Length", stat.size); // 파일 스트림 전송 const fileStream = fs.createReadStream(filePath); + fileStream.on("error", (err) => { + console.error("파일 스트림 오류:", err); + if (!res.headersSent) { + res.status(500).json({ success: false, message: "파일 읽기 오류" }); + } else { + res.end(); + } + }); fileStream.pipe(res); } catch (error) { console.error("파일 다운로드 오류:", error); @@ -1218,10 +1243,21 @@ export const getFileByToken = async (req: Request, res: Response) => { "Content-Disposition", `inline; filename="${encodeURIComponent(fileRecord.real_file_name!)}"` ); + res.setHeader("Cross-Origin-Resource-Policy", "cross-origin"); + const stat = fs.statSync(filePath); + res.setHeader("Content-Length", stat.size); res.setHeader("Cache-Control", "public, max-age=300"); // 5분 캐시 // 파일 스트림 전송 const fileStream = fs.createReadStream(filePath); + fileStream.on("error", (err) => { + console.error("파일 스트림 오류:", err); + if (!res.headersSent) { + res.status(500).json({ success: false, message: "파일 읽기 오류" }); + } else { + res.end(); + } + }); fileStream.pipe(res); } catch (error) { console.error("❌ 토큰 파일 접근 오류:", error); diff --git a/backend-node/src/controllers/materialStatusController.ts b/backend-node/src/controllers/materialStatusController.ts index 1c76246a..b5427659 100644 --- a/backend-node/src/controllers/materialStatusController.ts +++ b/backend-node/src/controllers/materialStatusController.ts @@ -226,11 +226,12 @@ export async function getMaterialStatus( return res.json({ success: true, data: [] }); } - // 4) 재고 조회 (창고/위치별) - const stockPlaceholders = materialIds + // 4) 재고 조회 (창고/위치별) — inventory_stock.item_code는 item_number 기준 + const materialCodes = materialIds.map((id) => materialMap[id].materialCode); + const stockPlaceholders = materialCodes .map((_, i) => `$${i + 1}`) .join(","); - const stockParams: any[] = [...materialIds]; + const stockParams: any[] = [...materialCodes]; let stockParamIdx = materialIds.length + 1; const stockConditions: string[] = [ diff --git a/backend-node/src/controllers/moldController.ts b/backend-node/src/controllers/moldController.ts index cf01f362..25e49186 100644 --- a/backend-node/src/controllers/moldController.ts +++ b/backend-node/src/controllers/moldController.ts @@ -94,7 +94,7 @@ export async function createMold(req: AuthenticatedRequest, res: Response): Prom mold_code, mold_name, mold_type, category, manufacturer, manufacturing_number, manufacturing_date, cavity_count, shot_count, mold_quantity, base_input_qty, operation_status, - remarks, image_path, memo, + remarks, image_path, memo, warranty_shot_count, } = req.body; if (!mold_code || !mold_name) { @@ -107,15 +107,16 @@ export async function createMold(req: AuthenticatedRequest, res: Response): Prom id, company_code, mold_code, mold_name, mold_type, category, manufacturer, manufacturing_number, manufacturing_date, cavity_count, shot_count, mold_quantity, base_input_qty, - operation_status, remarks, image_path, memo, writer, created_date - ) VALUES (gen_random_uuid()::text,$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,NOW()) + operation_status, remarks, image_path, memo, warranty_shot_count, writer, created_date + ) VALUES (gen_random_uuid()::text,$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,NOW()) RETURNING * `; const params = [ companyCode, mold_code, mold_name, mold_type || null, category || null, manufacturer || null, manufacturing_number || null, manufacturing_date || null, cavity_count || 0, shot_count || 0, mold_quantity || 1, base_input_qty || 0, - operation_status || "ACTIVE", remarks || null, image_path || null, memo || null, userId, + operation_status || "ACTIVE", remarks || null, image_path || null, memo || null, + warranty_shot_count || 0, userId, ]; const result = await query(sql, params); @@ -139,7 +140,7 @@ export async function updateMold(req: AuthenticatedRequest, res: Response): Prom mold_name, mold_type, category, manufacturer, manufacturing_number, manufacturing_date, cavity_count, shot_count, mold_quantity, base_input_qty, operation_status, - remarks, image_path, memo, + remarks, image_path, memo, warranty_shot_count, } = req.body; const sql = ` @@ -153,8 +154,9 @@ export async function updateMold(req: AuthenticatedRequest, res: Response): Prom base_input_qty = COALESCE($10, base_input_qty), operation_status = COALESCE($11, operation_status), remarks = $12, image_path = $13, memo = $14, + warranty_shot_count = $15, updated_date = NOW() - WHERE mold_code = $15 AND company_code = $16 + WHERE mold_code = $16 AND company_code = $17 RETURNING * `; const params = [ @@ -162,7 +164,7 @@ export async function updateMold(req: AuthenticatedRequest, res: Response): Prom manufacturing_number, manufacturing_date, cavity_count, shot_count, mold_quantity, base_input_qty, operation_status, remarks, image_path, memo, - moldCode, companyCode, + warranty_shot_count || 0, moldCode, companyCode, ]; const result = await query(sql, params); diff --git a/frontend/app/(main)/COMPANY_10/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_10/logistics/info/page.tsx index 543dd3dd..35764744 100644 --- a/frontend/app/(main)/COMPANY_10/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_10/logistics/info/page.tsx @@ -230,11 +230,10 @@ function flattenCategories(items: any[]): { value: string; label: string }[] { const result: { value: string; label: string }[] = []; function walk(arr: any[]) { for (const item of arr) { - if (item.value || item.name) { - result.push({ - value: item.value || item.name, - label: item.label || item.name || item.value, - }); + const val = item.valueCode || item.value || item.name; + const lbl = item.valueLabel || item.label || item.name || val; + if (val) { + result.push({ value: val, label: lbl }); } if (item.children?.length) walk(item.children); } diff --git a/frontend/app/(main)/COMPANY_10/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_10/logistics/receiving/page.tsx index 85cdc23c..5b730674 100644 --- a/frontend/app/(main)/COMPANY_10/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_10/logistics/receiving/page.tsx @@ -777,12 +777,16 @@ export default function ReceivingPage() { return; } + if (!modalWarehouse) { + toast.error("창고를 선택해주세요."); + return; + } setSaving(true); try { const res = await createReceiving({ inbound_number: modalInboundNo, inbound_date: modalInboundDate, - warehouse_code: modalWarehouse || undefined, + warehouse_code: modalWarehouse, location_code: modalLocation || undefined, inspector: modalInspector || undefined, manager: modalManager || undefined, diff --git a/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx index 2c0e1338..1059223c 100644 --- a/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx @@ -72,6 +72,7 @@ export default function ItemInspectionInfoPage() { const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string }[]>([]); const [inspOptions, setInspOptions] = useState<{ code: string; label: string; detail: string; method: string; types: string[] }[]>([]); const [inspTypeCatOptions, setInspTypeCatOptions] = useState<{ code: string; label: string }[]>([]); + const [inspMethodCatOptions, setInspMethodCatOptions] = useState<{ code: string; label: string }[]>([]); const [userOptions, setUserOptions] = useState<{ code: string; label: string }[]>([]); /* 검사유형별 검사항목 rows */ @@ -118,6 +119,15 @@ export default function ItemInspectionInfoPage() { setInspTypeCatOptions(flatCats); } catch { /* skip */ } + // 검사방법 카테고리 값 로드 (코드→라벨 매핑용) + try { + const methodRes = await apiClient.get(`/table-categories/${INSPECTION_TABLE}/inspection_method/values`); + const flatMethods: { code: string; label: string }[] = []; + const flattenM = (arr: any[]) => { for (const v of arr) { flatMethods.push({ code: v.valueCode, label: v.valueLabel }); if (v.children?.length) flattenM(v.children); } }; + if (methodRes.data?.data?.length) flattenM(methodRes.data.data); + setInspMethodCatOptions(flatMethods); + } catch { /* skip */ } + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; setUserOptions(users.map((u: any) => ({ code: u.user_id || u.id, @@ -221,11 +231,13 @@ export default function ItemInspectionInfoPage() { if (!typeKey) continue; typeFlags[typeKey] = true; if (!rowMap[typeKey]) rowMap[typeKey] = []; + const mCode = r.inspection_method || ""; + const mLabel = inspMethodCatOptions.find(o => o.code === mCode)?.label || mCode; rowMap[typeKey].push({ id: r.id, inspection_standard_id: r.inspection_standard_id || "", inspection_detail: r.inspection_item_name || r.inspection_standard || "", - inspection_method: r.inspection_method || "", + inspection_method: mLabel, apply_process: "", acceptance_criteria: r.pass_criteria || "", is_required: r.is_required === "true" || r.is_required === true, @@ -270,7 +282,9 @@ export default function ItemInspectionInfoPage() { if (r.id !== rowId) return r; if (field === "inspection_standard_id") { const opt = inspOptions.find(o => o.code === value); - return { ...r, inspection_standard_id: value, inspection_detail: opt?.detail || "", inspection_method: opt?.method || "" }; + const methodCode = opt?.method || ""; + const methodLabel = inspMethodCatOptions.find(o => o.code === methodCode)?.label || methodCode; + return { ...r, inspection_standard_id: value, inspection_detail: opt?.detail || "", inspection_method: methodLabel }; } return { ...r, [field]: value }; }), @@ -471,17 +485,36 @@ export default function ItemInspectionInfoPage() { {ts.visibleColumns.map((col) => renderCell(col.key))} - {isExpanded && group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( - + {isExpanded && ( + - {row.inspection_type} - {resolveInspLabel(row.inspection_standard_id)} - {row.inspection_item_name || "-"} - {row.inspection_method || "-"} - {row.pass_criteria || "-"} + +
+ + + 검사유형 + 검사기준 + 검사항목 + 검사방법 + 합격기준 + + + + {group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( + + {row.inspection_type} + {resolveInspLabel(row.inspection_standard_id)} + {row.inspection_item_name || "-"} + {row.inspection_method || "-"} + {row.pass_criteria || "-"} + + ))} + +
+ - ))} + )} ); })} diff --git a/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx index 543dd3dd..35764744 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx @@ -230,11 +230,10 @@ function flattenCategories(items: any[]): { value: string; label: string }[] { const result: { value: string; label: string }[] = []; function walk(arr: any[]) { for (const item of arr) { - if (item.value || item.name) { - result.push({ - value: item.value || item.name, - label: item.label || item.name || item.value, - }); + const val = item.valueCode || item.value || item.name; + const lbl = item.valueLabel || item.label || item.name || val; + if (val) { + result.push({ value: val, label: lbl }); } if (item.children?.length) walk(item.children); } diff --git a/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx index 85cdc23c..5b730674 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx @@ -777,12 +777,16 @@ export default function ReceivingPage() { return; } + if (!modalWarehouse) { + toast.error("창고를 선택해주세요."); + return; + } setSaving(true); try { const res = await createReceiving({ inbound_number: modalInboundNo, inbound_date: modalInboundDate, - warehouse_code: modalWarehouse || undefined, + warehouse_code: modalWarehouse, location_code: modalLocation || undefined, inspector: modalInspector || undefined, manager: modalManager || undefined, diff --git a/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx index 2c0e1338..1059223c 100644 --- a/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx @@ -72,6 +72,7 @@ export default function ItemInspectionInfoPage() { const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string }[]>([]); const [inspOptions, setInspOptions] = useState<{ code: string; label: string; detail: string; method: string; types: string[] }[]>([]); const [inspTypeCatOptions, setInspTypeCatOptions] = useState<{ code: string; label: string }[]>([]); + const [inspMethodCatOptions, setInspMethodCatOptions] = useState<{ code: string; label: string }[]>([]); const [userOptions, setUserOptions] = useState<{ code: string; label: string }[]>([]); /* 검사유형별 검사항목 rows */ @@ -118,6 +119,15 @@ export default function ItemInspectionInfoPage() { setInspTypeCatOptions(flatCats); } catch { /* skip */ } + // 검사방법 카테고리 값 로드 (코드→라벨 매핑용) + try { + const methodRes = await apiClient.get(`/table-categories/${INSPECTION_TABLE}/inspection_method/values`); + const flatMethods: { code: string; label: string }[] = []; + const flattenM = (arr: any[]) => { for (const v of arr) { flatMethods.push({ code: v.valueCode, label: v.valueLabel }); if (v.children?.length) flattenM(v.children); } }; + if (methodRes.data?.data?.length) flattenM(methodRes.data.data); + setInspMethodCatOptions(flatMethods); + } catch { /* skip */ } + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; setUserOptions(users.map((u: any) => ({ code: u.user_id || u.id, @@ -221,11 +231,13 @@ export default function ItemInspectionInfoPage() { if (!typeKey) continue; typeFlags[typeKey] = true; if (!rowMap[typeKey]) rowMap[typeKey] = []; + const mCode = r.inspection_method || ""; + const mLabel = inspMethodCatOptions.find(o => o.code === mCode)?.label || mCode; rowMap[typeKey].push({ id: r.id, inspection_standard_id: r.inspection_standard_id || "", inspection_detail: r.inspection_item_name || r.inspection_standard || "", - inspection_method: r.inspection_method || "", + inspection_method: mLabel, apply_process: "", acceptance_criteria: r.pass_criteria || "", is_required: r.is_required === "true" || r.is_required === true, @@ -270,7 +282,9 @@ export default function ItemInspectionInfoPage() { if (r.id !== rowId) return r; if (field === "inspection_standard_id") { const opt = inspOptions.find(o => o.code === value); - return { ...r, inspection_standard_id: value, inspection_detail: opt?.detail || "", inspection_method: opt?.method || "" }; + const methodCode = opt?.method || ""; + const methodLabel = inspMethodCatOptions.find(o => o.code === methodCode)?.label || methodCode; + return { ...r, inspection_standard_id: value, inspection_detail: opt?.detail || "", inspection_method: methodLabel }; } return { ...r, [field]: value }; }), @@ -471,17 +485,36 @@ export default function ItemInspectionInfoPage() { {ts.visibleColumns.map((col) => renderCell(col.key))} - {isExpanded && group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( - + {isExpanded && ( + - {row.inspection_type} - {resolveInspLabel(row.inspection_standard_id)} - {row.inspection_item_name || "-"} - {row.inspection_method || "-"} - {row.pass_criteria || "-"} + + + + + 검사유형 + 검사기준 + 검사항목 + 검사방법 + 합격기준 + + + + {group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( + + {row.inspection_type} + {resolveInspLabel(row.inspection_standard_id)} + {row.inspection_item_name || "-"} + {row.inspection_method || "-"} + {row.pass_criteria || "-"} + + ))} + +
+
- ))} + )} ); })} diff --git a/frontend/app/(main)/COMPANY_29/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_29/logistics/info/page.tsx index 543dd3dd..35764744 100644 --- a/frontend/app/(main)/COMPANY_29/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_29/logistics/info/page.tsx @@ -230,11 +230,10 @@ function flattenCategories(items: any[]): { value: string; label: string }[] { const result: { value: string; label: string }[] = []; function walk(arr: any[]) { for (const item of arr) { - if (item.value || item.name) { - result.push({ - value: item.value || item.name, - label: item.label || item.name || item.value, - }); + const val = item.valueCode || item.value || item.name; + const lbl = item.valueLabel || item.label || item.name || val; + if (val) { + result.push({ value: val, label: lbl }); } if (item.children?.length) walk(item.children); } diff --git a/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx index 85cdc23c..5b730674 100644 --- a/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx @@ -777,12 +777,16 @@ export default function ReceivingPage() { return; } + if (!modalWarehouse) { + toast.error("창고를 선택해주세요."); + return; + } setSaving(true); try { const res = await createReceiving({ inbound_number: modalInboundNo, inbound_date: modalInboundDate, - warehouse_code: modalWarehouse || undefined, + warehouse_code: modalWarehouse, location_code: modalLocation || undefined, inspector: modalInspector || undefined, manager: modalManager || undefined, diff --git a/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx index 2c0e1338..1059223c 100644 --- a/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx @@ -72,6 +72,7 @@ export default function ItemInspectionInfoPage() { const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string }[]>([]); const [inspOptions, setInspOptions] = useState<{ code: string; label: string; detail: string; method: string; types: string[] }[]>([]); const [inspTypeCatOptions, setInspTypeCatOptions] = useState<{ code: string; label: string }[]>([]); + const [inspMethodCatOptions, setInspMethodCatOptions] = useState<{ code: string; label: string }[]>([]); const [userOptions, setUserOptions] = useState<{ code: string; label: string }[]>([]); /* 검사유형별 검사항목 rows */ @@ -118,6 +119,15 @@ export default function ItemInspectionInfoPage() { setInspTypeCatOptions(flatCats); } catch { /* skip */ } + // 검사방법 카테고리 값 로드 (코드→라벨 매핑용) + try { + const methodRes = await apiClient.get(`/table-categories/${INSPECTION_TABLE}/inspection_method/values`); + const flatMethods: { code: string; label: string }[] = []; + const flattenM = (arr: any[]) => { for (const v of arr) { flatMethods.push({ code: v.valueCode, label: v.valueLabel }); if (v.children?.length) flattenM(v.children); } }; + if (methodRes.data?.data?.length) flattenM(methodRes.data.data); + setInspMethodCatOptions(flatMethods); + } catch { /* skip */ } + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; setUserOptions(users.map((u: any) => ({ code: u.user_id || u.id, @@ -221,11 +231,13 @@ export default function ItemInspectionInfoPage() { if (!typeKey) continue; typeFlags[typeKey] = true; if (!rowMap[typeKey]) rowMap[typeKey] = []; + const mCode = r.inspection_method || ""; + const mLabel = inspMethodCatOptions.find(o => o.code === mCode)?.label || mCode; rowMap[typeKey].push({ id: r.id, inspection_standard_id: r.inspection_standard_id || "", inspection_detail: r.inspection_item_name || r.inspection_standard || "", - inspection_method: r.inspection_method || "", + inspection_method: mLabel, apply_process: "", acceptance_criteria: r.pass_criteria || "", is_required: r.is_required === "true" || r.is_required === true, @@ -270,7 +282,9 @@ export default function ItemInspectionInfoPage() { if (r.id !== rowId) return r; if (field === "inspection_standard_id") { const opt = inspOptions.find(o => o.code === value); - return { ...r, inspection_standard_id: value, inspection_detail: opt?.detail || "", inspection_method: opt?.method || "" }; + const methodCode = opt?.method || ""; + const methodLabel = inspMethodCatOptions.find(o => o.code === methodCode)?.label || methodCode; + return { ...r, inspection_standard_id: value, inspection_detail: opt?.detail || "", inspection_method: methodLabel }; } return { ...r, [field]: value }; }), @@ -471,17 +485,36 @@ export default function ItemInspectionInfoPage() { {ts.visibleColumns.map((col) => renderCell(col.key))}
- {isExpanded && group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( - + {isExpanded && ( + - {row.inspection_type} - {resolveInspLabel(row.inspection_standard_id)} - {row.inspection_item_name || "-"} - {row.inspection_method || "-"} - {row.pass_criteria || "-"} + + + + + 검사유형 + 검사기준 + 검사항목 + 검사방법 + 합격기준 + + + + {group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( + + {row.inspection_type} + {resolveInspLabel(row.inspection_standard_id)} + {row.inspection_item_name || "-"} + {row.inspection_method || "-"} + {row.pass_criteria || "-"} + + ))} + +
+
- ))} + )} ); })} diff --git a/frontend/app/(main)/COMPANY_30/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_30/logistics/info/page.tsx index 543dd3dd..35764744 100644 --- a/frontend/app/(main)/COMPANY_30/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_30/logistics/info/page.tsx @@ -230,11 +230,10 @@ function flattenCategories(items: any[]): { value: string; label: string }[] { const result: { value: string; label: string }[] = []; function walk(arr: any[]) { for (const item of arr) { - if (item.value || item.name) { - result.push({ - value: item.value || item.name, - label: item.label || item.name || item.value, - }); + const val = item.valueCode || item.value || item.name; + const lbl = item.valueLabel || item.label || item.name || val; + if (val) { + result.push({ value: val, label: lbl }); } if (item.children?.length) walk(item.children); } diff --git a/frontend/app/(main)/COMPANY_30/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_30/logistics/receiving/page.tsx index 85cdc23c..5b730674 100644 --- a/frontend/app/(main)/COMPANY_30/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_30/logistics/receiving/page.tsx @@ -777,12 +777,16 @@ export default function ReceivingPage() { return; } + if (!modalWarehouse) { + toast.error("창고를 선택해주세요."); + return; + } setSaving(true); try { const res = await createReceiving({ inbound_number: modalInboundNo, inbound_date: modalInboundDate, - warehouse_code: modalWarehouse || undefined, + warehouse_code: modalWarehouse, location_code: modalLocation || undefined, inspector: modalInspector || undefined, manager: modalManager || undefined, diff --git a/frontend/app/(main)/COMPANY_30/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_30/quality/item-inspection/page.tsx index 2c0e1338..1059223c 100644 --- a/frontend/app/(main)/COMPANY_30/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_30/quality/item-inspection/page.tsx @@ -72,6 +72,7 @@ export default function ItemInspectionInfoPage() { const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string }[]>([]); const [inspOptions, setInspOptions] = useState<{ code: string; label: string; detail: string; method: string; types: string[] }[]>([]); const [inspTypeCatOptions, setInspTypeCatOptions] = useState<{ code: string; label: string }[]>([]); + const [inspMethodCatOptions, setInspMethodCatOptions] = useState<{ code: string; label: string }[]>([]); const [userOptions, setUserOptions] = useState<{ code: string; label: string }[]>([]); /* 검사유형별 검사항목 rows */ @@ -118,6 +119,15 @@ export default function ItemInspectionInfoPage() { setInspTypeCatOptions(flatCats); } catch { /* skip */ } + // 검사방법 카테고리 값 로드 (코드→라벨 매핑용) + try { + const methodRes = await apiClient.get(`/table-categories/${INSPECTION_TABLE}/inspection_method/values`); + const flatMethods: { code: string; label: string }[] = []; + const flattenM = (arr: any[]) => { for (const v of arr) { flatMethods.push({ code: v.valueCode, label: v.valueLabel }); if (v.children?.length) flattenM(v.children); } }; + if (methodRes.data?.data?.length) flattenM(methodRes.data.data); + setInspMethodCatOptions(flatMethods); + } catch { /* skip */ } + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; setUserOptions(users.map((u: any) => ({ code: u.user_id || u.id, @@ -221,11 +231,13 @@ export default function ItemInspectionInfoPage() { if (!typeKey) continue; typeFlags[typeKey] = true; if (!rowMap[typeKey]) rowMap[typeKey] = []; + const mCode = r.inspection_method || ""; + const mLabel = inspMethodCatOptions.find(o => o.code === mCode)?.label || mCode; rowMap[typeKey].push({ id: r.id, inspection_standard_id: r.inspection_standard_id || "", inspection_detail: r.inspection_item_name || r.inspection_standard || "", - inspection_method: r.inspection_method || "", + inspection_method: mLabel, apply_process: "", acceptance_criteria: r.pass_criteria || "", is_required: r.is_required === "true" || r.is_required === true, @@ -270,7 +282,9 @@ export default function ItemInspectionInfoPage() { if (r.id !== rowId) return r; if (field === "inspection_standard_id") { const opt = inspOptions.find(o => o.code === value); - return { ...r, inspection_standard_id: value, inspection_detail: opt?.detail || "", inspection_method: opt?.method || "" }; + const methodCode = opt?.method || ""; + const methodLabel = inspMethodCatOptions.find(o => o.code === methodCode)?.label || methodCode; + return { ...r, inspection_standard_id: value, inspection_detail: opt?.detail || "", inspection_method: methodLabel }; } return { ...r, [field]: value }; }), @@ -471,17 +485,36 @@ export default function ItemInspectionInfoPage() { {ts.visibleColumns.map((col) => renderCell(col.key))}
- {isExpanded && group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( - + {isExpanded && ( + - {row.inspection_type} - {resolveInspLabel(row.inspection_standard_id)} - {row.inspection_item_name || "-"} - {row.inspection_method || "-"} - {row.pass_criteria || "-"} + + + + + 검사유형 + 검사기준 + 검사항목 + 검사방법 + 합격기준 + + + + {group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( + + {row.inspection_type} + {resolveInspLabel(row.inspection_standard_id)} + {row.inspection_item_name || "-"} + {row.inspection_method || "-"} + {row.pass_criteria || "-"} + + ))} + +
+
- ))} + )} ); })} diff --git a/frontend/app/(main)/COMPANY_7/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_7/logistics/info/page.tsx index 543dd3dd..35764744 100644 --- a/frontend/app/(main)/COMPANY_7/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_7/logistics/info/page.tsx @@ -230,11 +230,10 @@ function flattenCategories(items: any[]): { value: string; label: string }[] { const result: { value: string; label: string }[] = []; function walk(arr: any[]) { for (const item of arr) { - if (item.value || item.name) { - result.push({ - value: item.value || item.name, - label: item.label || item.name || item.value, - }); + const val = item.valueCode || item.value || item.name; + const lbl = item.valueLabel || item.label || item.name || val; + if (val) { + result.push({ value: val, label: lbl }); } if (item.children?.length) walk(item.children); } diff --git a/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx index 85cdc23c..5b730674 100644 --- a/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx @@ -777,12 +777,16 @@ export default function ReceivingPage() { return; } + if (!modalWarehouse) { + toast.error("창고를 선택해주세요."); + return; + } setSaving(true); try { const res = await createReceiving({ inbound_number: modalInboundNo, inbound_date: modalInboundDate, - warehouse_code: modalWarehouse || undefined, + warehouse_code: modalWarehouse, location_code: modalLocation || undefined, inspector: modalInspector || undefined, manager: modalManager || undefined, diff --git a/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx index 2c0e1338..1059223c 100644 --- a/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx @@ -72,6 +72,7 @@ export default function ItemInspectionInfoPage() { const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string }[]>([]); const [inspOptions, setInspOptions] = useState<{ code: string; label: string; detail: string; method: string; types: string[] }[]>([]); const [inspTypeCatOptions, setInspTypeCatOptions] = useState<{ code: string; label: string }[]>([]); + const [inspMethodCatOptions, setInspMethodCatOptions] = useState<{ code: string; label: string }[]>([]); const [userOptions, setUserOptions] = useState<{ code: string; label: string }[]>([]); /* 검사유형별 검사항목 rows */ @@ -118,6 +119,15 @@ export default function ItemInspectionInfoPage() { setInspTypeCatOptions(flatCats); } catch { /* skip */ } + // 검사방법 카테고리 값 로드 (코드→라벨 매핑용) + try { + const methodRes = await apiClient.get(`/table-categories/${INSPECTION_TABLE}/inspection_method/values`); + const flatMethods: { code: string; label: string }[] = []; + const flattenM = (arr: any[]) => { for (const v of arr) { flatMethods.push({ code: v.valueCode, label: v.valueLabel }); if (v.children?.length) flattenM(v.children); } }; + if (methodRes.data?.data?.length) flattenM(methodRes.data.data); + setInspMethodCatOptions(flatMethods); + } catch { /* skip */ } + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; setUserOptions(users.map((u: any) => ({ code: u.user_id || u.id, @@ -221,11 +231,13 @@ export default function ItemInspectionInfoPage() { if (!typeKey) continue; typeFlags[typeKey] = true; if (!rowMap[typeKey]) rowMap[typeKey] = []; + const mCode = r.inspection_method || ""; + const mLabel = inspMethodCatOptions.find(o => o.code === mCode)?.label || mCode; rowMap[typeKey].push({ id: r.id, inspection_standard_id: r.inspection_standard_id || "", inspection_detail: r.inspection_item_name || r.inspection_standard || "", - inspection_method: r.inspection_method || "", + inspection_method: mLabel, apply_process: "", acceptance_criteria: r.pass_criteria || "", is_required: r.is_required === "true" || r.is_required === true, @@ -270,7 +282,9 @@ export default function ItemInspectionInfoPage() { if (r.id !== rowId) return r; if (field === "inspection_standard_id") { const opt = inspOptions.find(o => o.code === value); - return { ...r, inspection_standard_id: value, inspection_detail: opt?.detail || "", inspection_method: opt?.method || "" }; + const methodCode = opt?.method || ""; + const methodLabel = inspMethodCatOptions.find(o => o.code === methodCode)?.label || methodCode; + return { ...r, inspection_standard_id: value, inspection_detail: opt?.detail || "", inspection_method: methodLabel }; } return { ...r, [field]: value }; }), @@ -471,17 +485,36 @@ export default function ItemInspectionInfoPage() { {ts.visibleColumns.map((col) => renderCell(col.key))}
- {isExpanded && group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( - + {isExpanded && ( + - {row.inspection_type} - {resolveInspLabel(row.inspection_standard_id)} - {row.inspection_item_name || "-"} - {row.inspection_method || "-"} - {row.pass_criteria || "-"} + + + + + 검사유형 + 검사기준 + 검사항목 + 검사방법 + 합격기준 + + + + {group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( + + {row.inspection_type} + {resolveInspLabel(row.inspection_standard_id)} + {row.inspection_item_name || "-"} + {row.inspection_method || "-"} + {row.pass_criteria || "-"} + + ))} + +
+
- ))} + )} ); })} diff --git a/frontend/app/(main)/COMPANY_8/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_8/logistics/info/page.tsx index 543dd3dd..35764744 100644 --- a/frontend/app/(main)/COMPANY_8/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_8/logistics/info/page.tsx @@ -230,11 +230,10 @@ function flattenCategories(items: any[]): { value: string; label: string }[] { const result: { value: string; label: string }[] = []; function walk(arr: any[]) { for (const item of arr) { - if (item.value || item.name) { - result.push({ - value: item.value || item.name, - label: item.label || item.name || item.value, - }); + const val = item.valueCode || item.value || item.name; + const lbl = item.valueLabel || item.label || item.name || val; + if (val) { + result.push({ value: val, label: lbl }); } if (item.children?.length) walk(item.children); } diff --git a/frontend/app/(main)/COMPANY_8/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_8/logistics/receiving/page.tsx index 85cdc23c..5b730674 100644 --- a/frontend/app/(main)/COMPANY_8/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_8/logistics/receiving/page.tsx @@ -777,12 +777,16 @@ export default function ReceivingPage() { return; } + if (!modalWarehouse) { + toast.error("창고를 선택해주세요."); + return; + } setSaving(true); try { const res = await createReceiving({ inbound_number: modalInboundNo, inbound_date: modalInboundDate, - warehouse_code: modalWarehouse || undefined, + warehouse_code: modalWarehouse, location_code: modalLocation || undefined, inspector: modalInspector || undefined, manager: modalManager || undefined, diff --git a/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx index 2c0e1338..1059223c 100644 --- a/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx @@ -72,6 +72,7 @@ export default function ItemInspectionInfoPage() { const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string }[]>([]); const [inspOptions, setInspOptions] = useState<{ code: string; label: string; detail: string; method: string; types: string[] }[]>([]); const [inspTypeCatOptions, setInspTypeCatOptions] = useState<{ code: string; label: string }[]>([]); + const [inspMethodCatOptions, setInspMethodCatOptions] = useState<{ code: string; label: string }[]>([]); const [userOptions, setUserOptions] = useState<{ code: string; label: string }[]>([]); /* 검사유형별 검사항목 rows */ @@ -118,6 +119,15 @@ export default function ItemInspectionInfoPage() { setInspTypeCatOptions(flatCats); } catch { /* skip */ } + // 검사방법 카테고리 값 로드 (코드→라벨 매핑용) + try { + const methodRes = await apiClient.get(`/table-categories/${INSPECTION_TABLE}/inspection_method/values`); + const flatMethods: { code: string; label: string }[] = []; + const flattenM = (arr: any[]) => { for (const v of arr) { flatMethods.push({ code: v.valueCode, label: v.valueLabel }); if (v.children?.length) flattenM(v.children); } }; + if (methodRes.data?.data?.length) flattenM(methodRes.data.data); + setInspMethodCatOptions(flatMethods); + } catch { /* skip */ } + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; setUserOptions(users.map((u: any) => ({ code: u.user_id || u.id, @@ -221,11 +231,13 @@ export default function ItemInspectionInfoPage() { if (!typeKey) continue; typeFlags[typeKey] = true; if (!rowMap[typeKey]) rowMap[typeKey] = []; + const mCode = r.inspection_method || ""; + const mLabel = inspMethodCatOptions.find(o => o.code === mCode)?.label || mCode; rowMap[typeKey].push({ id: r.id, inspection_standard_id: r.inspection_standard_id || "", inspection_detail: r.inspection_item_name || r.inspection_standard || "", - inspection_method: r.inspection_method || "", + inspection_method: mLabel, apply_process: "", acceptance_criteria: r.pass_criteria || "", is_required: r.is_required === "true" || r.is_required === true, @@ -270,7 +282,9 @@ export default function ItemInspectionInfoPage() { if (r.id !== rowId) return r; if (field === "inspection_standard_id") { const opt = inspOptions.find(o => o.code === value); - return { ...r, inspection_standard_id: value, inspection_detail: opt?.detail || "", inspection_method: opt?.method || "" }; + const methodCode = opt?.method || ""; + const methodLabel = inspMethodCatOptions.find(o => o.code === methodCode)?.label || methodCode; + return { ...r, inspection_standard_id: value, inspection_detail: opt?.detail || "", inspection_method: methodLabel }; } return { ...r, [field]: value }; }), @@ -471,17 +485,36 @@ export default function ItemInspectionInfoPage() { {ts.visibleColumns.map((col) => renderCell(col.key))}
- {isExpanded && group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( - + {isExpanded && ( + - {row.inspection_type} - {resolveInspLabel(row.inspection_standard_id)} - {row.inspection_item_name || "-"} - {row.inspection_method || "-"} - {row.pass_criteria || "-"} + + + + + 검사유형 + 검사기준 + 검사항목 + 검사방법 + 합격기준 + + + + {group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( + + {row.inspection_type} + {resolveInspLabel(row.inspection_standard_id)} + {row.inspection_item_name || "-"} + {row.inspection_method || "-"} + {row.pass_criteria || "-"} + + ))} + +
+
- ))} + )} ); })} diff --git a/frontend/app/(main)/COMPANY_9/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_9/logistics/info/page.tsx index 543dd3dd..35764744 100644 --- a/frontend/app/(main)/COMPANY_9/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_9/logistics/info/page.tsx @@ -230,11 +230,10 @@ function flattenCategories(items: any[]): { value: string; label: string }[] { const result: { value: string; label: string }[] = []; function walk(arr: any[]) { for (const item of arr) { - if (item.value || item.name) { - result.push({ - value: item.value || item.name, - label: item.label || item.name || item.value, - }); + const val = item.valueCode || item.value || item.name; + const lbl = item.valueLabel || item.label || item.name || val; + if (val) { + result.push({ value: val, label: lbl }); } if (item.children?.length) walk(item.children); } diff --git a/frontend/app/(main)/COMPANY_9/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_9/logistics/receiving/page.tsx index 85cdc23c..5b730674 100644 --- a/frontend/app/(main)/COMPANY_9/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_9/logistics/receiving/page.tsx @@ -777,12 +777,16 @@ export default function ReceivingPage() { return; } + if (!modalWarehouse) { + toast.error("창고를 선택해주세요."); + return; + } setSaving(true); try { const res = await createReceiving({ inbound_number: modalInboundNo, inbound_date: modalInboundDate, - warehouse_code: modalWarehouse || undefined, + warehouse_code: modalWarehouse, location_code: modalLocation || undefined, inspector: modalInspector || undefined, manager: modalManager || undefined, diff --git a/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx index 2c0e1338..1059223c 100644 --- a/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx @@ -72,6 +72,7 @@ export default function ItemInspectionInfoPage() { const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string }[]>([]); const [inspOptions, setInspOptions] = useState<{ code: string; label: string; detail: string; method: string; types: string[] }[]>([]); const [inspTypeCatOptions, setInspTypeCatOptions] = useState<{ code: string; label: string }[]>([]); + const [inspMethodCatOptions, setInspMethodCatOptions] = useState<{ code: string; label: string }[]>([]); const [userOptions, setUserOptions] = useState<{ code: string; label: string }[]>([]); /* 검사유형별 검사항목 rows */ @@ -118,6 +119,15 @@ export default function ItemInspectionInfoPage() { setInspTypeCatOptions(flatCats); } catch { /* skip */ } + // 검사방법 카테고리 값 로드 (코드→라벨 매핑용) + try { + const methodRes = await apiClient.get(`/table-categories/${INSPECTION_TABLE}/inspection_method/values`); + const flatMethods: { code: string; label: string }[] = []; + const flattenM = (arr: any[]) => { for (const v of arr) { flatMethods.push({ code: v.valueCode, label: v.valueLabel }); if (v.children?.length) flattenM(v.children); } }; + if (methodRes.data?.data?.length) flattenM(methodRes.data.data); + setInspMethodCatOptions(flatMethods); + } catch { /* skip */ } + const users = userRes.data?.data?.data || userRes.data?.data?.rows || []; setUserOptions(users.map((u: any) => ({ code: u.user_id || u.id, @@ -221,11 +231,13 @@ export default function ItemInspectionInfoPage() { if (!typeKey) continue; typeFlags[typeKey] = true; if (!rowMap[typeKey]) rowMap[typeKey] = []; + const mCode = r.inspection_method || ""; + const mLabel = inspMethodCatOptions.find(o => o.code === mCode)?.label || mCode; rowMap[typeKey].push({ id: r.id, inspection_standard_id: r.inspection_standard_id || "", inspection_detail: r.inspection_item_name || r.inspection_standard || "", - inspection_method: r.inspection_method || "", + inspection_method: mLabel, apply_process: "", acceptance_criteria: r.pass_criteria || "", is_required: r.is_required === "true" || r.is_required === true, @@ -270,7 +282,9 @@ export default function ItemInspectionInfoPage() { if (r.id !== rowId) return r; if (field === "inspection_standard_id") { const opt = inspOptions.find(o => o.code === value); - return { ...r, inspection_standard_id: value, inspection_detail: opt?.detail || "", inspection_method: opt?.method || "" }; + const methodCode = opt?.method || ""; + const methodLabel = inspMethodCatOptions.find(o => o.code === methodCode)?.label || methodCode; + return { ...r, inspection_standard_id: value, inspection_detail: opt?.detail || "", inspection_method: methodLabel }; } return { ...r, [field]: value }; }), @@ -471,17 +485,36 @@ export default function ItemInspectionInfoPage() { {ts.visibleColumns.map((col) => renderCell(col.key))}
- {isExpanded && group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( - + {isExpanded && ( + - {row.inspection_type} - {resolveInspLabel(row.inspection_standard_id)} - {row.inspection_item_name || "-"} - {row.inspection_method || "-"} - {row.pass_criteria || "-"} + + + + + 검사유형 + 검사기준 + 검사항목 + 검사방법 + 합격기준 + + + + {group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( + + {row.inspection_type} + {resolveInspLabel(row.inspection_standard_id)} + {row.inspection_item_name || "-"} + {row.inspection_method || "-"} + {row.pass_criteria || "-"} + + ))} + +
+
- ))} + )} ); })} From 7c97ec8ea318e6f50ced456153d345e8a6a30f8d Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 10 Apr 2026 16:55:49 +0900 Subject: [PATCH 7/9] refactor: Enhance item inspection page functionality and UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added resizable panel components to improve layout flexibility. - Updated item name label from "품목명" to "품명" for consistency. - Refactored state management for selected items and inspection types to enhance user interaction. - Improved modal handling and search functionality for item selection. - Enhanced inspection method and category loading to ensure accurate data representation. These changes aim to provide a better user experience and streamline the item inspection process across multiple companies. --- .../quality/item-inspection/page.tsx | 623 +++++++++--------- .../quality/item-inspection/page.tsx | 623 +++++++++--------- .../quality/item-inspection/page.tsx | 623 +++++++++--------- .../quality/item-inspection/page.tsx | 623 +++++++++--------- .../(main)/COMPANY_30/sales/order/page.tsx | 63 +- .../quality/item-inspection/page.tsx | 623 +++++++++--------- .../quality/item-inspection/page.tsx | 623 +++++++++--------- .../quality/item-inspection/page.tsx | 623 +++++++++--------- .../app/(main)/COMPANY_9/sales/order/page.tsx | 63 +- 9 files changed, 2156 insertions(+), 2331 deletions(-) diff --git a/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx index 1059223c..05facbbc 100644 --- a/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx @@ -9,8 +9,9 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { - Plus, Trash2, Save, Loader2, Pencil, Inbox, Settings2, Search, ChevronDown, + Plus, Trash2, Save, Loader2, Pencil, Inbox, Settings2, Search, ClipboardList, } from "lucide-react"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; import { cn } from "@/lib/utils"; @@ -20,19 +21,17 @@ import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { toast } from "sonner"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; -import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const TABLE_NAME = "item_inspection_info"; +const ITEM_TABLE = "item_info"; +const INSPECTION_TABLE = "inspection_standard"; const GRID_COLUMNS = [ { key: "item_code", label: "품목코드" }, - { key: "item_name", label: "품목명" }, + { key: "item_name", label: "품명" }, { key: "inspection_type", label: "검사유형" }, - { key: "item_count", label: "항목수" }, { key: "is_active", label: "사용여부" }, ]; -const ITEM_TABLE = "item_info"; -const INSPECTION_TABLE = "inspection_standard"; const INSPECTION_TYPES = [ { key: "incoming_inspection", label: "수입검사", matchLabels: ["수입검사", "입고검사", "수입", "입고"] }, @@ -61,25 +60,29 @@ export default function ItemInspectionInfoPage() { const [totalCount, setTotalCount] = useState(0); const [searchFilters, setSearchFilters] = useState([]); const [checkedIds, setCheckedIds] = useState([]); - const [expandedItems, setExpandedItems] = useState>(new Set()); + // 우측 패널: 선택된 품목 + const [selectedItemCode, setSelectedItemCode] = useState(null); + const [selectedTypeTab, setSelectedTypeTab] = useState(""); + + // 모달 const [modalOpen, setModalOpen] = useState(false); const [editMode, setEditMode] = useState(false); const [form, setForm] = useState>({}); const [saving, setSaving] = useState(false); - /* FK 옵션 */ + // FK 옵션 const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string }[]>([]); const [inspOptions, setInspOptions] = useState<{ code: string; label: string; detail: string; method: string; types: string[] }[]>([]); const [inspTypeCatOptions, setInspTypeCatOptions] = useState<{ code: string; label: string }[]>([]); const [inspMethodCatOptions, setInspMethodCatOptions] = useState<{ code: string; label: string }[]>([]); const [userOptions, setUserOptions] = useState<{ code: string; label: string }[]>([]); - /* 검사유형별 검사항목 rows */ + // 검사유형별 검사항목 rows (모달용) const [inspectionRows, setInspectionRows] = useState>({}); const [collapsedTypes, setCollapsedTypes] = useState>({}); - /* 품목 선택 모달 */ + // 품목 선택 모달 const [itemModalOpen, setItemModalOpen] = useState(false); const [itemSearchKeyword, setItemSearchKeyword] = useState(""); const [filteredItems, setFilteredItems] = useState([]); @@ -110,7 +113,7 @@ export default function ItemInspectionInfoPage() { types: r.inspection_type ? r.inspection_type.split(",").filter(Boolean) : [], }))); - // 검사유형 카테고리 값 로드 (코드→라벨 매핑용) + // 검사유형 카테고리 try { const catRes = await apiClient.get(`/table-categories/${INSPECTION_TABLE}/inspection_type/values`); const flatCats: { code: string; label: string }[] = []; @@ -119,7 +122,7 @@ export default function ItemInspectionInfoPage() { setInspTypeCatOptions(flatCats); } catch { /* skip */ } - // 검사방법 카테고리 값 로드 (코드→라벨 매핑용) + // 검사방법 카테고리 try { const methodRes = await apiClient.get(`/table-categories/${INSPECTION_TABLE}/inspection_method/values`); const flatMethods: { code: string; label: string }[] = []; @@ -139,20 +142,12 @@ export default function ItemInspectionInfoPage() { }, []); /* ═══════════════════ 품목 선택 모달 ═══════════════════ */ - const openItemModal = () => { - setItemSearchKeyword(""); - setFilteredItems(itemOptions); - setItemModalOpen(true); - }; - + const openItemModal = () => { setItemSearchKeyword(""); setFilteredItems(itemOptions); setItemModalOpen(true); }; const handleItemSearch = () => { const kw = itemSearchKeyword.trim().toLowerCase(); if (!kw) { setFilteredItems(itemOptions); return; } - setFilteredItems(itemOptions.filter(o => - o.code.toLowerCase().includes(kw) || o.name.toLowerCase().includes(kw) - )); + setFilteredItems(itemOptions.filter(o => o.code.toLowerCase().includes(kw) || o.name.toLowerCase().includes(kw))); }; - const selectItem = (item: typeof itemOptions[0]) => { setForm(p => ({ ...p, item_code: item.code, item_name: item.name })); setItemModalOpen(false); @@ -196,24 +191,45 @@ export default function ItemInspectionInfoPage() { return Object.values(map); }, [data]); - // 검사기준 ID → 라벨 resolve + // 선택된 품목의 그룹 데이터 + const selectedGroup = useMemo(() => { + if (!selectedItemCode) return null; + return groupedData.find(g => g.item_code === selectedItemCode) || null; + }, [selectedItemCode, groupedData]); + + // 선택된 탭의 검사항목 행 + const selectedTabRows = useMemo(() => { + if (!selectedGroup || !selectedTypeTab) return []; + return selectedGroup.rows.filter((r: any) => r.inspection_type === selectedTypeTab && r.inspection_standard_id); + }, [selectedGroup, selectedTypeTab]); + + // 검사기준 ID → 라벨 const resolveInspLabel = useCallback((id: string) => { - const opt = inspOptions.find(o => o.code === id); - return opt?.label || id || "-"; + return inspOptions.find(o => o.code === id)?.label || id || "-"; }, [inspOptions]); + // 검사방법 코드 → 라벨 + const resolveMethodLabel = useCallback((code: string) => { + return inspMethodCatOptions.find(o => o.code === code)?.label || code || "-"; + }, [inspMethodCatOptions]); + /* ═══════════════════ CRUD ═══════════════════ */ const openCreate = () => { setForm({}); setEditMode(false); setInspectionRows({}); setCollapsedTypes({}); setModalOpen(true); }; - const openEdit = async (row: any) => { + + const openEdit = async (itemCode?: string) => { + const code = itemCode || selectedItemCode; + if (!code) { toast.error("수정할 항목을 선택해주세요"); return; } + const group = groupedData.find(g => g.item_code === code); + if (!group) return; + const row = group.rows[0]; setForm({ ...row }); setEditMode(true); setCollapsedTypes({}); - // 같은 item_code의 모든 검사항목 행을 조회하여 유형별로 분류 try { const res = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, { page: 1, size: 500, - dataFilter: { enabled: true, filters: [{ columnName: "item_code", operator: "equals", value: row.item_code }] }, + dataFilter: { enabled: true, filters: [{ columnName: "item_code", operator: "equals", value: code }] }, autoFilter: true, }); const allRows = res.data?.data?.data || res.data?.data?.rows || []; @@ -222,7 +238,6 @@ export default function ItemInspectionInfoPage() { for (const r of allRows) { const inspType = r.inspection_type || ""; - // 카테고리 코드/라벨로 INSPECTION_TYPES 키 매칭 const matched = INSPECTION_TYPES.find(t => t.matchLabels.some(ml => inspType.includes(ml)) || inspTypeCatOptions.some(cat => inspType.includes(cat.code) && t.matchLabels.some(ml => cat.label.includes(ml))) @@ -243,38 +258,22 @@ export default function ItemInspectionInfoPage() { is_required: r.is_required === "true" || r.is_required === true, }); } - setInspectionRows(rowMap); setForm(p => ({ ...p, ...typeFlags })); - } catch { - setInspectionRows({}); - } + } catch { setInspectionRows({}); } setModalOpen(true); }; - /* ═══════════════════ 검사항목 행 관리 ═══════════════════ */ + /* ═══════════════════ 검사항목 행 관리 (모달) ═══════════════════ */ const addInspRow = (typeKey: string) => { setInspectionRows(prev => ({ ...prev, - [typeKey]: [...(prev[typeKey] || []), { - id: crypto.randomUUID(), - inspection_standard_id: "", - inspection_detail: "", - inspection_method: "", - apply_process: "", - acceptance_criteria: "", - is_required: false, - }], + [typeKey]: [...(prev[typeKey] || []), { id: crypto.randomUUID(), inspection_standard_id: "", inspection_detail: "", inspection_method: "", apply_process: "", acceptance_criteria: "", is_required: false }], })); }; - const removeInspRow = (typeKey: string, rowId: string) => { - setInspectionRows(prev => ({ - ...prev, - [typeKey]: (prev[typeKey] || []).filter(r => r.id !== rowId), - })); + setInspectionRows(prev => ({ ...prev, [typeKey]: (prev[typeKey] || []).filter(r => r.id !== rowId) })); }; - const updateInspRow = (typeKey: string, rowId: string, field: string, value: any) => { setInspectionRows(prev => ({ ...prev, @@ -290,28 +289,19 @@ export default function ItemInspectionInfoPage() { }), })); }; - - /** 검사유형 키에 매칭되는 검사기준만 필터링 */ const getFilteredInspOptions = (typeKey: string) => { const typeDef = INSPECTION_TYPES.find(t => t.key === typeKey); if (!typeDef) return inspOptions; - // matchLabels와 카테고리 라벨을 비교하여 해당 카테고리 코드를 찾음 - const matchCodes = inspTypeCatOptions - .filter(cat => typeDef.matchLabels.some(ml => cat.label.includes(ml))) - .map(cat => cat.code); + const matchCodes = inspTypeCatOptions.filter(cat => typeDef.matchLabels.some(ml => cat.label.includes(ml))).map(cat => cat.code); if (matchCodes.length === 0) return inspOptions; return inspOptions.filter(opt => opt.types.some(t => matchCodes.includes(t))); }; - - const toggleCollapse = (typeKey: string) => { - setCollapsedTypes(prev => ({ ...prev, [typeKey]: !prev[typeKey] })); - }; + const toggleCollapse = (typeKey: string) => { setCollapsedTypes(prev => ({ ...prev, [typeKey]: !prev[typeKey] })); }; const handleSave = async () => { - if (!form.item_code) { toast.error("품목코드는 필수 입력이에요"); return; } + if (!form.item_code) { toast.error("품목코드는 필수예요"); return; } setSaving(true); try { - // 기존 행 삭제 (수정 모드) if (editMode) { const existRes = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, { page: 1, size: 500, @@ -320,54 +310,29 @@ export default function ItemInspectionInfoPage() { }); const existing = existRes.data?.data?.data || existRes.data?.data?.rows || []; if (existing.length > 0) { - await apiClient.delete(`/table-management/tables/${TABLE_NAME}/delete`, { - data: existing.map((r: any) => ({ id: r.id })), - }); + await apiClient.delete(`/table-management/tables/${TABLE_NAME}/delete`, { data: existing.map((r: any) => ({ id: r.id })) }); } } - - // 검사유형별 항목을 개별 행으로 INSERT const enabledTypes = INSPECTION_TYPES.filter(t => !!form[t.key]); const rows: any[] = []; for (const t of enabledTypes) { - const typeLabel = t.label; const typeRows = inspectionRows[t.key] || []; if (typeRows.length === 0) { - // 유형만 체크하고 항목 없는 경우에도 1행 생성 - rows.push({ - id: crypto.randomUUID(), - item_code: form.item_code, - item_name: form.item_name, - inspection_type: typeLabel, - is_active: form.is_active || "사용", - manager_id: form.manager_id || "", - memo: form.remarks || "", - }); + rows.push({ id: crypto.randomUUID(), item_code: form.item_code, item_name: form.item_name, inspection_type: t.label, is_active: form.is_active || "사용", manager_id: form.manager_id || "", memo: form.remarks || "" }); } else { for (const r of typeRows) { rows.push({ - id: crypto.randomUUID(), - item_code: form.item_code, - item_name: form.item_name, - inspection_type: typeLabel, - inspection_standard_id: r.inspection_standard_id || "", - inspection_item_name: r.inspection_detail || "", - inspection_method: r.inspection_method || "", - pass_criteria: r.acceptance_criteria || "", - is_required: r.is_required ? "true" : "false", - is_active: form.is_active || "사용", - manager_id: form.manager_id || "", - memo: form.remarks || "", + id: crypto.randomUUID(), item_code: form.item_code, item_name: form.item_name, inspection_type: t.label, + inspection_standard_id: r.inspection_standard_id || "", inspection_item_name: r.inspection_detail || "", + inspection_method: r.inspection_method || "", pass_criteria: r.acceptance_criteria || "", + is_required: r.is_required ? "true" : "false", is_active: form.is_active || "사용", + manager_id: form.manager_id || "", memo: form.remarks || "", }); } } } - - for (const row of rows) { - await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, row); - } - - toast.success(editMode ? "품목검사정보를 수정했어요" : "품목검사정보를 등록했어요"); + for (const row of rows) { await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, row); } + toast.success(editMode ? "수정했어요" : "등록했어요"); setModalOpen(false); fetchData(); } catch { toast.error("저장에 실패했어요"); } @@ -375,157 +340,231 @@ export default function ItemInspectionInfoPage() { }; const handleDelete = async () => { - if (checkedIds.length === 0) { toast.error("삭제할 항목을 선택해주세요"); return; } - const ok = await confirm("품목검사정보 삭제", { - description: `선택한 ${checkedIds.length}건을 삭제할까요? 이 작업은 되돌릴 수 없어요.`, - }); + if (!selectedItemCode) { toast.error("삭제할 품목을 선택해주세요"); return; } + const group = groupedData.find(g => g.item_code === selectedItemCode); + if (!group) return; + const ok = await confirm(`${selectedItemCode} 검사정보 삭제`, { description: "선택한 품목의 검사정보를 모두 삭제할까요?", variant: "destructive", confirmText: "삭제" }); if (!ok) return; try { - await apiClient.delete(`/table-management/tables/${TABLE_NAME}/delete`, { - data: checkedIds.map(id => ({ id })), - }); - toast.success(`${checkedIds.length}건을 삭제했어요`); - setCheckedIds([]); + await apiClient.delete(`/table-management/tables/${TABLE_NAME}/delete`, { data: group.rows.map((r: any) => ({ id: r.id })) }); + toast.success("삭제했어요"); + setSelectedItemCode(null); fetchData(); } catch { toast.error("삭제에 실패했어요"); } }; /* ═══════════════════ JSX ═══════════════════ */ return ( -
+
{ConfirmDialogComponent} -
-
- - - - - -
- } - /> -
-
- {loading ? ( -
- ) : groupedData.length === 0 ? ( -
- -

등록된 품목검사정보가 없어요

-
- ) : ( - - - - - 0 && checkedIds.length === data.length} onCheckedChange={(v) => setCheckedIds(v ? data.map(r => r.id) : [])} /> - {ts.visibleColumns.map((col) => ( - - {col.label} - - ))} - - - - {ts.groupData(groupedData).map((group) => { - if ((group as any)._isGroupSummary || (group as any)._isGroupHeader) return null; - const isExpanded = expandedItems.has(group.item_code); - const groupIds = group.rows.map((r: any) => r.id); - const allChecked = groupIds.every((id: string) => checkedIds.includes(id)); - const renderCell = (key: string) => { - switch (key) { - case "item_code": return {group.item_code}; - case "item_name": return {group.item_name}; - case "inspection_type": return ( - -
- {group.types.map((t: string) => {t})} -
-
- ); - case "item_count": return {group.rows.filter((r: any) => r.inspection_standard_id).length}; - case "is_active": return ( - - - {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} - - - ); - default: return {(group as any)[key] ?? ""}; - } - }; - return ( - - setExpandedItems(prev => { const next = new Set(prev); if (next.has(group.item_code)) next.delete(group.item_code); else next.add(group.item_code); return next; })} - onDoubleClick={() => openEdit(group.rows[0])} - > - - - - { e.stopPropagation(); setCheckedIds(prev => allChecked ? prev.filter(id => !groupIds.includes(id)) : [...new Set([...prev, ...groupIds])]); }}> - {}} /> - - {ts.visibleColumns.map((col) => renderCell(col.key))} - - {isExpanded && ( - - - - -
- - - 검사유형 - 검사기준 - 검사항목 - 검사방법 - 합격기준 - - - - {group.rows.filter((r: any) => r.inspection_standard_id).map((row: any) => ( - - {row.inspection_type} - {resolveInspLabel(row.inspection_standard_id)} - {row.inspection_item_name || "-"} - {row.inspection_method || "-"} - {row.pass_criteria || "-"} - - ))} - -
- - - )} - - ); - })} - - - )} -
전체 {groupedData.length}건 (품목 기준)
-
+ {/* 검색 필터 */} +
+
- {/* ═══════════════════ 등록/수정 모달 (품목선택 뷰 포함) ═══════════════════ */} + {/* 좌우 분할 패널 */} +
+ + {/* ═══════ 좌측: 품목 목록 ═══════ */} + +
+
+
+ + 품목 목록 + {groupedData.length}건 +
+
+ + + + +
+
+
+ {loading ? ( +
+ ) : groupedData.length === 0 ? ( +
+ +

등록된 품목검사정보가 없어요

+
+ ) : ( + + + + {ts.visibleColumns.map((col) => ( + + {col.label} + + ))} + + + + {groupedData.map((group) => ( + { + setSelectedItemCode(group.item_code); + setSelectedTypeTab(group.types[0] || ""); + }} + > + {ts.visibleColumns.map((col) => { + switch (col.key) { + case "item_code": return {group.item_code}; + case "item_name": return {group.item_name}; + case "inspection_type": return ( + +
+ {group.types.map((t: string) => ( + {t} + ))} +
+
+ ); + case "is_active": return ( + + + {group.is_active === "사용" || group.is_active === "true" ? "사용" : "미사용"} + + + ); + default: return {(group as any)[col.key] ?? ""}; + } + })} +
+ ))} +
+
+ )} +
+
+ 전체 {groupedData.length}건 (품목 기준) +
+
+
+ + + + {/* ═══════ 우측: 검사유형별 검사항목 ═══════ */} + +
+
+
+ 검사유형별 검사항목 +
+ {selectedGroup && ( +
+ + +
+ )} +
+ + {!selectedGroup ? ( +
+
+ +

좌측에서 품목을 선택해주세요

+
+
+ ) : ( +
+ {/* 검사유형 탭 */} +
+ {selectedGroup.types.map((type: string) => { + const count = selectedGroup.rows.filter((r: any) => r.inspection_type === type && r.inspection_standard_id).length; + return ( + + ); + })} +
+ + {/* 검사항목 상세 테이블 */} +
+ {selectedTypeTab && ( +
+
+ {selectedTypeTab} + 검사항목 상세 +
+
+ + + + 검사항목 + 검사기준 + 검사방법 + 적용공정 + 판단기준 + 합격기준 + 필수 + + + + {selectedTabRows.length === 0 ? ( + + 등록된 검사항목이 없어요 + + ) : selectedTabRows.map((row: any) => ( + + {row.inspection_item_name || "-"} + {resolveInspLabel(row.inspection_standard_id)} + {resolveMethodLabel(row.inspection_method)} + {row.apply_process || "-"} + + {row.judgment_criteria ? ( + {row.judgment_criteria} + ) : "-"} + + {row.pass_criteria || "-"} + + {row.is_required === "true" || row.is_required === true ? ( + 필수 + ) : "-"} + + + ))} + +
+
+
+ )} +
+
+ )} +
+
+
+
+ + {/* ═══════════════════ 등록/수정 모달 ═══════════════════ */} { if (!open) setItemModalOpen(false); setModalOpen(open); }}> {itemModalOpen ? ( @@ -535,16 +574,8 @@ export default function ItemInspectionInfoPage() { 품목코드 또는 품목명으로 검색
- setItemSearchKeyword(e.target.value)} - onKeyDown={(e) => { if (e.key === "Enter") handleItemSearch(); }} - /> - + setItemSearchKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleItemSearch(); }} /> +
@@ -560,11 +591,7 @@ export default function ItemInspectionInfoPage() { {filteredItems.length === 0 ? ( 검색 결과가 없어요 ) : filteredItems.map((item) => ( - selectItem(item)} - > + selectItem(item)}> {item.code} {item.name} {item.item_type} @@ -574,9 +601,7 @@ export default function ItemInspectionInfoPage() {
- - - + ) : ( <> @@ -587,14 +612,14 @@ export default function ItemInspectionInfoPage() {
{/* 품목 정보 */}
-

📦 품목 정보

+

품목 정보

- +