From e305e78155bc308ad43dfaff75177c284569fdc9 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Mon, 16 Mar 2026 11:43:26 +0900 Subject: [PATCH] Implement Comma Value Resolution in Entity Join Service - Added a new method `resolveCommaValues` in `EntityJoinService` to handle comma-separated values for entity joins, allowing for individual code resolution and label conversion. - Integrated the new method into `TableManagementService` to process data after executing join queries. - Enhanced the `DynamicComponentRenderer` to maintain entity label columns based on existing configurations. Made-with: Cursor --- .../src/services/entityJoinService.ts | 70 +++++++++++++++++++ .../src/services/tableManagementService.ts | 5 +- frontend/components/v2/V2Input.tsx | 2 +- .../lib/registry/DynamicComponentRenderer.tsx | 8 ++- 4 files changed, 81 insertions(+), 4 deletions(-) diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index 1f345727..1a28f67a 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -823,6 +823,76 @@ export class EntityJoinService { return []; } } + + /** + * 콤마 구분 다중값 해결 (겸직 부서 등) + * entity join이 NULL인데 소스값에 콤마가 있으면 개별 코드를 각각 조회해서 라벨로 변환 + */ + async resolveCommaValues( + data: Record[], + joinConfigs: EntityJoinConfig[] + ): Promise[]> { + if (!data.length || !joinConfigs.length) return data; + + for (const config of joinConfigs) { + const sourceCol = config.sourceColumn; + const displayCol = config.displayColumns?.[0] || config.displayColumn; + if (!displayCol || displayCol === "none") continue; + + const aliasCol = config.aliasColumn || `${sourceCol}_${displayCol}`; + const labelCol = `${sourceCol}_label`; + + const codesSet = new Set(); + const rowsToResolve: number[] = []; + + data.forEach((row, idx) => { + const srcVal = row[sourceCol]; + if (!srcVal || typeof srcVal !== "string" || !srcVal.includes(",")) return; + + const joinedVal = row[aliasCol] || row[labelCol]; + if (joinedVal && joinedVal !== "") return; + + rowsToResolve.push(idx); + srcVal.split(",").map((v: string) => v.trim()).filter(Boolean).forEach((code: string) => codesSet.add(code)); + }); + + if (codesSet.size === 0) continue; + + const codes = Array.from(codesSet); + const refCol = config.referenceColumn || "id"; + const placeholders = codes.map((_, i) => `$${i + 1}`).join(","); + try { + const result = await query>( + `SELECT "${refCol}"::TEXT as _key, "${displayCol}"::TEXT as _label + FROM ${config.referenceTable} + WHERE "${refCol}"::TEXT IN (${placeholders})`, + codes + ); + + const labelMap = new Map(); + result.forEach((r) => labelMap.set(r._key, r._label)); + + for (const idx of rowsToResolve) { + const srcVal = data[idx][sourceCol] as string; + const resolvedLabels = srcVal + .split(",") + .map((v: string) => v.trim()) + .filter(Boolean) + .map((code: string) => labelMap.get(code) || code) + .join(", "); + + data[idx][aliasCol] = resolvedLabels; + data[idx][labelCol] = resolvedLabels; + } + + logger.info(`콤마 구분 entity 값 해결: ${sourceCol} → ${codesSet.size}개 코드, ${rowsToResolve.length}개 행`); + } catch (err) { + logger.warn(`콤마 구분 entity 값 해결 실패: ${sourceCol}`, err); + } + } + + return data; + } } export const entityJoinService = new EntityJoinService(); diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 2ddae736..82b66438 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -3588,12 +3588,15 @@ export class TableManagementService { `✅ [executeJoinQuery] 조회 완료: ${dataResult?.length}개 행` ); - const data = Array.isArray(dataResult) ? dataResult : []; + let data = Array.isArray(dataResult) ? dataResult : []; const total = Array.isArray(countResult) && countResult.length > 0 ? Number((countResult[0] as any).total) : 0; + // 콤마 구분 다중값 후처리 (겸직 부서 등) + data = await entityJoinService.resolveCommaValues(data, joinConfigs); + const queryTime = Date.now() - startTime; return { diff --git a/frontend/components/v2/V2Input.tsx b/frontend/components/v2/V2Input.tsx index c8204faf..626bbbe3 100644 --- a/frontend/components/v2/V2Input.tsx +++ b/frontend/components/v2/V2Input.tsx @@ -1085,7 +1085,7 @@ export const V2Input = forwardRef((props, ref) => const hasCustomRadius = !!style?.borderRadius; const customTextStyle: React.CSSProperties = {}; - if (style?.color) customTextStyle.color = style.color; + if (style?.color) customTextStyle.color = getAdaptiveLabelColor(style.color); if (style?.fontSize) customTextStyle.fontSize = style.fontSize; if (style?.fontWeight) customTextStyle.fontWeight = style.fontWeight; if (style?.textAlign) customTextStyle.textAlign = style.textAlign as React.CSSProperties["textAlign"]; diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 859d136f..46b17fe2 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -102,12 +102,16 @@ function mergeColumnMeta(tableName: string | undefined, columnName: string | und if (dbInputType === "entity") { const refTable = meta.reference_table || meta.referenceTable; const refColumn = meta.reference_column || meta.referenceColumn; - const displayCol = meta.display_column || meta.displayColumn; + const rawDisplayCol = meta.display_column || meta.displayColumn; + const displayCol = rawDisplayCol && rawDisplayCol !== "none" && rawDisplayCol !== "" ? rawDisplayCol : undefined; if (refTable) { merged.source = "entity"; merged.entityTable = refTable; merged.entityValueColumn = refColumn || "id"; - merged.entityLabelColumn = displayCol || "name"; + // 화면 설정에 이미 entityLabelColumn이 있으면 유지, 없으면 DB 값 또는 기본값 사용 + if (!merged.entityLabelColumn) { + merged.entityLabelColumn = displayCol || "name"; + } merged.fieldType = "entity"; merged.inputType = "entity"; }