From 9d368b1864d283d623b73a2ace7e57cb9c0dcd93 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 6 Feb 2026 17:10:24 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EB=A7=A4=ED=95=91=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20valueCode=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NumberingRuleService에서 카테고리 매핑 로직을 개선하여, valueCode를 사용한 매핑을 추가했습니다. - 카테고리 값 역변환 로직을 추가하여, category_values 테이블에서 valueCode를 통해 valueId를 조회할 수 있도록 하였습니다. - AutoConfigPanel에서 categoryValueCode를 추가하여 V2Select와의 호환성을 높였습니다. - numbering-rule.ts 타입 정의에 categoryValueCode를 추가하여, 카테고리 값 코드에 대한 매칭을 지원합니다. --- .../src/services/numberingRuleService.ts | 155 +++++++++++++----- .../numbering-rule/AutoConfigPanel.tsx | 2 + frontend/types/numbering-rule.ts | 1 + 3 files changed, 121 insertions(+), 37 deletions(-) diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index 4f5bf1e9..a8765d18 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -885,9 +885,9 @@ class NumberingRuleService { const rule = await this.getRuleById(ruleId, companyCode); if (!rule) throw new Error("규칙을 찾을 수 없습니다"); - const parts = rule.parts + const parts = await Promise.all(rule.parts .sort((a: any, b: any) => a.order - b.order) - .map((part: any) => { + .map(async (part: any) => { if (part.generationMethod === "manual") { // 수동 입력 - 항상 ____ 마커 사용 (프론트엔드에서 편집 가능하게 처리) // placeholder 텍스트는 프론트엔드에서 별도로 표시 @@ -982,17 +982,52 @@ class NumberingRuleService { // 카테고리 매핑에서 해당 값에 대한 형식 찾기 // selectedValue는 valueCode일 수 있음 (V2Select에서 valueCode를 value로 사용) const selectedValueStr = String(selectedValue); - const mapping = categoryMappings.find((m: any) => { - // ID로 매칭 + let mapping = categoryMappings.find((m: any) => { + // ID로 매칭 (기존 방식: V2Select가 valueId를 사용하던 경우) if (m.categoryValueId?.toString() === selectedValueStr) return true; - // 라벨로 매칭 - if (m.categoryValueLabel === selectedValueStr) return true; - // valueCode로 매칭 (라벨과 동일할 수 있음) + // valueCode로 매칭 (매핑에 categoryValueCode가 있는 경우) + if (m.categoryValueCode && m.categoryValueCode === selectedValueStr) + return true; + // 라벨로 매칭 (폴백) if (m.categoryValueLabel === selectedValueStr) return true; return false; }); + // 매핑을 못 찾았으면 category_values 테이블에서 valueCode → valueId 역변환 시도 + if (!mapping) { + try { + const pool = getPool(); + const [catTableName, catColumnName] = categoryKey.includes(".") + ? categoryKey.split(".") + : [categoryKey, categoryKey]; + const cvResult = await pool.query( + `SELECT value_id, value_code, value_label FROM category_values + WHERE table_name = $1 AND column_name = $2 AND value_code = $3 LIMIT 1`, + [catTableName, catColumnName, selectedValueStr] + ); + if (cvResult.rows.length > 0) { + const resolvedId = cvResult.rows[0].value_id; + const resolvedLabel = cvResult.rows[0].value_label; + mapping = categoryMappings.find((m: any) => { + if (m.categoryValueId?.toString() === String(resolvedId)) return true; + if (m.categoryValueLabel === resolvedLabel) return true; + return false; + }); + if (mapping) { + logger.info("카테고리 매핑 역변환 성공 (valueCode→valueId)", { + valueCode: selectedValueStr, + resolvedId, + resolvedLabel, + format: mapping.format, + }); + } + } + } catch (lookupError: any) { + logger.warn("카테고리 값 역변환 조회 실패", { error: lookupError.message }); + } + } + if (mapping) { logger.info("카테고리 매핑 적용", { selectedValue, @@ -1016,7 +1051,7 @@ class NumberingRuleService { logger.warn("알 수 없는 파트 타입", { partType: part.partType }); return ""; } - }); + })); const previewCode = parts.join(rule.separator || ""); logger.info("코드 미리보기 생성", { @@ -1059,9 +1094,9 @@ class NumberingRuleService { if (manualParts.length > 0 && userInputCode) { // 프리뷰 코드를 생성해서 ____ 위치 파악 // 🔧 category 파트도 처리하여 올바른 템플릿 생성 - const previewParts = rule.parts + const previewParts = await Promise.all(rule.parts .sort((a: any, b: any) => a.order - b.order) - .map((part: any) => { + .map(async (part: any) => { if (part.generationMethod === "manual") { return "____"; } @@ -1077,36 +1112,57 @@ class NumberingRuleService { return "DATEPART"; // 날짜 자리 표시 case "category": { // 카테고리 파트: formData에서 실제 값을 가져와서 매핑된 형식 사용 - const categoryKey = autoConfig.categoryKey; - const categoryMappings = autoConfig.categoryMappings || []; + const catKey2 = autoConfig.categoryKey; + const catMappings2 = autoConfig.categoryMappings || []; - if (!categoryKey || !formData) { + if (!catKey2 || !formData) { return "CATEGORY"; // 폴백 } - const columnName = categoryKey.includes(".") - ? categoryKey.split(".")[1] - : categoryKey; - const selectedValue = formData[columnName]; + const colName2 = catKey2.includes(".") + ? catKey2.split(".")[1] + : catKey2; + const selVal2 = formData[colName2]; - if (!selectedValue) { + if (!selVal2) { return "CATEGORY"; // 폴백 } - const selectedValueStr = String(selectedValue); - const mapping = categoryMappings.find((m: any) => { - if (m.categoryValueId?.toString() === selectedValueStr) - return true; - if (m.categoryValueLabel === selectedValueStr) return true; + const selValStr2 = String(selVal2); + let catMapping2 = catMappings2.find((m: any) => { + if (m.categoryValueId?.toString() === selValStr2) return true; + if (m.categoryValueCode && m.categoryValueCode === selValStr2) return true; + if (m.categoryValueLabel === selValStr2) return true; return false; }); - return mapping?.format || "CATEGORY"; + // valueCode → valueId 역변환 시도 + if (!catMapping2) { + try { + const pool2 = getPool(); + const [ct2, cc2] = catKey2.includes(".") ? catKey2.split(".") : [catKey2, catKey2]; + const cvr2 = await pool2.query( + `SELECT value_id, value_label FROM category_values WHERE table_name = $1 AND column_name = $2 AND value_code = $3 LIMIT 1`, + [ct2, cc2, selValStr2] + ); + if (cvr2.rows.length > 0) { + const rid2 = cvr2.rows[0].value_id; + const rlabel2 = cvr2.rows[0].value_label; + catMapping2 = catMappings2.find((m: any) => { + if (m.categoryValueId?.toString() === String(rid2)) return true; + if (m.categoryValueLabel === rlabel2) return true; + return false; + }); + } + } catch { /* ignore */ } + } + + return catMapping2?.format || "CATEGORY"; } default: return ""; } - }); + })); const separator = rule.separator || ""; const previewTemplate = previewParts.join(separator); @@ -1150,9 +1206,9 @@ class NumberingRuleService { } let manualPartIndex = 0; - const parts = rule.parts + const parts = await Promise.all(rule.parts .sort((a: any, b: any) => a.order - b.order) - .map((part: any) => { + .map(async (part: any) => { if (part.generationMethod === "manual") { // 추출된 수동 입력 값 사용, 없으면 기본값 사용 const manualValue = @@ -1267,28 +1323,53 @@ class NumberingRuleService { // 카테고리 매핑에서 해당 값에 대한 형식 찾기 const selectedValueStr = String(selectedValue); - const mapping = categoryMappings.find((m: any) => { - // ID로 매칭 - if (m.categoryValueId?.toString() === selectedValueStr) - return true; - // 라벨로 매칭 + let allocMapping = categoryMappings.find((m: any) => { + if (m.categoryValueId?.toString() === selectedValueStr) return true; + if (m.categoryValueCode && m.categoryValueCode === selectedValueStr) return true; if (m.categoryValueLabel === selectedValueStr) return true; return false; }); - if (mapping) { + // valueCode → valueId 역변환 시도 + if (!allocMapping) { + try { + const pool3 = getPool(); + const [ct3, cc3] = categoryKey.includes(".") ? categoryKey.split(".") : [categoryKey, categoryKey]; + const cvr3 = await pool3.query( + `SELECT value_id, value_label FROM category_values WHERE table_name = $1 AND column_name = $2 AND value_code = $3 LIMIT 1`, + [ct3, cc3, selectedValueStr] + ); + if (cvr3.rows.length > 0) { + const rid3 = cvr3.rows[0].value_id; + const rlabel3 = cvr3.rows[0].value_label; + allocMapping = categoryMappings.find((m: any) => { + if (m.categoryValueId?.toString() === String(rid3)) return true; + if (m.categoryValueLabel === rlabel3) return true; + return false; + }); + if (allocMapping) { + logger.info("allocateCode: 카테고리 매핑 역변환 성공", { + valueCode: selectedValueStr, resolvedId: rid3, format: allocMapping.format, + }); + } + } + } catch { /* ignore */ } + } + + if (allocMapping) { logger.info("allocateCode: 카테고리 매핑 적용", { selectedValue, - format: mapping.format, - categoryValueLabel: mapping.categoryValueLabel, + format: allocMapping.format, + categoryValueLabel: allocMapping.categoryValueLabel, }); - return mapping.format || ""; + return allocMapping.format || ""; } logger.warn("allocateCode: 카테고리 매핑을 찾을 수 없음", { selectedValue, availableMappings: categoryMappings.map((m: any) => ({ id: m.categoryValueId, + code: m.categoryValueCode, label: m.categoryValueLabel, })), }); @@ -1299,7 +1380,7 @@ class NumberingRuleService { logger.warn("알 수 없는 파트 타입", { partType: part.partType }); return ""; } - }); + })); const allocatedCode = parts.join(rule.separator || ""); diff --git a/frontend/components/numbering-rule/AutoConfigPanel.tsx b/frontend/components/numbering-rule/AutoConfigPanel.tsx index a902327f..b51ea500 100644 --- a/frontend/components/numbering-rule/AutoConfigPanel.tsx +++ b/frontend/components/numbering-rule/AutoConfigPanel.tsx @@ -685,6 +685,7 @@ const CategoryConfigPanel: React.FC = ({ return { valueId: selectedId, + valueCode: node.valueCode, // valueCode 추가 (V2Select 호환) valueLabel: node.valueLabel, valuePath: pathParts.join(" > "), }; @@ -698,6 +699,7 @@ const CategoryConfigPanel: React.FC = ({ const newMapping: CategoryFormatMapping = { categoryValueId: selectedInfo.valueId, + categoryValueCode: selectedInfo.valueCode, // V2Select에서 valueCode를 value로 사용하므로 매칭용 저장 categoryValueLabel: selectedInfo.valueLabel, categoryValuePath: selectedInfo.valuePath, format: newFormat.trim(), diff --git a/frontend/types/numbering-rule.ts b/frontend/types/numbering-rule.ts index 7f21fa44..49264541 100644 --- a/frontend/types/numbering-rule.ts +++ b/frontend/types/numbering-rule.ts @@ -37,6 +37,7 @@ export type DateFormat = */ export interface CategoryFormatMapping { categoryValueId: number; // 카테고리 값 ID + categoryValueCode?: string; // 카테고리 값 코드 (V2Select에서 valueCode 사용 시 매칭용) categoryValueLabel: string; // 카테고리 값 라벨 (표시용) categoryValuePath?: string; // 전체 경로 (예: "원자재/벌크/가스켓") format: string; // 생성할 형식 (예: "ITM", "VLV")