feat: 화면 그룹 생성 및 업데이트 기능 개선

- 화면 그룹 생성 시 회사 코드 결정 로직 추가: 최고 관리자가 특정 회사를 선택할 수 있도록 변경
- 부모 그룹이 있는 경우 그룹 레벨 및 계층 경로 계산 로직 추가
- 화면 그룹 생성 후 계층 경로 업데이트 기능 구현
- 화면 그룹 업데이트 시 불필요한 코드 제거 및 최적화
- 프론트엔드에서 화면 선택 시 그룹 및 서브 테이블 정보 연동 기능 개선
This commit is contained in:
DDD1542 2026-01-07 14:49:49 +09:00
parent 6925e3af3f
commit 48e9840fa0
1 changed files with 675 additions and 41 deletions

View File

@ -132,17 +132,37 @@ export const getScreenGroup = async (req: Request, res: Response) => {
// 화면 그룹 생성
export const createScreenGroup = async (req: Request, res: Response) => {
try {
const companyCode = (req.user as any).companyCode;
const userCompanyCode = (req.user as any).companyCode;
const userId = (req.user as any).userId;
const { group_name, group_code, main_table_name, description, icon, display_order, is_active } = req.body;
const { group_name, group_code, main_table_name, description, icon, display_order, is_active, parent_group_id, target_company_code } = req.body;
if (!group_name || !group_code) {
return res.status(400).json({ success: false, message: "그룹명과 그룹코드는 필수입니다." });
}
// 회사 코드 결정: 최고 관리자가 특정 회사를 선택한 경우 해당 회사로, 아니면 사용자 회사
let finalCompanyCode = userCompanyCode;
if (userCompanyCode === "*" && target_company_code) {
// 최고 관리자가 특정 회사를 선택한 경우
finalCompanyCode = target_company_code;
}
// 부모 그룹이 있으면 group_level과 hierarchy_path 계산
let groupLevel = 0;
let parentHierarchyPath = "";
if (parent_group_id) {
const parentQuery = `SELECT id, group_level, hierarchy_path FROM screen_groups WHERE id = $1`;
const parentResult = await pool.query(parentQuery, [parent_group_id]);
if (parentResult.rows.length > 0) {
groupLevel = (parentResult.rows[0].group_level || 0) + 1;
parentHierarchyPath = parentResult.rows[0].hierarchy_path || `/${parent_group_id}/`;
}
}
const query = `
INSERT INTO screen_groups (group_name, group_code, main_table_name, description, icon, display_order, is_active, company_code, writer)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
INSERT INTO screen_groups (group_name, group_code, main_table_name, description, icon, display_order, is_active, company_code, writer, parent_group_id, group_level)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *
`;
const params = [
@ -153,15 +173,27 @@ export const createScreenGroup = async (req: Request, res: Response) => {
icon || null,
display_order || 0,
is_active || 'Y',
companyCode === "*" ? "*" : companyCode,
userId
finalCompanyCode,
userId,
parent_group_id || null,
groupLevel
];
const result = await pool.query(query, params);
const newGroupId = result.rows[0].id;
logger.info("화면 그룹 생성", { companyCode, groupId: result.rows[0].id, groupName: group_name });
// hierarchy_path 업데이트
const hierarchyPath = parent_group_id
? `${parentHierarchyPath}${newGroupId}/`.replace('//', '/')
: `/${newGroupId}/`;
await pool.query(`UPDATE screen_groups SET hierarchy_path = $1 WHERE id = $2`, [hierarchyPath, newGroupId]);
res.json({ success: true, data: result.rows[0], message: "화면 그룹이 생성되었습니다." });
// 업데이트된 데이터 반환
const updatedResult = await pool.query(`SELECT * FROM screen_groups WHERE id = $1`, [newGroupId]);
logger.info("화면 그룹 생성", { userCompanyCode, finalCompanyCode, groupId: newGroupId, groupName: group_name, parentGroupId: parent_group_id });
res.json({ success: true, data: updatedResult.rows[0], message: "화면 그룹이 생성되었습니다." });
} catch (error: any) {
logger.error("화면 그룹 생성 실패:", error);
if (error.code === '23505') {
@ -175,21 +207,72 @@ export const createScreenGroup = async (req: Request, res: Response) => {
export const updateScreenGroup = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const companyCode = (req.user as any).companyCode;
const { group_name, group_code, main_table_name, description, icon, display_order, is_active } = req.body;
const userCompanyCode = (req.user as any).companyCode;
const { group_name, group_code, main_table_name, description, icon, display_order, is_active, parent_group_id, target_company_code } = req.body;
let query = `
UPDATE screen_groups
SET group_name = $1, group_code = $2, main_table_name = $3, description = $4,
icon = $5, display_order = $6, is_active = $7, updated_date = NOW()
WHERE id = $8
`;
const params: any[] = [group_name, group_code, main_table_name, description, icon, display_order, is_active, id];
// 회사 코드 결정: 최고 관리자가 특정 회사를 선택한 경우 해당 회사로, 아니면 현재 그룹의 회사 유지
let finalCompanyCode = target_company_code || null;
// 멀티테넌시 필터링
if (companyCode !== "*") {
query += ` AND company_code = $9`;
params.push(companyCode);
// 부모 그룹이 변경되면 group_level과 hierarchy_path 재계산
let groupLevel = 0;
let hierarchyPath = `/${id}/`;
if (parent_group_id !== undefined && parent_group_id !== null) {
// 자기 자신을 부모로 지정하는 것 방지
if (Number(parent_group_id) === Number(id)) {
return res.status(400).json({ success: false, message: "자기 자신을 상위 그룹으로 지정할 수 없습니다." });
}
const parentQuery = `SELECT id, group_level, hierarchy_path FROM screen_groups WHERE id = $1`;
const parentResult = await pool.query(parentQuery, [parent_group_id]);
if (parentResult.rows.length > 0) {
// 순환 참조 방지: 부모의 hierarchy_path에 현재 그룹 ID가 포함되어 있으면 오류
if (parentResult.rows[0].hierarchy_path && parentResult.rows[0].hierarchy_path.includes(`/${id}/`)) {
return res.status(400).json({ success: false, message: "하위 그룹을 상위 그룹으로 지정할 수 없습니다." });
}
groupLevel = (parentResult.rows[0].group_level || 0) + 1;
const parentPath = parentResult.rows[0].hierarchy_path || `/${parent_group_id}/`;
hierarchyPath = `${parentPath}${id}/`.replace('//', '/');
}
}
// 쿼리 구성: 회사 코드 변경 포함 여부
let query: string;
let params: any[];
if (userCompanyCode === "*" && finalCompanyCode) {
// 최고 관리자가 회사를 변경하는 경우
query = `
UPDATE screen_groups
SET group_name = $1, group_code = $2, main_table_name = $3, description = $4,
icon = $5, display_order = $6, is_active = $7, updated_date = NOW(),
parent_group_id = $8, group_level = $9, hierarchy_path = $10, company_code = $11
WHERE id = $12
`;
params = [
group_name, group_code, main_table_name, description, icon, display_order, is_active,
parent_group_id || null, groupLevel, hierarchyPath, finalCompanyCode, id
];
} else {
// 회사 코드 변경 없음
query = `
UPDATE screen_groups
SET group_name = $1, group_code = $2, main_table_name = $3, description = $4,
icon = $5, display_order = $6, is_active = $7, updated_date = NOW(),
parent_group_id = $8, group_level = $9, hierarchy_path = $10
WHERE id = $11
`;
params = [
group_name, group_code, main_table_name, description, icon, display_order, is_active,
parent_group_id || null, groupLevel, hierarchyPath, id
];
}
// 멀티테넌시 필터링 (최고 관리자가 아닌 경우)
if (userCompanyCode !== "*") {
const paramIndex = params.length + 1;
query += ` AND company_code = $${paramIndex}`;
params.push(userCompanyCode);
}
query += " RETURNING *";
@ -200,7 +283,7 @@ export const updateScreenGroup = async (req: Request, res: Response) => {
return res.status(404).json({ success: false, message: "화면 그룹을 찾을 수 없거나 권한이 없습니다." });
}
logger.info("화면 그룹 수정", { companyCode, groupId: id });
logger.info("화면 그룹 수정", { userCompanyCode, groupId: id, parentGroupId: parent_group_id, targetCompanyCode: finalCompanyCode });
res.json({ success: true, data: result.rows[0], message: "화면 그룹이 수정되었습니다." });
} catch (error: any) {
@ -904,6 +987,7 @@ export const getMultipleScreenLayoutSummary = async (req: Request, res: Response
// 여러 화면의 컴포넌트 정보 (좌표 포함) 한번에 조회
// componentType이 더 정확한 위젯 종류 (table-list, button-primary 등)
// 다양한 컴포넌트 타입에서 사용 컬럼 추출
const query = `
SELECT
screen_id,
@ -914,7 +998,15 @@ export const getMultipleScreenLayoutSummary = async (req: Request, res: Response
height,
properties->>'componentType' as component_kind,
properties->>'widgetType' as widget_type,
properties->>'label' as label
properties->>'label' as label,
COALESCE(
properties->'componentConfig'->>'bindField',
properties->>'bindField',
properties->'componentConfig'->>'field',
properties->>'field'
) as bind_field,
-- componentConfig (JavaScript에서 )
properties->'componentConfig' as component_config
FROM screen_layouts
WHERE screen_id = ANY($1)
AND component_type = 'component'
@ -944,6 +1036,75 @@ export const getMultipleScreenLayoutSummary = async (req: Request, res: Response
// componentKind가 더 정확한 타입 (table-list, button-primary, table-search-widget 등)
const componentKind = row.component_kind || row.widget_type || 'text';
const widgetType = row.widget_type || 'text';
const componentConfig = row.component_config || {};
// 다양한 컴포넌트 타입에서 usedColumns, joinColumns 추출
let usedColumns: string[] = [];
let joinColumns: string[] = [];
// 1. 기본 columns 배열에서 추출 (table-list 등)
if (Array.isArray(componentConfig.columns)) {
componentConfig.columns.forEach((col: any) => {
const colName = col.columnName || col.field || col.name;
if (colName && !usedColumns.includes(colName)) {
usedColumns.push(colName);
}
if (col.isEntityJoin === true && colName && !joinColumns.includes(colName)) {
joinColumns.push(colName);
}
});
}
// 2. split-panel-layout의 leftPanel.columns, rightPanel.columns 추출
if (componentKind === 'split-panel-layout') {
if (componentConfig.leftPanel?.columns && Array.isArray(componentConfig.leftPanel.columns)) {
componentConfig.leftPanel.columns.forEach((col: any) => {
const colName = col.name || col.columnName || col.field;
if (colName && !usedColumns.includes(colName)) {
usedColumns.push(colName);
}
});
}
if (componentConfig.rightPanel?.columns && Array.isArray(componentConfig.rightPanel.columns)) {
componentConfig.rightPanel.columns.forEach((col: any) => {
const colName = col.name || col.columnName || col.field;
if (colName) {
// customer_mng.customer_name 같은 경우 조인 컬럼으로 처리
if (colName.includes('.')) {
if (!joinColumns.includes(colName)) {
joinColumns.push(colName);
}
} else {
if (!usedColumns.includes(colName)) {
usedColumns.push(colName);
}
}
}
});
}
}
// 3. selected-items-detail-input의 additionalFields, displayColumns 추출
if (componentKind === 'selected-items-detail-input') {
if (componentConfig.additionalFields && Array.isArray(componentConfig.additionalFields)) {
componentConfig.additionalFields.forEach((field: any) => {
const fieldName = field.name || field.field;
if (fieldName && !usedColumns.includes(fieldName)) {
usedColumns.push(fieldName);
}
});
}
// displayColumns는 연관 테이블에서 가져오는 표시용 컬럼이므로
// 메인 테이블의 joinColumns가 아님 (parentDataMapping에서 별도 추출됨)
// 단, 참조용으로 usedColumns에는 추가 가능
if (componentConfig.displayColumns && Array.isArray(componentConfig.displayColumns)) {
componentConfig.displayColumns.forEach((col: any) => {
const colName = col.name || col.columnName || col.field;
// displayColumns는 연관 테이블 컬럼이므로 메인 테이블 usedColumns에 추가하지 않음
// 조인 컬럼은 parentDataMapping.targetField에서 추출됨
});
}
}
if (summaryMap[screenId]) {
summaryMap[screenId].widgetCounts[componentKind] =
@ -959,6 +1120,9 @@ export const getMultipleScreenLayoutSummary = async (req: Request, res: Response
componentKind: componentKind, // 정확한 컴포넌트 종류
widgetType: widgetType,
label: row.label,
bindField: row.bind_field || null, // 바인딩된 컬럼명
usedColumns: usedColumns, // 이 컴포넌트에서 사용하는 컬럼 목록
joinColumns: joinColumns, // 이 컴포넌트에서 조인 컬럼 목록
});
// 캔버스 크기 계산 (최대 좌표 기준)
@ -1009,9 +1173,21 @@ export const getScreenSubTables = async (req: Request, res: Response) => {
return res.status(400).json({ success: false, message: "screenIds 배열이 필요합니다." });
}
// 화면별 메인 테이블과 서브 테이블 관계 조회
// componentConfig에서 tableName, sourceTable 추출
const query = `
// 화면별 서브 테이블 그룹화
const screenSubTables: Record<number, {
screenId: number;
screenName: string;
mainTable: string;
subTables: Array<{
tableName: string;
componentType: string;
relationType: string; // 'join' | 'lookup' | 'source' | 'reference'
fieldMappings?: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }>;
}>;
}> = {};
// 1. 기존 방식: componentConfig에서 tableName, sourceTable, fieldMappings 추출
const componentQuery = `
SELECT DISTINCT
sd.screen_id,
sd.screen_name,
@ -1021,7 +1197,9 @@ export const getScreenSubTables = async (req: Request, res: Response) => {
sl.properties->'componentConfig'->>'sourceTable'
) as sub_table,
sl.properties->>'componentType' as component_type,
sl.properties->'componentConfig'->>'targetTable' as target_table
sl.properties->'componentConfig'->>'targetTable' as target_table,
sl.properties->'componentConfig'->'fieldMappings' as field_mappings,
sl.properties->'componentConfig'->'columns' as columns_config
FROM screen_definitions sd
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
WHERE sd.screen_id = ANY($1)
@ -1032,21 +1210,50 @@ export const getScreenSubTables = async (req: Request, res: Response) => {
ORDER BY sd.screen_id
`;
const result = await pool.query(query, [screenIds]);
const componentResult = await pool.query(componentQuery, [screenIds]);
// 화면별 서브 테이블 그룹화
const screenSubTables: Record<number, {
screenId: number;
screenName: string;
mainTable: string;
subTables: Array<{
tableName: string;
componentType: string;
relationType: string; // 'join' | 'lookup' | 'source'
}>;
}> = {};
// fieldMappings의 한글 컬럼명을 조회하기 위한 테이블-컬럼 쌍 수집
const columnLabelLookups: Array<{ table: string; column: string }> = [];
componentResult.rows.forEach((row: any) => {
if (row.field_mappings && Array.isArray(row.field_mappings)) {
row.field_mappings.forEach((fm: any) => {
const mainTable = row.main_table;
const subTable = row.sub_table;
if (fm.sourceField && subTable) {
columnLabelLookups.push({ table: subTable, column: fm.sourceField });
}
if (fm.targetField && mainTable) {
columnLabelLookups.push({ table: mainTable, column: fm.targetField });
}
});
}
});
result.rows.forEach((row: any) => {
// 한글 컬럼명 조회
const columnLabelMap = new Map<string, string>(); // "table.column" -> "한글명"
if (columnLabelLookups.length > 0) {
const uniqueLookups = [...new Set(columnLabelLookups.map(l => `${l.table}|${l.column}`))];
const conditions = uniqueLookups.map((lookup, i) => {
const [table, column] = lookup.split('|');
return `(table_name = $${i * 2 + 1} AND column_name = $${i * 2 + 2})`;
});
const params = uniqueLookups.flatMap(lookup => lookup.split('|'));
if (conditions.length > 0) {
const labelQuery = `
SELECT table_name, column_name, column_label
FROM column_labels
WHERE ${conditions.join(' OR ')}
`;
const labelResult = await pool.query(labelQuery, params);
labelResult.rows.forEach((row: any) => {
const key = `${row.table_name}.${row.column_name}`;
columnLabelMap.set(key, row.column_label || row.column_name);
});
}
}
componentResult.rows.forEach((row: any) => {
const screenId = row.screen_id;
const mainTable = row.main_table;
const subTable = row.sub_table;
@ -1082,15 +1289,442 @@ export const getScreenSubTables = async (req: Request, res: Response) => {
relationType = 'join';
}
// fieldMappings 파싱 (JSON 배열 또는 null)
let fieldMappings: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }> | undefined;
if (row.field_mappings && Array.isArray(row.field_mappings)) {
// 1. 직접 fieldMappings가 있는 경우
fieldMappings = row.field_mappings.map((fm: any) => {
const sourceField = fm.sourceField || fm.source_field || '';
const targetField = fm.targetField || fm.target_field || '';
// 한글 컬럼명 조회 (sourceField는 서브테이블 컬럼, targetField는 메인테이블 컬럼)
const sourceKey = `${subTable}.${sourceField}`;
const targetKey = `${mainTable}.${targetField}`;
return {
sourceField,
targetField,
// sourceField(서브테이블 컬럼)의 한글명
sourceDisplayName: columnLabelMap.get(sourceKey) || sourceField,
// targetField(메인테이블 컬럼)의 한글명
targetDisplayName: columnLabelMap.get(targetKey) || targetField,
};
}).filter((fm: any) => fm.sourceField || fm.targetField);
} else if (row.columns_config && Array.isArray(row.columns_config)) {
// 2. columns_config.mapping에서 추출 (item_info 같은 경우)
// mapping.type === 'source'인 경우: sourceField(서브테이블) → field(메인테이블)
fieldMappings = [];
row.columns_config.forEach((col: any) => {
if (col.mapping && col.mapping.type === 'source' && col.mapping.sourceField) {
fieldMappings!.push({
sourceField: col.field || '', // 메인 테이블 컬럼
targetField: col.mapping.sourceField || '', // 서브 테이블 컬럼
sourceDisplayName: col.label || col.field || '', // 한글 라벨
targetDisplayName: col.mapping.sourceField || '', // 서브 테이블은 영문만
});
}
});
if (fieldMappings.length === 0) {
fieldMappings = undefined;
}
}
screenSubTables[screenId].subTables.push({
tableName: subTable,
componentType: componentType,
relationType: relationType,
fieldMappings: fieldMappings,
});
}
});
logger.info("화면 서브 테이블 정보 조회", { screenIds, resultCount: Object.keys(screenSubTables).length });
// 2. 추가 방식: 화면에서 사용하는 컬럼 중 column_labels.reference_table이 설정된 경우
// 화면의 usedColumns/joinColumns에서 reference_table 조회
const referenceQuery = `
WITH screen_used_columns AS (
-- (componentConfig.columns에서)
SELECT
sd.screen_id,
sd.screen_name,
sd.table_name as main_table,
jsonb_array_elements_text(
COALESCE(
sl.properties->'componentConfig'->'columns',
'[]'::jsonb
)
)::jsonb->>'columnName' as column_name
FROM screen_definitions sd
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
WHERE sd.screen_id = ANY($1)
AND sl.properties->'componentConfig'->'columns' IS NOT NULL
AND jsonb_array_length(sl.properties->'componentConfig'->'columns') > 0
UNION
-- bindField도
SELECT
sd.screen_id,
sd.screen_name,
sd.table_name as main_table,
COALESCE(
sl.properties->'componentConfig'->>'bindField',
sl.properties->>'bindField',
sl.properties->'componentConfig'->>'field',
sl.properties->>'field'
) as column_name
FROM screen_definitions sd
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
WHERE sd.screen_id = ANY($1)
AND (
sl.properties->'componentConfig'->>'bindField' IS NOT NULL
OR sl.properties->>'bindField' IS NOT NULL
OR sl.properties->'componentConfig'->>'field' IS NOT NULL
OR sl.properties->>'field' IS NOT NULL
)
)
SELECT DISTINCT
suc.screen_id,
suc.screen_name,
suc.main_table,
suc.column_name,
cl.column_label as source_display_name,
cl.reference_table,
cl.reference_column,
ref_cl.column_label as target_display_name
FROM screen_used_columns suc
JOIN column_labels cl ON cl.table_name = suc.main_table AND cl.column_name = suc.column_name
LEFT JOIN column_labels ref_cl ON ref_cl.table_name = cl.reference_table AND ref_cl.column_name = cl.reference_column
WHERE cl.reference_table IS NOT NULL
AND cl.reference_table != ''
AND cl.reference_table != suc.main_table
ORDER BY suc.screen_id
`;
const referenceResult = await pool.query(referenceQuery, [screenIds]);
logger.info("column_labels reference_table 조회 결과", {
screenIds,
referenceCount: referenceResult.rows.length,
references: referenceResult.rows.map((r: any) => ({
screenId: r.screen_id,
column: r.column_name,
refTable: r.reference_table
}))
});
referenceResult.rows.forEach((row: any) => {
const screenId = row.screen_id;
const mainTable = row.main_table;
const referenceTable = row.reference_table;
if (!referenceTable || referenceTable === mainTable) {
return;
}
if (!screenSubTables[screenId]) {
screenSubTables[screenId] = {
screenId,
screenName: row.screen_name,
mainTable: mainTable || '',
subTables: [],
};
}
// 중복 체크
const exists = screenSubTables[screenId].subTables.some(
(st) => st.tableName === referenceTable
);
if (!exists) {
screenSubTables[screenId].subTables.push({
tableName: referenceTable,
componentType: 'column_reference',
relationType: 'reference',
fieldMappings: [{
sourceField: row.column_name,
targetField: row.reference_column || 'id',
sourceDisplayName: row.source_display_name || row.column_name,
targetDisplayName: row.target_display_name || row.reference_column || 'id',
}],
});
} else {
// 이미 존재하면 fieldMappings에 추가
const existingSubTable = screenSubTables[screenId].subTables.find(
(st) => st.tableName === referenceTable
);
if (existingSubTable && existingSubTable.fieldMappings) {
const mappingExists = existingSubTable.fieldMappings.some(
(fm) => fm.sourceField === row.column_name
);
if (!mappingExists) {
existingSubTable.fieldMappings.push({
sourceField: row.column_name,
targetField: row.reference_column || 'id',
sourceDisplayName: row.source_display_name || row.column_name,
targetDisplayName: row.target_display_name || row.reference_column || 'id',
});
}
}
}
});
// 3. parentDataMapping 파싱 (selected-items-detail-input 등에서 사용)
const parentMappingQuery = `
SELECT
sd.screen_id,
sd.screen_name,
sd.table_name as main_table,
sl.properties->>'componentType' as component_type,
sl.properties->'componentConfig'->'parentDataMapping' as parent_data_mapping
FROM screen_definitions sd
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
WHERE sd.screen_id = ANY($1)
AND sl.properties->'componentConfig'->'parentDataMapping' IS NOT NULL
`;
const parentMappingResult = await pool.query(parentMappingQuery, [screenIds]);
parentMappingResult.rows.forEach((row: any) => {
const screenId = row.screen_id;
const mainTable = row.main_table;
const componentType = row.component_type || 'parentDataMapping';
const parentDataMapping = row.parent_data_mapping;
if (!Array.isArray(parentDataMapping)) return;
if (!screenSubTables[screenId]) {
screenSubTables[screenId] = {
screenId,
screenName: row.screen_name,
mainTable: mainTable || '',
subTables: [],
};
}
parentDataMapping.forEach((mapping: any) => {
const sourceTable = mapping.sourceTable;
if (!sourceTable || sourceTable === mainTable) return;
// 중복 체크
const existingSubTable = screenSubTables[screenId].subTables.find(
(st) => st.tableName === sourceTable
);
const newMapping = {
sourceTable: sourceTable, // 연관 테이블 정보 추가
sourceField: mapping.sourceField || '',
targetField: mapping.targetField || '',
sourceDisplayName: mapping.sourceField || '',
targetDisplayName: mapping.targetField || '',
};
if (existingSubTable) {
// 이미 존재하면 fieldMappings에 추가
if (!existingSubTable.fieldMappings) {
existingSubTable.fieldMappings = [];
}
const mappingExists = existingSubTable.fieldMappings.some(
(fm: any) => fm.sourceField === newMapping.sourceField && fm.targetField === newMapping.targetField
);
if (!mappingExists) {
existingSubTable.fieldMappings.push(newMapping);
}
} else {
screenSubTables[screenId].subTables.push({
tableName: sourceTable,
componentType: componentType,
relationType: 'parentMapping',
fieldMappings: [newMapping],
});
}
});
});
logger.info("parentDataMapping 파싱 완료", {
screenIds,
parentMappingCount: parentMappingResult.rows.length
});
// 4. rightPanel.relation 파싱 (split-panel-layout 등에서 사용)
const rightPanelQuery = `
SELECT
sd.screen_id,
sd.screen_name,
sd.table_name as main_table,
sl.properties->>'componentType' as component_type,
sl.properties->'componentConfig'->'rightPanel'->'relation' as right_panel_relation,
sl.properties->'componentConfig'->'rightPanel'->'tableName' as right_panel_table
FROM screen_definitions sd
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
WHERE sd.screen_id = ANY($1)
AND sl.properties->'componentConfig'->'rightPanel'->'relation' IS NOT NULL
`;
const rightPanelResult = await pool.query(rightPanelQuery, [screenIds]);
rightPanelResult.rows.forEach((row: any) => {
const screenId = row.screen_id;
const mainTable = row.main_table;
const componentType = row.component_type || 'split-panel-layout';
const relation = row.right_panel_relation;
const rightPanelTable = row.right_panel_table;
// relation 객체에서 테이블 및 필드 매핑 추출
const subTable = rightPanelTable || relation?.targetTable || relation?.tableName;
if (!subTable || subTable === mainTable) return;
if (!screenSubTables[screenId]) {
screenSubTables[screenId] = {
screenId,
screenName: row.screen_name,
mainTable: mainTable || '',
subTables: [],
};
}
// 중복 체크
const existingSubTable = screenSubTables[screenId].subTables.find(
(st) => st.tableName === subTable
);
// relation에서 필드 매핑 추출
const fieldMappings: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }> = [];
if (relation?.sourceField && relation?.targetField) {
fieldMappings.push({
sourceField: relation.sourceField,
targetField: relation.targetField,
sourceDisplayName: relation.sourceField,
targetDisplayName: relation.targetField,
});
}
// fieldMappings 배열이 있는 경우
if (relation?.fieldMappings && Array.isArray(relation.fieldMappings)) {
relation.fieldMappings.forEach((fm: any) => {
fieldMappings.push({
sourceField: fm.sourceField || fm.source_field || '',
targetField: fm.targetField || fm.target_field || '',
sourceDisplayName: fm.sourceField || fm.source_field || '',
targetDisplayName: fm.targetField || fm.target_field || '',
});
});
}
if (existingSubTable) {
// 이미 존재하면 fieldMappings에 추가
if (!existingSubTable.fieldMappings) {
existingSubTable.fieldMappings = [];
}
fieldMappings.forEach((newMapping) => {
const mappingExists = existingSubTable.fieldMappings!.some(
(fm) => fm.sourceField === newMapping.sourceField && fm.targetField === newMapping.targetField
);
if (!mappingExists) {
existingSubTable.fieldMappings!.push(newMapping);
}
});
} else {
screenSubTables[screenId].subTables.push({
tableName: subTable,
componentType: componentType,
relationType: 'rightPanelRelation',
fieldMappings: fieldMappings.length > 0 ? fieldMappings : undefined,
});
}
});
logger.info("rightPanel.relation 파싱 완료", {
screenIds,
rightPanelCount: rightPanelResult.rows.length
});
// 5. 모든 fieldMappings의 한글명을 column_labels에서 가져와서 적용
// 모든 테이블/컬럼 조합을 수집
const columnLookups: Array<{ tableName: string; columnName: string }> = [];
Object.values(screenSubTables).forEach((screenData: any) => {
screenData.subTables.forEach((subTable: any) => {
if (subTable.fieldMappings) {
subTable.fieldMappings.forEach((mapping: any) => {
// sourceTable + sourceField (연관 테이블의 컬럼)
if (mapping.sourceTable && mapping.sourceField) {
columnLookups.push({ tableName: mapping.sourceTable, columnName: mapping.sourceField });
}
// mainTable + targetField (메인 테이블의 컬럼)
if (screenData.mainTable && mapping.targetField) {
columnLookups.push({ tableName: screenData.mainTable, columnName: mapping.targetField });
}
});
}
});
});
// 중복 제거
const uniqueColumnLookups = columnLookups.filter((item, index, self) =>
index === self.findIndex((t) => t.tableName === item.tableName && t.columnName === item.columnName)
);
// column_labels에서 한글명 조회
const columnLabelsMap: { [key: string]: string } = {};
if (uniqueColumnLookups.length > 0) {
const columnLabelsQuery = `
SELECT
table_name,
column_name,
column_label
FROM column_labels
WHERE (table_name, column_name) IN (
${uniqueColumnLookups.map((_, i) => `($${i * 2 + 1}, $${i * 2 + 2})`).join(', ')}
)
`;
const columnLabelsParams = uniqueColumnLookups.flatMap(item => [item.tableName, item.columnName]);
try {
const columnLabelsResult = await pool.query(columnLabelsQuery, columnLabelsParams);
columnLabelsResult.rows.forEach((row: any) => {
const key = `${row.table_name}.${row.column_name}`;
columnLabelsMap[key] = row.column_label;
});
logger.info("column_labels 한글명 조회 완료", { count: columnLabelsResult.rows.length });
} catch (error: any) {
logger.warn("column_labels 한글명 조회 실패 (무시하고 계속 진행):", error.message);
}
}
// 각 fieldMappings에 한글명 적용
Object.values(screenSubTables).forEach((screenData: any) => {
screenData.subTables.forEach((subTable: any) => {
if (subTable.fieldMappings) {
subTable.fieldMappings.forEach((mapping: any) => {
// sourceDisplayName: 연관 테이블의 컬럼 한글명
if (mapping.sourceTable && mapping.sourceField) {
const sourceKey = `${mapping.sourceTable}.${mapping.sourceField}`;
if (columnLabelsMap[sourceKey]) {
mapping.sourceDisplayName = columnLabelsMap[sourceKey];
}
}
// targetDisplayName: 메인 테이블의 컬럼 한글명
if (screenData.mainTable && mapping.targetField) {
const targetKey = `${screenData.mainTable}.${mapping.targetField}`;
if (columnLabelsMap[targetKey]) {
mapping.targetDisplayName = columnLabelsMap[targetKey];
}
}
});
}
});
});
logger.info("화면 서브 테이블 정보 조회 완료", {
screenIds,
resultCount: Object.keys(screenSubTables).length,
details: Object.values(screenSubTables).map(s => ({
screenId: s.screenId,
mainTable: s.mainTable,
subTables: s.subTables.map(st => st.tableName)
}))
});
res.json({
success: true,