diff --git a/.cursor/rules/multilang-component-guide.mdc b/.cursor/rules/multilang-component-guide.mdc index 60bdc0ec..97140312 100644 --- a/.cursor/rules/multilang-component-guide.mdc +++ b/.cursor/rules/multilang-component-guide.mdc @@ -140,7 +140,7 @@ if (comp.componentType === "my-new-component") { if (config?.title) { addLabel({ id: `${comp.id}_title`, - componentId: `${comp.id}_title`, + componentId: `${comp.id}_title`,- label: config.title, type: "title", parentType: "my-new-component", diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 2e67040a..9dad459c 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -1314,7 +1314,7 @@ export class TableManagementService { // 각 값을 LIKE 또는 = 조건으로 처리 const conditions: string[] = []; const values: any[] = []; - + value.forEach((v: any, idx: number) => { const safeValue = String(v).trim(); // 정확히 일치하거나, 콤마로 구분된 값 중 하나로 포함 @@ -1323,17 +1323,24 @@ export class TableManagementService { // - "2," 로 시작 // - ",2" 로 끝남 // - ",2," 중간에 포함 - const paramBase = paramIndex + (idx * 4); + const paramBase = paramIndex + idx * 4; conditions.push(`( ${columnName}::text = $${paramBase} OR ${columnName}::text LIKE $${paramBase + 1} OR ${columnName}::text LIKE $${paramBase + 2} OR ${columnName}::text LIKE $${paramBase + 3} )`); - values.push(safeValue, `${safeValue},%`, `%,${safeValue}`, `%,${safeValue},%`); + values.push( + safeValue, + `${safeValue},%`, + `%,${safeValue}`, + `%,${safeValue},%` + ); }); - logger.info(`🔍 다중 값 배열 검색: ${columnName} IN [${value.join(", ")}]`); + logger.info( + `🔍 다중 값 배열 검색: ${columnName} IN [${value.join(", ")}]` + ); return { whereClause: `(${conditions.join(" OR ")})`, values, @@ -1772,21 +1779,29 @@ export class TableManagementService { // contains 연산자 (기본): 참조 테이블의 표시 컬럼으로 검색 const referenceColumn = entityTypeInfo.referenceColumn || "id"; const referenceTable = entityTypeInfo.referenceTable; - + // displayColumn이 비어있거나 "none"이면 참조 테이블에서 자동 감지 (entityJoinService와 동일한 로직) let displayColumn = entityTypeInfo.displayColumn; - if (!displayColumn || displayColumn === "none" || displayColumn === "") { - displayColumn = await this.findDisplayColumnForTable(referenceTable, referenceColumn); + if ( + !displayColumn || + displayColumn === "none" || + displayColumn === "" + ) { + displayColumn = await this.findDisplayColumnForTable( + referenceTable, + referenceColumn + ); logger.info( `🔍 [buildEntitySearchCondition] displayColumn 자동 감지: ${referenceTable} -> ${displayColumn}` ); } // 참조 테이블의 표시 컬럼으로 검색 + // 🔧 main. 접두사 추가: EXISTS 서브쿼리에서 외부 테이블 참조 시 명시적으로 지정 return { whereClause: `EXISTS ( SELECT 1 FROM ${referenceTable} ref - WHERE ref.${referenceColumn} = ${columnName} + WHERE ref.${referenceColumn} = main.${columnName} AND ref.${displayColumn} ILIKE $${paramIndex} )`, values: [`%${value}%`], @@ -2150,14 +2165,14 @@ export class TableManagementService { // 안전한 테이블명 검증 const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, ""); - // 전체 개수 조회 - const countQuery = `SELECT COUNT(*) as count FROM ${safeTableName} ${whereClause}`; + // 전체 개수 조회 (main 별칭 추가 - buildWhereClause가 main. 접두사를 사용하므로 필요) + const countQuery = `SELECT COUNT(*) as count FROM ${safeTableName} main ${whereClause}`; const countResult = await query(countQuery, searchValues); const total = parseInt(countResult[0].count); - // 데이터 조회 + // 데이터 조회 (main 별칭 추가) const dataQuery = ` - SELECT * FROM ${safeTableName} + SELECT main.* FROM ${safeTableName} main ${whereClause} ${orderClause} LIMIT $${paramIndex} OFFSET $${paramIndex + 1} @@ -2494,7 +2509,7 @@ export class TableManagementService { skippedColumns.push(column); return; } - + const dataType = columnTypeMap.get(column) || "text"; setConditions.push( `"${column}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}` @@ -2506,7 +2521,9 @@ export class TableManagementService { }); if (skippedColumns.length > 0) { - logger.info(`⚠️ 테이블에 존재하지 않는 컬럼 스킵: ${skippedColumns.join(", ")}`); + logger.info( + `⚠️ 테이블에 존재하지 않는 컬럼 스킵: ${skippedColumns.join(", ")}` + ); } // WHERE 조건 생성 (PRIMARY KEY 우선, 없으면 모든 원본 데이터 사용) @@ -2776,10 +2793,14 @@ export class TableManagementService { // 실제 소스 컬럼이 partner_id인데 프론트엔드가 customer_id로 추론하는 경우 대응 if (!baseJoinConfig && (additionalColumn as any).referenceTable) { baseJoinConfig = joinConfigs.find( - (config) => config.referenceTable === (additionalColumn as any).referenceTable + (config) => + config.referenceTable === + (additionalColumn as any).referenceTable ); if (baseJoinConfig) { - logger.info(`🔄 referenceTable로 조인 설정 찾음: ${(additionalColumn as any).referenceTable} → ${baseJoinConfig.sourceColumn}`); + logger.info( + `🔄 referenceTable로 조인 설정 찾음: ${(additionalColumn as any).referenceTable} → ${baseJoinConfig.sourceColumn}` + ); } } @@ -2787,25 +2808,31 @@ export class TableManagementService { // joinAlias에서 실제 컬럼명 추출 const sourceColumn = baseJoinConfig.sourceColumn; // 실제 소스 컬럼 (예: partner_id) const originalJoinAlias = additionalColumn.joinAlias; // 프론트엔드가 보낸 별칭 (예: customer_id_customer_name) - + // 🔄 프론트엔드가 잘못된 소스 컬럼으로 추론한 경우 처리 // customer_id_customer_name → customer_name 추출 (customer_id_ 부분 제거) // 또는 partner_id_customer_name → customer_name 추출 (partner_id_ 부분 제거) let actualColumnName: string; - + // 프론트엔드가 보낸 joinAlias에서 실제 컬럼명 추출 const frontendSourceColumn = additionalColumn.sourceColumn; // 프론트엔드가 추론한 소스 컬럼 (customer_id) if (originalJoinAlias.startsWith(`${frontendSourceColumn}_`)) { // 프론트엔드가 추론한 소스 컬럼으로 시작하면 그 부분 제거 - actualColumnName = originalJoinAlias.replace(`${frontendSourceColumn}_`, ""); + actualColumnName = originalJoinAlias.replace( + `${frontendSourceColumn}_`, + "" + ); } else if (originalJoinAlias.startsWith(`${sourceColumn}_`)) { // 실제 소스 컬럼으로 시작하면 그 부분 제거 - actualColumnName = originalJoinAlias.replace(`${sourceColumn}_`, ""); + actualColumnName = originalJoinAlias.replace( + `${sourceColumn}_`, + "" + ); } else { // 어느 것도 아니면 원본 사용 actualColumnName = originalJoinAlias; } - + // 🆕 올바른 joinAlias 재생성 (실제 소스 컬럼 기반) const correctedJoinAlias = `${sourceColumn}_${actualColumnName}`; @@ -3199,8 +3226,10 @@ export class TableManagementService { } // Entity 조인 컬럼 검색이 있는지 확인 (기본 조인 + 추가 조인 컬럼 모두 포함) + // 🔧 sourceColumn도 포함: search={"order_no":"..."} 형태도 Entity 검색으로 인식 const allEntityColumns = [ ...joinConfigs.map((config) => config.aliasColumn), + ...joinConfigs.map((config) => config.sourceColumn), // 🔧 소스 컬럼도 포함 // 추가 조인 컬럼들도 포함 (writer_dept_code, company_code_status 등) ...joinConfigs.flatMap((config) => { const additionalColumns = []; @@ -3606,8 +3635,10 @@ export class TableManagementService { }); // main. 접두사 추가 (조인 쿼리용) + // 🔧 이미 접두사(. 앞)가 있는 경우는 교체하지 않음 (ref.column, main.column 등) + // Negative lookbehind (?> { + ): Promise< + Array<{ + leftColumn: string; + rightColumn: string; + direction: "left_to_right" | "right_to_left"; + inputType: string; + displayColumn?: string; + }> + > { try { - logger.info(`두 테이블 간 엔티티 관계 감지 시작: ${leftTable} <-> ${rightTable}`); - + logger.info( + `두 테이블 간 엔티티 관계 감지 시작: ${leftTable} <-> ${rightTable}` + ); + const relations: Array<{ leftColumn: string; rightColumn: string; @@ -4806,12 +4844,17 @@ export class TableManagementService { logger.info(`엔티티 관계 감지 완료: ${relations.length}개 발견`); relations.forEach((rel, idx) => { - logger.info(` ${idx + 1}. ${leftTable}.${rel.leftColumn} <-> ${rightTable}.${rel.rightColumn} (${rel.direction})`); + logger.info( + ` ${idx + 1}. ${leftTable}.${rel.leftColumn} <-> ${rightTable}.${rel.rightColumn} (${rel.direction})` + ); }); return relations; } catch (error) { - logger.error(`엔티티 관계 감지 실패: ${leftTable} <-> ${rightTable}`, error); + logger.error( + `엔티티 관계 감지 실패: ${leftTable} <-> ${rightTable}`, + error + ); return []; } } diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 50f7c41b..0113a9a8 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -917,10 +917,15 @@ export const SplitPanelLayoutComponent: React.FC const { entityJoinApi } = await import("@/lib/api/entityJoin"); // 복합키 조건 생성 + // 🔧 관계 필터링은 정확한 값 매칭이 필요하므로 equals 연산자 사용 + // (entity 타입 컬럼의 경우 기본 contains 연산자가 참조 테이블의 표시 컬럼으로 검색하여 실패함) const searchConditions: Record = {}; keys.forEach((key) => { if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) { - searchConditions[key.rightColumn] = leftItem[key.leftColumn]; + searchConditions[key.rightColumn] = { + value: leftItem[key.leftColumn], + operator: "equals", + }; } });