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:
DDD1542
2026-02-10 10:51:23 +09:00
parent 9e1a54c738
commit 3c8c2ebcf4
5 changed files with 392 additions and 374 deletions

View File

@@ -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 정보 (참고용으로만 사용, 필터링하지 않음)

View File

@@ -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 정보를 맵으로 변환

View File

@@ -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]
);