Merge remote-tracking branch 'upstream/main'
All checks were successful
Build and Push Images / build-and-push (push) Successful in 13m31s

This commit is contained in:
kjs
2026-01-22 10:32:34 +09:00
30 changed files with 1999 additions and 342 deletions

View File

@@ -606,7 +606,7 @@ router.get(
});
}
const { enableEntityJoin, groupByColumns } = req.query;
const { enableEntityJoin, groupByColumns, primaryKeyColumn } = req.query;
const enableEntityJoinFlag =
enableEntityJoin === "true" ||
(typeof enableEntityJoin === "boolean" && enableEntityJoin);
@@ -626,17 +626,22 @@ router.get(
}
}
// 🆕 primaryKeyColumn 파싱
const primaryKeyColumnStr = typeof primaryKeyColumn === "string" ? primaryKeyColumn : undefined;
console.log(`🔍 레코드 상세 조회: ${tableName}/${id}`, {
enableEntityJoin: enableEntityJoinFlag,
groupByColumns: groupByColumnsArray,
primaryKeyColumn: primaryKeyColumnStr,
});
// 레코드 상세 조회 (Entity Join 옵션 + 그룹핑 옵션 포함)
// 레코드 상세 조회 (Entity Join 옵션 + 그룹핑 옵션 + Primary Key 컬럼 포함)
const result = await dataService.getRecordDetail(
tableName,
id,
enableEntityJoinFlag,
groupByColumnsArray
groupByColumnsArray,
primaryKeyColumnStr // 🆕 Primary Key 컬럼명 전달
);
if (!result.success) {

View File

@@ -490,7 +490,8 @@ class DataService {
tableName: string,
id: string | number,
enableEntityJoin: boolean = false,
groupByColumns: string[] = []
groupByColumns: string[] = [],
primaryKeyColumn?: string // 🆕 클라이언트에서 전달한 Primary Key 컬럼명
): Promise<ServiceResponse<any>> {
try {
// 테이블 접근 검증
@@ -499,20 +500,30 @@ class DataService {
return validation.error!;
}
// Primary Key 컬럼 찾기
const pkResult = await query<{ attname: string }>(
`SELECT a.attname
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = $1::regclass AND i.indisprimary`,
[tableName]
);
// 🆕 클라이언트에서 전달한 Primary Key 컬럼이 있으면 우선 사용
let pkColumn = primaryKeyColumn || "";
// Primary Key 컬럼이 없으면 자동 감지
if (!pkColumn) {
const pkResult = await query<{ attname: string }>(
`SELECT a.attname
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = $1::regclass AND i.indisprimary`,
[tableName]
);
let pkColumn = "id"; // 기본값
if (pkResult.length > 0) {
pkColumn = pkResult[0].attname;
pkColumn = "id"; // 기본값
if (pkResult.length > 0) {
pkColumn = pkResult[0].attname;
}
console.log(`🔑 [getRecordDetail] 자동 감지된 Primary Key:`, pkResult);
} else {
console.log(`🔑 [getRecordDetail] 클라이언트 제공 Primary Key: ${pkColumn}`);
}
console.log(`🔑 [getRecordDetail] 테이블: ${tableName}, Primary Key 컬럼: ${pkColumn}, 조회 ID: ${id}`);
// 🆕 Entity Join이 활성화된 경우
if (enableEntityJoin) {
const { EntityJoinService } = await import("./entityJoinService");

View File

@@ -334,6 +334,10 @@ export class EntityJoinService {
);
});
// 🔧 _label 별칭 중복 방지를 위한 Set
// 같은 sourceColumn에서 여러 조인 설정이 있을 때 _label은 첫 번째만 생성
const generatedLabelAliases = new Set<string>();
const joinColumns = joinConfigs
.map((config) => {
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
@@ -368,16 +372,26 @@ export class EntityJoinService {
// _label 필드도 함께 SELECT (프론트엔드 getColumnUniqueValues용)
// sourceColumn_label 형식으로 추가
resultColumns.push(
`COALESCE(${alias}.${col}::TEXT, '') AS ${config.sourceColumn}_label`
);
// 🔧 중복 방지: 같은 sourceColumn에서 _label은 첫 번째만 생성
const labelAlias = `${config.sourceColumn}_label`;
if (!generatedLabelAliases.has(labelAlias)) {
resultColumns.push(
`COALESCE(${alias}.${col}::TEXT, '') AS ${labelAlias}`
);
generatedLabelAliases.add(labelAlias);
}
// 🆕 referenceColumn (PK)도 항상 SELECT (parentDataMapping용)
// 예: customer_code, item_number 등
// col과 동일해도 별도의 alias로 추가 (customer_code as customer_code)
resultColumns.push(
`COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.referenceColumn}`
);
// 🔧 중복 방지: referenceColumn도 한 번만 추가
const refColAlias = config.referenceColumn;
if (!generatedLabelAliases.has(refColAlias)) {
resultColumns.push(
`COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${refColAlias}`
);
generatedLabelAliases.add(refColAlias);
}
} else {
resultColumns.push(
`COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}`
@@ -392,6 +406,11 @@ export class EntityJoinService {
const individualAlias = `${config.sourceColumn}_${col}`;
// 🔧 중복 방지: 같은 alias가 이미 생성되었으면 스킵
if (generatedLabelAliases.has(individualAlias)) {
return;
}
if (isJoinTableColumn) {
// 조인 테이블 컬럼은 조인 별칭 사용
resultColumns.push(
@@ -403,6 +422,7 @@ export class EntityJoinService {
`COALESCE(main.${col}::TEXT, '') AS ${individualAlias}`
);
}
generatedLabelAliases.add(individualAlias);
});
// 🆕 referenceColumn (PK)도 함께 SELECT (parentDataMapping용)
@@ -410,11 +430,13 @@ export class EntityJoinService {
config.referenceTable && config.referenceTable !== tableName;
if (
isJoinTableColumn &&
!displayColumns.includes(config.referenceColumn)
!displayColumns.includes(config.referenceColumn) &&
!generatedLabelAliases.has(config.referenceColumn) // 🔧 중복 방지
) {
resultColumns.push(
`COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.referenceColumn}`
);
generatedLabelAliases.add(config.referenceColumn);
}
}

View File

@@ -921,11 +921,12 @@ export class TableManagementService {
...layout.properties,
widgetType: inputType,
inputType: inputType,
// componentConfig 내부의 type 업데이트
// componentConfig 내부의 type, inputType, webType 모두 업데이트
componentConfig: {
...layout.properties?.componentConfig,
type: newComponentType,
inputType: inputType,
webType: inputType, // 프론트엔드 SelectBasicComponent에서 카테고리 로딩 여부 판단에 사용
},
};
@@ -941,7 +942,7 @@ export class TableManagementService {
);
logger.info(
`화면 레이아웃 업데이트: screen_id=${layout.screen_id}, component_id=${layout.component_id}, widgetType=${inputType}, componentType=${newComponentType}`
`화면 레이아웃 업데이트: screen_id=${layout.screen_id}, component_id=${layout.component_id}, widgetType=${inputType}, webType=${inputType}, componentType=${newComponentType}`
);
}