feat: Enhance entity join functionality with company code support
- Updated the EntityJoinController to log the company code during entity join configuration retrieval. - Modified the entityJoinService to accept company code as a parameter, allowing for company-specific entity join detection. - Enhanced the TableManagementService to pass the company code when detecting entity joins and retrieving reference table columns. - Implemented a helper function in the SplitPanelLayoutComponent to extract additional join columns based on the entity join configuration. - Improved the SplitPanelLayoutConfigPanel to display entity join columns dynamically, enhancing user experience and functionality.
This commit is contained in:
@@ -193,10 +193,11 @@ export class EntityJoinController {
|
||||
async getEntityJoinConfigs(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
const companyCode = (req as any).user?.companyCode;
|
||||
|
||||
logger.info(`Entity 조인 설정 조회: ${tableName}`);
|
||||
logger.info(`Entity 조인 설정 조회: ${tableName} (companyCode: ${companyCode})`);
|
||||
|
||||
const joinConfigs = await entityJoinService.detectEntityJoins(tableName);
|
||||
const joinConfigs = await entityJoinService.detectEntityJoins(tableName, undefined, companyCode);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
@@ -224,11 +225,12 @@ export class EntityJoinController {
|
||||
async getReferenceTableColumns(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
const companyCode = (req as any).user?.companyCode;
|
||||
|
||||
logger.info(`참조 테이블 컬럼 조회: ${tableName}`);
|
||||
logger.info(`참조 테이블 컬럼 조회: ${tableName} (companyCode: ${companyCode})`);
|
||||
|
||||
const columns =
|
||||
await tableManagementService.getReferenceTableColumns(tableName);
|
||||
await tableManagementService.getReferenceTableColumns(tableName, companyCode);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
@@ -408,11 +410,12 @@ export class EntityJoinController {
|
||||
async getEntityJoinColumns(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
const companyCode = (req as any).user?.companyCode;
|
||||
|
||||
logger.info(`Entity 조인 컬럼 조회: ${tableName}`);
|
||||
logger.info(`Entity 조인 컬럼 조회: ${tableName} (companyCode: ${companyCode})`);
|
||||
|
||||
// 1. 현재 테이블의 Entity 조인 설정 조회
|
||||
const allJoinConfigs = await entityJoinService.detectEntityJoins(tableName);
|
||||
const allJoinConfigs = await entityJoinService.detectEntityJoins(tableName, undefined, companyCode);
|
||||
|
||||
// 🆕 화면 디자이너용: table_column_category_values는 카테고리 드롭다운용이므로 제외
|
||||
// 카테고리 값은 엔티티 조인 컬럼이 아니라 셀렉트박스 옵션으로 사용됨
|
||||
@@ -439,7 +442,7 @@ export class EntityJoinController {
|
||||
try {
|
||||
const columns =
|
||||
await tableManagementService.getReferenceTableColumns(
|
||||
config.referenceTable
|
||||
config.referenceTable, companyCode
|
||||
);
|
||||
|
||||
// 현재 display_column 정보 (참고용으로만 사용, 필터링하지 않음)
|
||||
|
||||
@@ -16,16 +16,18 @@ export class EntityJoinService {
|
||||
* 테이블의 Entity 컬럼들을 감지하여 조인 설정 생성
|
||||
* @param tableName 테이블명
|
||||
* @param screenEntityConfigs 화면별 엔티티 설정 (선택사항)
|
||||
* @param companyCode 회사코드 (회사별 설정 우선, 없으면 전체 조회)
|
||||
*/
|
||||
async detectEntityJoins(
|
||||
tableName: string,
|
||||
screenEntityConfigs?: Record<string, any>
|
||||
screenEntityConfigs?: Record<string, any>,
|
||||
companyCode?: string
|
||||
): Promise<EntityJoinConfig[]> {
|
||||
try {
|
||||
logger.info(`Entity 컬럼 감지 시작: ${tableName}`);
|
||||
logger.info(`Entity 컬럼 감지 시작: ${tableName} (companyCode: ${companyCode || 'all'})`);
|
||||
|
||||
// table_type_columns에서 entity 및 category 타입인 컬럼들 조회
|
||||
// company_code = '*' (공통 설정) 우선 조회
|
||||
// 회사코드가 있으면 해당 회사 + '*' 만 조회, 회사별 우선
|
||||
const entityColumns = await query<{
|
||||
column_name: string;
|
||||
input_type: string;
|
||||
@@ -33,14 +35,17 @@ export class EntityJoinService {
|
||||
reference_column: string;
|
||||
display_column: string | null;
|
||||
}>(
|
||||
`SELECT column_name, input_type, reference_table, reference_column, display_column
|
||||
`SELECT DISTINCT ON (column_name)
|
||||
column_name, input_type, reference_table, reference_column, display_column
|
||||
FROM table_type_columns
|
||||
WHERE table_name = $1
|
||||
AND input_type IN ('entity', 'category')
|
||||
AND company_code = '*'
|
||||
AND reference_table IS NOT NULL
|
||||
AND reference_table != ''`,
|
||||
[tableName]
|
||||
AND reference_table != ''
|
||||
${companyCode ? `AND company_code IN ($2, '*')` : ''}
|
||||
ORDER BY column_name,
|
||||
CASE WHEN company_code = '*' THEN 1 ELSE 0 END`,
|
||||
companyCode ? [tableName, companyCode] : [tableName]
|
||||
);
|
||||
|
||||
logger.info(`🔍 Entity 컬럼 조회 결과: ${entityColumns.length}개 발견`);
|
||||
@@ -272,7 +277,8 @@ export class EntityJoinService {
|
||||
orderBy: string = "",
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
columnTypes?: Map<string, string> // 컬럼명 → 데이터 타입 매핑
|
||||
columnTypes?: Map<string, string>, // 컬럼명 → 데이터 타입 매핑
|
||||
referenceTableColumns?: Map<string, string[]> // 🆕 참조 테이블별 전체 컬럼 목록
|
||||
): { query: string; aliasMap: Map<string, string> } {
|
||||
try {
|
||||
// 기본 SELECT 컬럼들 (날짜는 YYYY-MM-DD 형식, 나머지는 TEXT 캐스팅)
|
||||
@@ -338,115 +344,100 @@ export class EntityJoinService {
|
||||
);
|
||||
});
|
||||
|
||||
// 🔧 _label 별칭 중복 방지를 위한 Set
|
||||
// 같은 sourceColumn에서 여러 조인 설정이 있을 때 _label은 첫 번째만 생성
|
||||
const generatedLabelAliases = new Set<string>();
|
||||
// 🔧 생성된 별칭 중복 방지를 위한 Set
|
||||
const generatedAliases = new Set<string>();
|
||||
|
||||
const joinColumns = joinConfigs
|
||||
const joinColumns = uniqueReferenceTableConfigs
|
||||
.map((config) => {
|
||||
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
||||
const alias = aliasMap.get(aliasKey);
|
||||
const displayColumns = config.displayColumns || [
|
||||
config.displayColumn,
|
||||
];
|
||||
const separator = config.separator || " - ";
|
||||
|
||||
// 결과 컬럼 배열 (aliasColumn + _label 필드)
|
||||
const resultColumns: string[] = [];
|
||||
|
||||
if (displayColumns.length === 0 || !displayColumns[0]) {
|
||||
// displayColumns가 빈 배열이거나 첫 번째 값이 null/undefined인 경우
|
||||
// 조인 테이블의 referenceColumn을 기본값으로 사용
|
||||
resultColumns.push(
|
||||
`COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.aliasColumn}`
|
||||
);
|
||||
} else if (displayColumns.length === 1) {
|
||||
// 단일 컬럼인 경우
|
||||
const col = displayColumns[0];
|
||||
// 🆕 참조 테이블의 전체 컬럼 목록이 있으면 모든 컬럼을 SELECT
|
||||
const refTableCols = referenceTableColumns?.get(
|
||||
`${config.referenceTable}:${config.sourceColumn}`
|
||||
) || referenceTableColumns?.get(config.referenceTable);
|
||||
|
||||
// ✅ 개선: referenceTable이 설정되어 있으면 조인 테이블에서 가져옴
|
||||
// 이렇게 하면 item_info.size, item_info.material 등 모든 조인 테이블 컬럼 지원
|
||||
const isJoinTableColumn =
|
||||
config.referenceTable && config.referenceTable !== tableName;
|
||||
if (refTableCols && refTableCols.length > 0) {
|
||||
// 메타 컬럼은 제외 (메인 테이블과 중복되거나 불필요)
|
||||
const skipColumns = new Set(["company_code", "created_date", "updated_date", "writer"]);
|
||||
|
||||
for (const col of refTableCols) {
|
||||
if (skipColumns.has(col)) continue;
|
||||
|
||||
const colAlias = `${config.sourceColumn}_${col}`;
|
||||
if (generatedAliases.has(colAlias)) continue;
|
||||
|
||||
if (isJoinTableColumn) {
|
||||
resultColumns.push(
|
||||
`COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}`
|
||||
`COALESCE(${alias}."${col}"::TEXT, '') AS "${colAlias}"`
|
||||
);
|
||||
generatedAliases.add(colAlias);
|
||||
}
|
||||
|
||||
// _label 필드도 함께 SELECT (프론트엔드 getColumnUniqueValues용)
|
||||
// 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)
|
||||
// 🔧 중복 방지: referenceColumn도 한 번만 추가
|
||||
const refColAlias = config.referenceColumn;
|
||||
if (!generatedLabelAliases.has(refColAlias)) {
|
||||
resultColumns.push(
|
||||
`COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${refColAlias}`
|
||||
);
|
||||
generatedLabelAliases.add(refColAlias);
|
||||
}
|
||||
} else {
|
||||
// _label 필드도 추가 (기존 호환성)
|
||||
const labelAlias = `${config.sourceColumn}_label`;
|
||||
if (!generatedAliases.has(labelAlias)) {
|
||||
// 표시용 컬럼 자동 감지: *_name > name > label > referenceColumn
|
||||
const nameCol = refTableCols.find((c) => c.endsWith("_name") && c !== "company_name");
|
||||
const displayCol = nameCol || refTableCols.find((c) => c === "name") || config.referenceColumn;
|
||||
resultColumns.push(
|
||||
`COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}`
|
||||
`COALESCE(${alias}."${displayCol}"::TEXT, '') AS "${labelAlias}"`
|
||||
);
|
||||
generatedAliases.add(labelAlias);
|
||||
}
|
||||
} else {
|
||||
// 🆕 여러 컬럼인 경우 각 컬럼을 개별 alias로 반환 (합치지 않음)
|
||||
// 예: item_info.standard_price → sourceColumn_standard_price (item_id_standard_price)
|
||||
displayColumns.forEach((col) => {
|
||||
// 🔄 기존 로직 (참조 테이블 컬럼 목록이 없는 경우 - fallback)
|
||||
const displayColumns = config.displayColumns || [config.displayColumn];
|
||||
|
||||
if (displayColumns.length === 0 || !displayColumns[0]) {
|
||||
resultColumns.push(
|
||||
`COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.aliasColumn}`
|
||||
);
|
||||
} else if (displayColumns.length === 1) {
|
||||
const col = displayColumns[0];
|
||||
const isJoinTableColumn =
|
||||
config.referenceTable && config.referenceTable !== tableName;
|
||||
|
||||
const individualAlias = `${config.sourceColumn}_${col}`;
|
||||
|
||||
// 🔧 중복 방지: 같은 alias가 이미 생성되었으면 스킵
|
||||
if (generatedLabelAliases.has(individualAlias)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isJoinTableColumn) {
|
||||
// 조인 테이블 컬럼은 조인 별칭 사용
|
||||
resultColumns.push(
|
||||
`COALESCE(${alias}.${col}::TEXT, '') AS ${individualAlias}`
|
||||
`COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}`
|
||||
);
|
||||
const labelAlias = `${config.sourceColumn}_label`;
|
||||
if (!generatedAliases.has(labelAlias)) {
|
||||
resultColumns.push(
|
||||
`COALESCE(${alias}.${col}::TEXT, '') AS ${labelAlias}`
|
||||
);
|
||||
generatedAliases.add(labelAlias);
|
||||
}
|
||||
} else {
|
||||
// 기본 테이블 컬럼은 main 별칭 사용
|
||||
resultColumns.push(
|
||||
`COALESCE(main.${col}::TEXT, '') AS ${individualAlias}`
|
||||
`COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}`
|
||||
);
|
||||
}
|
||||
generatedLabelAliases.add(individualAlias);
|
||||
});
|
||||
} else {
|
||||
displayColumns.forEach((col) => {
|
||||
const isJoinTableColumn =
|
||||
config.referenceTable && config.referenceTable !== tableName;
|
||||
const individualAlias = `${config.sourceColumn}_${col}`;
|
||||
if (generatedAliases.has(individualAlias)) return;
|
||||
|
||||
// 🆕 referenceColumn (PK)도 함께 SELECT (parentDataMapping용)
|
||||
const isJoinTableColumn =
|
||||
config.referenceTable && config.referenceTable !== tableName;
|
||||
if (
|
||||
isJoinTableColumn &&
|
||||
!displayColumns.includes(config.referenceColumn) &&
|
||||
!generatedLabelAliases.has(config.referenceColumn) // 🔧 중복 방지
|
||||
) {
|
||||
resultColumns.push(
|
||||
`COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.referenceColumn}`
|
||||
);
|
||||
generatedLabelAliases.add(config.referenceColumn);
|
||||
if (isJoinTableColumn) {
|
||||
resultColumns.push(
|
||||
`COALESCE(${alias}.${col}::TEXT, '') AS ${individualAlias}`
|
||||
);
|
||||
} else {
|
||||
resultColumns.push(
|
||||
`COALESCE(main.${col}::TEXT, '') AS ${individualAlias}`
|
||||
);
|
||||
}
|
||||
generatedAliases.add(individualAlias);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 resultColumns를 반환
|
||||
return resultColumns.join(", ");
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
|
||||
// SELECT 절 구성
|
||||
@@ -725,7 +716,7 @@ export class EntityJoinService {
|
||||
/**
|
||||
* 참조 테이블의 컬럼 목록 조회 (UI용)
|
||||
*/
|
||||
async getReferenceTableColumns(tableName: string): Promise<
|
||||
async getReferenceTableColumns(tableName: string, companyCode?: string): Promise<
|
||||
Array<{
|
||||
columnName: string;
|
||||
displayName: string;
|
||||
@@ -750,16 +741,19 @@ export class EntityJoinService {
|
||||
);
|
||||
|
||||
// 2. table_type_columns 테이블에서 라벨과 input_type 정보 조회
|
||||
// 회사코드가 있으면 해당 회사 + '*' 만, 회사별 우선
|
||||
const columnLabels = await query<{
|
||||
column_name: string;
|
||||
column_label: string | null;
|
||||
input_type: string | null;
|
||||
}>(
|
||||
`SELECT column_name, column_label, input_type
|
||||
`SELECT DISTINCT ON (column_name) column_name, column_label, input_type
|
||||
FROM table_type_columns
|
||||
WHERE table_name = $1
|
||||
AND company_code = '*'`,
|
||||
[tableName]
|
||||
${companyCode ? `AND company_code IN ($2, '*')` : ''}
|
||||
ORDER BY column_name,
|
||||
CASE WHEN company_code = '*' THEN 1 ELSE 0 END`,
|
||||
companyCode ? [tableName, companyCode] : [tableName]
|
||||
);
|
||||
|
||||
// 3. 라벨 및 inputType 정보를 맵으로 변환
|
||||
|
||||
@@ -2875,10 +2875,11 @@ export class TableManagementService {
|
||||
};
|
||||
}
|
||||
|
||||
// Entity 조인 설정 감지 (화면별 엔티티 설정 전달)
|
||||
// Entity 조인 설정 감지 (화면별 엔티티 설정 + 회사코드 전달)
|
||||
let joinConfigs = await entityJoinService.detectEntityJoins(
|
||||
tableName,
|
||||
options.screenEntityConfigs
|
||||
options.screenEntityConfigs,
|
||||
options.companyCode
|
||||
);
|
||||
|
||||
logger.info(
|
||||
@@ -3258,6 +3259,28 @@ export class TableManagementService {
|
||||
startTime: number
|
||||
): Promise<EntityJoinResponse> {
|
||||
try {
|
||||
// 🆕 참조 테이블별 전체 컬럼 목록 미리 조회
|
||||
const referenceTableColumns = new Map<string, string[]>();
|
||||
const uniqueRefTables = new Set(
|
||||
joinConfigs
|
||||
.filter((c) => c.referenceTable !== "table_column_category_values") // 카테고리는 제외
|
||||
.map((c) => `${c.referenceTable}:${c.sourceColumn}`)
|
||||
);
|
||||
|
||||
for (const key of uniqueRefTables) {
|
||||
const refTable = key.split(":")[0];
|
||||
if (!referenceTableColumns.has(key)) {
|
||||
const cols = await query<{ column_name: string }>(
|
||||
`SELECT column_name FROM information_schema.columns
|
||||
WHERE table_name = $1 AND table_schema = 'public'
|
||||
ORDER BY ordinal_position`,
|
||||
[refTable]
|
||||
);
|
||||
referenceTableColumns.set(key, cols.map((c) => c.column_name));
|
||||
logger.info(`🔍 참조 테이블 컬럼 조회: ${refTable} → ${cols.length}개`);
|
||||
}
|
||||
}
|
||||
|
||||
// 데이터 조회 쿼리
|
||||
const dataQuery = entityJoinService.buildJoinQuery(
|
||||
tableName,
|
||||
@@ -3266,7 +3289,9 @@ export class TableManagementService {
|
||||
whereClause,
|
||||
orderBy,
|
||||
limit,
|
||||
offset
|
||||
offset,
|
||||
undefined,
|
||||
referenceTableColumns // 🆕 참조 테이블 전체 컬럼 전달
|
||||
).query;
|
||||
|
||||
// 카운트 쿼리
|
||||
@@ -3767,12 +3792,12 @@ export class TableManagementService {
|
||||
reference_table: string;
|
||||
reference_column: string;
|
||||
}>(
|
||||
`SELECT column_name, reference_table, reference_column
|
||||
`SELECT DISTINCT ON (column_name) column_name, reference_table, reference_column
|
||||
FROM table_type_columns
|
||||
WHERE table_name = $1
|
||||
AND input_type = 'entity'
|
||||
AND reference_table = $2
|
||||
AND company_code = '*'
|
||||
ORDER BY column_name, CASE WHEN company_code = '*' THEN 1 ELSE 0 END
|
||||
LIMIT 1`,
|
||||
[tableName, refTable]
|
||||
);
|
||||
@@ -3883,7 +3908,7 @@ export class TableManagementService {
|
||||
/**
|
||||
* 참조 테이블의 표시 컬럼 목록 조회
|
||||
*/
|
||||
async getReferenceTableColumns(tableName: string): Promise<
|
||||
async getReferenceTableColumns(tableName: string, companyCode?: string): Promise<
|
||||
Array<{
|
||||
columnName: string;
|
||||
displayName: string;
|
||||
@@ -3891,7 +3916,7 @@ export class TableManagementService {
|
||||
inputType?: string;
|
||||
}>
|
||||
> {
|
||||
return await entityJoinService.getReferenceTableColumns(tableName);
|
||||
return await entityJoinService.getReferenceTableColumns(tableName, companyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5005,14 +5030,14 @@ export class TableManagementService {
|
||||
input_type: string;
|
||||
display_column: string | null;
|
||||
}>(
|
||||
`SELECT column_name, reference_column, input_type, display_column
|
||||
`SELECT DISTINCT ON (column_name) column_name, reference_column, input_type, display_column
|
||||
FROM table_type_columns
|
||||
WHERE table_name = $1
|
||||
AND input_type IN ('entity', 'category')
|
||||
AND reference_table = $2
|
||||
AND reference_column IS NOT NULL
|
||||
AND reference_column != ''
|
||||
AND company_code = '*'`,
|
||||
ORDER BY column_name, CASE WHEN company_code = '*' THEN 1 ELSE 0 END`,
|
||||
[rightTable, leftTable]
|
||||
);
|
||||
|
||||
@@ -5034,14 +5059,14 @@ export class TableManagementService {
|
||||
input_type: string;
|
||||
display_column: string | null;
|
||||
}>(
|
||||
`SELECT column_name, reference_column, input_type, display_column
|
||||
`SELECT DISTINCT ON (column_name) column_name, reference_column, input_type, display_column
|
||||
FROM table_type_columns
|
||||
WHERE table_name = $1
|
||||
AND input_type IN ('entity', 'category')
|
||||
AND reference_table = $2
|
||||
AND reference_column IS NOT NULL
|
||||
AND reference_column != ''
|
||||
AND company_code = '*'`,
|
||||
ORDER BY column_name, CASE WHEN company_code = '*' THEN 1 ELSE 0 END`,
|
||||
[leftTable, rightTable]
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user