refactor: 테이블 관리 서비스에서 쿼리 및 로깅 개선

- 다중 값 배열 검색 시 조건 처리 로직 개선
- 쿼리에서 main. 접두사 추가하여 명확한 테이블 참조 보장
- 불필요한 공백 제거 및 코드 가독성 향상
- 엔티티 관계 감지 로깅 개선으로 디버깅 용이성 증가
- 새로운 수주관리 및 거래처 테이블 추가로 멀티테넌시 지원 강화
This commit is contained in:
kjs 2026-01-19 12:07:29 +09:00
parent a020985630
commit e4667cce5f
3 changed files with 85 additions and 37 deletions

View File

@ -140,7 +140,7 @@ if (comp.componentType === "my-new-component") {
if (config?.title) { if (config?.title) {
addLabel({ addLabel({
id: `${comp.id}_title`, id: `${comp.id}_title`,
componentId: `${comp.id}_title`, componentId: `${comp.id}_title`,-
label: config.title, label: config.title,
type: "title", type: "title",
parentType: "my-new-component", parentType: "my-new-component",

View File

@ -1323,17 +1323,24 @@ export class TableManagementService {
// - "2," 로 시작 // - "2," 로 시작
// - ",2" 로 끝남 // - ",2" 로 끝남
// - ",2," 중간에 포함 // - ",2," 중간에 포함
const paramBase = paramIndex + (idx * 4); const paramBase = paramIndex + idx * 4;
conditions.push(`( conditions.push(`(
${columnName}::text = $${paramBase} OR ${columnName}::text = $${paramBase} OR
${columnName}::text LIKE $${paramBase + 1} OR ${columnName}::text LIKE $${paramBase + 1} OR
${columnName}::text LIKE $${paramBase + 2} OR ${columnName}::text LIKE $${paramBase + 2} OR
${columnName}::text LIKE $${paramBase + 3} ${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 { return {
whereClause: `(${conditions.join(" OR ")})`, whereClause: `(${conditions.join(" OR ")})`,
values, values,
@ -1775,18 +1782,26 @@ export class TableManagementService {
// displayColumn이 비어있거나 "none"이면 참조 테이블에서 자동 감지 (entityJoinService와 동일한 로직) // displayColumn이 비어있거나 "none"이면 참조 테이블에서 자동 감지 (entityJoinService와 동일한 로직)
let displayColumn = entityTypeInfo.displayColumn; let displayColumn = entityTypeInfo.displayColumn;
if (!displayColumn || displayColumn === "none" || displayColumn === "") { if (
displayColumn = await this.findDisplayColumnForTable(referenceTable, referenceColumn); !displayColumn ||
displayColumn === "none" ||
displayColumn === ""
) {
displayColumn = await this.findDisplayColumnForTable(
referenceTable,
referenceColumn
);
logger.info( logger.info(
`🔍 [buildEntitySearchCondition] displayColumn 자동 감지: ${referenceTable} -> ${displayColumn}` `🔍 [buildEntitySearchCondition] displayColumn 자동 감지: ${referenceTable} -> ${displayColumn}`
); );
} }
// 참조 테이블의 표시 컬럼으로 검색 // 참조 테이블의 표시 컬럼으로 검색
// 🔧 main. 접두사 추가: EXISTS 서브쿼리에서 외부 테이블 참조 시 명시적으로 지정
return { return {
whereClause: `EXISTS ( whereClause: `EXISTS (
SELECT 1 FROM ${referenceTable} ref SELECT 1 FROM ${referenceTable} ref
WHERE ref.${referenceColumn} = ${columnName} WHERE ref.${referenceColumn} = main.${columnName}
AND ref.${displayColumn} ILIKE $${paramIndex} AND ref.${displayColumn} ILIKE $${paramIndex}
)`, )`,
values: [`%${value}%`], values: [`%${value}%`],
@ -2150,14 +2165,14 @@ export class TableManagementService {
// 안전한 테이블명 검증 // 안전한 테이블명 검증
const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, ""); const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, "");
// 전체 개수 조회 // 전체 개수 조회 (main 별칭 추가 - buildWhereClause가 main. 접두사를 사용하므로 필요)
const countQuery = `SELECT COUNT(*) as count FROM ${safeTableName} ${whereClause}`; const countQuery = `SELECT COUNT(*) as count FROM ${safeTableName} main ${whereClause}`;
const countResult = await query<any>(countQuery, searchValues); const countResult = await query<any>(countQuery, searchValues);
const total = parseInt(countResult[0].count); const total = parseInt(countResult[0].count);
// 데이터 조회 // 데이터 조회 (main 별칭 추가)
const dataQuery = ` const dataQuery = `
SELECT * FROM ${safeTableName} SELECT main.* FROM ${safeTableName} main
${whereClause} ${whereClause}
${orderClause} ${orderClause}
LIMIT $${paramIndex} OFFSET $${paramIndex + 1} LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
@ -2506,7 +2521,9 @@ export class TableManagementService {
}); });
if (skippedColumns.length > 0) { if (skippedColumns.length > 0) {
logger.info(`⚠️ 테이블에 존재하지 않는 컬럼 스킵: ${skippedColumns.join(", ")}`); logger.info(
`⚠️ 테이블에 존재하지 않는 컬럼 스킵: ${skippedColumns.join(", ")}`
);
} }
// WHERE 조건 생성 (PRIMARY KEY 우선, 없으면 모든 원본 데이터 사용) // WHERE 조건 생성 (PRIMARY KEY 우선, 없으면 모든 원본 데이터 사용)
@ -2776,10 +2793,14 @@ export class TableManagementService {
// 실제 소스 컬럼이 partner_id인데 프론트엔드가 customer_id로 추론하는 경우 대응 // 실제 소스 컬럼이 partner_id인데 프론트엔드가 customer_id로 추론하는 경우 대응
if (!baseJoinConfig && (additionalColumn as any).referenceTable) { if (!baseJoinConfig && (additionalColumn as any).referenceTable) {
baseJoinConfig = joinConfigs.find( baseJoinConfig = joinConfigs.find(
(config) => config.referenceTable === (additionalColumn as any).referenceTable (config) =>
config.referenceTable ===
(additionalColumn as any).referenceTable
); );
if (baseJoinConfig) { if (baseJoinConfig) {
logger.info(`🔄 referenceTable로 조인 설정 찾음: ${(additionalColumn as any).referenceTable}${baseJoinConfig.sourceColumn}`); logger.info(
`🔄 referenceTable로 조인 설정 찾음: ${(additionalColumn as any).referenceTable}${baseJoinConfig.sourceColumn}`
);
} }
} }
@ -2797,10 +2818,16 @@ export class TableManagementService {
const frontendSourceColumn = additionalColumn.sourceColumn; // 프론트엔드가 추론한 소스 컬럼 (customer_id) const frontendSourceColumn = additionalColumn.sourceColumn; // 프론트엔드가 추론한 소스 컬럼 (customer_id)
if (originalJoinAlias.startsWith(`${frontendSourceColumn}_`)) { if (originalJoinAlias.startsWith(`${frontendSourceColumn}_`)) {
// 프론트엔드가 추론한 소스 컬럼으로 시작하면 그 부분 제거 // 프론트엔드가 추론한 소스 컬럼으로 시작하면 그 부분 제거
actualColumnName = originalJoinAlias.replace(`${frontendSourceColumn}_`, ""); actualColumnName = originalJoinAlias.replace(
`${frontendSourceColumn}_`,
""
);
} else if (originalJoinAlias.startsWith(`${sourceColumn}_`)) { } else if (originalJoinAlias.startsWith(`${sourceColumn}_`)) {
// 실제 소스 컬럼으로 시작하면 그 부분 제거 // 실제 소스 컬럼으로 시작하면 그 부분 제거
actualColumnName = originalJoinAlias.replace(`${sourceColumn}_`, ""); actualColumnName = originalJoinAlias.replace(
`${sourceColumn}_`,
""
);
} else { } else {
// 어느 것도 아니면 원본 사용 // 어느 것도 아니면 원본 사용
actualColumnName = originalJoinAlias; actualColumnName = originalJoinAlias;
@ -3199,8 +3226,10 @@ export class TableManagementService {
} }
// Entity 조인 컬럼 검색이 있는지 확인 (기본 조인 + 추가 조인 컬럼 모두 포함) // Entity 조인 컬럼 검색이 있는지 확인 (기본 조인 + 추가 조인 컬럼 모두 포함)
// 🔧 sourceColumn도 포함: search={"order_no":"..."} 형태도 Entity 검색으로 인식
const allEntityColumns = [ const allEntityColumns = [
...joinConfigs.map((config) => config.aliasColumn), ...joinConfigs.map((config) => config.aliasColumn),
...joinConfigs.map((config) => config.sourceColumn), // 🔧 소스 컬럼도 포함
// 추가 조인 컬럼들도 포함 (writer_dept_code, company_code_status 등) // 추가 조인 컬럼들도 포함 (writer_dept_code, company_code_status 등)
...joinConfigs.flatMap((config) => { ...joinConfigs.flatMap((config) => {
const additionalColumns = []; const additionalColumns = [];
@ -3606,8 +3635,10 @@ export class TableManagementService {
}); });
// main. 접두사 추가 (조인 쿼리용) // main. 접두사 추가 (조인 쿼리용)
// 🔧 이미 접두사(. 앞)가 있는 경우는 교체하지 않음 (ref.column, main.column 등)
// Negative lookbehind (?<!\.) 사용: 앞에 .이 없는 경우만 매칭
condition = condition.replace( condition = condition.replace(
new RegExp(`\\b${columnName}\\b`, "g"), new RegExp(`(?<!\\.)\\b${columnName}\\b`, "g"),
`main.${columnName}` `main.${columnName}`
); );
conditions.push(condition); conditions.push(condition);
@ -3812,6 +3843,9 @@ export class TableManagementService {
"customer_mng", "customer_mng",
"item_info", "item_info",
"dept_info", "dept_info",
"sales_order_mng", // 🔧 수주관리 테이블 추가
"sales_order_detail", // 🔧 수주상세 테이블 추가
"partner_info", // 🔧 거래처 테이블 추가
// 필요시 추가 // 필요시 추가
]; ];
@ -4730,15 +4764,19 @@ export class TableManagementService {
async detectTableEntityRelations( async detectTableEntityRelations(
leftTable: string, leftTable: string,
rightTable: string rightTable: string
): Promise<Array<{ ): Promise<
Array<{
leftColumn: string; leftColumn: string;
rightColumn: string; rightColumn: string;
direction: "left_to_right" | "right_to_left"; direction: "left_to_right" | "right_to_left";
inputType: string; inputType: string;
displayColumn?: string; displayColumn?: string;
}>> { }>
> {
try { try {
logger.info(`두 테이블 간 엔티티 관계 감지 시작: ${leftTable} <-> ${rightTable}`); logger.info(
`두 테이블 간 엔티티 관계 감지 시작: ${leftTable} <-> ${rightTable}`
);
const relations: Array<{ const relations: Array<{
leftColumn: string; leftColumn: string;
@ -4806,12 +4844,17 @@ export class TableManagementService {
logger.info(`엔티티 관계 감지 완료: ${relations.length}개 발견`); logger.info(`엔티티 관계 감지 완료: ${relations.length}개 발견`);
relations.forEach((rel, idx) => { 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; return relations;
} catch (error) { } catch (error) {
logger.error(`엔티티 관계 감지 실패: ${leftTable} <-> ${rightTable}`, error); logger.error(
`엔티티 관계 감지 실패: ${leftTable} <-> ${rightTable}`,
error
);
return []; return [];
} }
} }

View File

@ -917,10 +917,15 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const { entityJoinApi } = await import("@/lib/api/entityJoin"); const { entityJoinApi } = await import("@/lib/api/entityJoin");
// 복합키 조건 생성 // 복합키 조건 생성
// 🔧 관계 필터링은 정확한 값 매칭이 필요하므로 equals 연산자 사용
// (entity 타입 컬럼의 경우 기본 contains 연산자가 참조 테이블의 표시 컬럼으로 검색하여 실패함)
const searchConditions: Record<string, any> = {}; const searchConditions: Record<string, any> = {};
keys.forEach((key) => { keys.forEach((key) => {
if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) { if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) {
searchConditions[key.rightColumn] = leftItem[key.leftColumn]; searchConditions[key.rightColumn] = {
value: leftItem[key.leftColumn],
operator: "equals",
};
} }
}); });