fix: 화면 복제 기능 개선 및 관련 버그 수정

- 화면 복제 기능을 개선하여 DB 구조 개편 후의 효율적인 화면 관리를 지원합니다.
- 그룹 복제 시 버튼의 `targetScreenId`가 새 화면으로 매핑되지 않는 버그를 수정하였습니다.
- 관련된 서비스 및 쿼리에서 `table_type_columns`를 사용하여 라벨 정보를 조회하도록 변경하였습니다.
- 여러 컨트롤러 및 서비스에서 `column_labels` 대신 `table_type_columns`를 참조하도록 업데이트하였습니다.
This commit is contained in:
DDD1542
2026-01-28 11:24:25 +09:00
parent 1753822211
commit 192b678bce
43 changed files with 7826 additions and 677 deletions

View File

@@ -3404,7 +3404,7 @@ export const resetUserPassword = async (
/**
* 테이블 스키마 조회 (엑셀 업로드 컬럼 매핑용)
* column_labels 테이블에서 라벨 정보도 함께 가져옴
* table_type_columns 테이블에서 라벨 정보도 함께 가져옴
*/
export async function getTableSchema(
req: AuthenticatedRequest,
@@ -3424,7 +3424,7 @@ export async function getTableSchema(
logger.info("테이블 스키마 조회", { tableName, companyCode });
// information_schema와 column_labels를 JOIN하여 컬럼 정보와 라벨 정보 함께 가져오기
// information_schema와 table_type_columns를 JOIN하여 컬럼 정보와 라벨 정보 함께 가져오기
const schemaQuery = `
SELECT
ic.column_name,
@@ -3434,15 +3434,16 @@ export async function getTableSchema(
ic.character_maximum_length,
ic.numeric_precision,
ic.numeric_scale,
cl.column_label,
cl.display_order
ttc.column_label,
ttc.display_order
FROM information_schema.columns ic
LEFT JOIN column_labels cl
ON cl.table_name = ic.table_name
AND cl.column_name = ic.column_name
LEFT JOIN table_type_columns ttc
ON ttc.table_name = ic.table_name
AND ttc.column_name = ic.column_name
AND ttc.company_code = '*'
WHERE ic.table_schema = 'public'
AND ic.table_name = $1
ORDER BY COALESCE(cl.display_order, ic.ordinal_position), ic.ordinal_position
ORDER BY COALESCE(ttc.display_order, ic.ordinal_position), ic.ordinal_position
`;
const columns = await query<any>(schemaQuery, [tableName]);

View File

@@ -130,9 +130,20 @@ router.get("/test/value/:valueId", async (req: AuthenticatedRequest, res: Respon
router.post("/test/value", async (req: AuthenticatedRequest, res: Response) => {
try {
const input: CreateCategoryValueInput = req.body;
const companyCode = req.user?.companyCode || "*";
const userCompanyCode = req.user?.companyCode || "*";
const createdBy = req.user?.userId;
// 🔧 최고 관리자가 특정 회사를 선택한 경우, targetCompanyCode 우선 사용
// 단, 최고 관리자(companyCode = '*')만 다른 회사 코드 사용 가능
let companyCode = userCompanyCode;
if (input.targetCompanyCode && userCompanyCode === "*") {
companyCode = input.targetCompanyCode;
logger.info("🔓 최고 관리자 회사 코드 오버라이드 (카테고리 값 생성)", {
originalCompanyCode: userCompanyCode,
targetCompanyCode: input.targetCompanyCode,
});
}
if (!input.tableName || !input.columnName || !input.valueCode || !input.valueLabel) {
return res.status(400).json({
success: false,

View File

@@ -36,10 +36,10 @@ export class EntityReferenceController {
search,
});
// 컬럼 정보 조회
// 컬럼 정보 조회 (table_type_columns에서)
const columnInfo = await queryOne<any>(
`SELECT * FROM column_labels
WHERE table_name = $1 AND column_name = $2
`SELECT * FROM table_type_columns
WHERE table_name = $1 AND column_name = $2 AND company_code = '*'
LIMIT 1`,
[tableName, columnName]
);
@@ -51,15 +51,15 @@ export class EntityReferenceController {
});
}
// webType 확인
if (columnInfo.web_type !== "entity") {
// inputType 확인
if (columnInfo.input_type !== "entity") {
return res.status(400).json({
success: false,
message: `컬럼 '${tableName}.${columnName}'은 entity 타입이 아닙니다. webType: ${columnInfo.web_type}`,
message: `컬럼 '${tableName}.${columnName}'은 entity 타입이 아닙니다. inputType: ${columnInfo.input_type}`,
});
}
// column_labels에서 직접 참조 정보 가져오기
// table_type_columns에서 직접 참조 정보 가져오기
const referenceTable = columnInfo.reference_table;
const referenceColumn = columnInfo.reference_column;
const displayColumn = columnInfo.display_column || "name";
@@ -68,7 +68,7 @@ export class EntityReferenceController {
if (!referenceTable || !referenceColumn) {
return res.status(400).json({
success: false,
message: `Entity 타입 컬럼 '${tableName}.${columnName}'에 참조 테이블 정보가 설정되지 않았습니다. column_labels에서 reference_table과 reference_column을 확인해주세요.`,
message: `Entity 타입 컬럼 '${tableName}.${columnName}'에 참조 테이블 정보가 설정되지 않았습니다. table_type_columns에서 reference_table과 reference_column을 확인해주세요.`,
});
}
@@ -85,7 +85,7 @@ export class EntityReferenceController {
);
return res.status(400).json({
success: false,
message: `참조 테이블 '${referenceTable}'이 존재하지 않습니다. column_labels의 reference_table 설정을 확인해주세요.`,
message: `참조 테이블 '${referenceTable}'이 존재하지 않습니다. table_type_columns의 reference_table 설정을 확인해주세요.`,
});
}

View File

@@ -627,19 +627,19 @@ export class FlowController {
return;
}
// column_labels 테이블에서 라벨 정보 조회
// table_type_columns 테이블에서 라벨 정보 조회
const { query } = await import("../database/db");
const labelRows = await query<{
column_name: string;
column_label: string | null;
}>(
`SELECT column_name, column_label
FROM column_labels
WHERE table_name = $1 AND column_label IS NOT NULL`,
FROM table_type_columns
WHERE table_name = $1 AND column_label IS NOT NULL AND company_code = '*'`,
[tableName]
);
console.log(`✅ [FlowController] column_labels 조회 완료:`, {
console.log(`✅ [FlowController] table_type_columns 조회 완료:`, {
tableName,
rowCount: labelRows.length,
labels: labelRows.map((r) => ({

View File

@@ -1310,8 +1310,8 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons
if (conditions.length > 0) {
const labelQuery = `
SELECT table_name, column_name, column_label
FROM column_labels
WHERE ${conditions.join(' OR ')}
FROM table_type_columns
WHERE (${conditions.join(' OR ')}) AND company_code = '*'
`;
const labelResult = await pool.query(labelQuery, params);
labelResult.rows.forEach((row: any) => {
@@ -1407,7 +1407,7 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons
}
});
// 2. 추가 방식: 화면에서 사용하는 컬럼 중 column_labels.reference_table이 설정된 경우
// 2. 추가 방식: 화면에서 사용하는 컬럼 중 table_type_columns.reference_table이 설정된 경우
// 화면의 usedColumns/joinColumns에서 reference_table 조회
const referenceQuery = `
WITH screen_used_columns AS (
@@ -1513,8 +1513,8 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons
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
JOIN table_type_columns cl ON cl.table_name = suc.main_table AND cl.column_name = suc.column_name AND cl.company_code = '*'
LEFT JOIN table_type_columns ref_cl ON ref_cl.table_name = cl.reference_table AND ref_cl.column_name = cl.reference_column AND ref_cl.company_code = '*'
WHERE cl.reference_table IS NOT NULL
AND cl.reference_table != ''
AND cl.reference_table != suc.main_table
@@ -1524,7 +1524,7 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons
const referenceResult = await pool.query(referenceQuery, [screenIds]);
logger.info("column_labels reference_table 조회 결과", {
logger.info("table_type_columns reference_table 조회 결과", {
screenIds,
referenceCount: referenceResult.rows.length,
references: referenceResult.rows.map((r: any) => ({
@@ -1804,7 +1804,7 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons
rightPanelCount: rightPanelResult.rows.length
});
// 5. joinedTables에 대한 FK 컬럼을 column_labels에서 조회
// 5. joinedTables에 대한 FK 컬럼을 table_type_columns에서 조회
// rightPanelRelation에서 joinedTables가 있는 경우, 해당 테이블과 조인하는 FK 컬럼 찾기
const joinedTableFKLookups: Array<{ subTableName: string; refTable: string }> = [];
Object.values(screenSubTables).forEach((screenData: any) => {
@@ -1817,7 +1817,7 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons
});
});
// column_labels에서 FK 컬럼 조회 (reference_table로 조인하는 컬럼 찾기)
// table_type_columns에서 FK 컬럼 조회 (reference_table로 조인하는 컬럼 찾기)
const joinColumnsByTable: { [key: string]: string[] } = {}; // tableName → [FK 컬럼들]
if (joinedTableFKLookups.length > 0) {
const uniqueLookups = joinedTableFKLookups.filter((item, index, self) =>
@@ -1836,10 +1836,11 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons
cl.reference_table,
cl.reference_column,
tl.table_label as reference_table_label
FROM column_labels cl
FROM table_type_columns cl
LEFT JOIN table_labels tl ON cl.reference_table = tl.table_name
WHERE cl.table_name = ANY($1)
AND cl.reference_table = ANY($2)
AND cl.company_code = '*'
`;
const fkResult = await pool.query(fkQuery, [subTableNames, refTableNames]);
@@ -1884,7 +1885,7 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons
});
}
// 5. 모든 fieldMappings의 한글명을 column_labels에서 가져와서 적용
// 5. 모든 fieldMappings의 한글명을 table_type_columns에서 가져와서 적용
// 모든 테이블/컬럼 조합을 수집
const columnLookups: Array<{ tableName: string; columnName: string }> = [];
Object.values(screenSubTables).forEach((screenData: any) => {
@@ -1909,7 +1910,7 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons
index === self.findIndex((t) => t.tableName === item.tableName && t.columnName === item.columnName)
);
// column_labels에서 한글명 조회
// table_type_columns에서 한글명 조회
const columnLabelsMap: { [key: string]: string } = {};
if (uniqueColumnLookups.length > 0) {
const columnLabelsQuery = `
@@ -1917,10 +1918,11 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons
table_name,
column_name,
column_label
FROM column_labels
FROM table_type_columns
WHERE (table_name, column_name) IN (
${uniqueColumnLookups.map((_, i) => `($${i * 2 + 1}, $${i * 2 + 2})`).join(', ')}
)
AND company_code = '*'
`;
const columnLabelsParams = uniqueColumnLookups.flatMap(item => [item.tableName, item.columnName]);
@@ -1930,9 +1932,9 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons
const key = `${row.table_name}.${row.column_name}`;
columnLabelsMap[key] = row.column_label;
});
logger.info("column_labels 한글명 조회 완료", { count: columnLabelsResult.rows.length });
logger.info("table_type_columns 한글명 조회 완료", { count: columnLabelsResult.rows.length });
} catch (error: any) {
logger.warn("column_labels 한글명 조회 실패 (무시하고 계속 진행):", error.message);
logger.warn("table_type_columns 한글명 조회 실패 (무시하고 계속 진행):", error.message);
}
}
@@ -2421,3 +2423,4 @@ export const getMenuTreeFromScreenGroups = async (req: AuthenticatedRequest, res
});
}
};

View File

@@ -674,6 +674,63 @@ export const getLayout = async (req: AuthenticatedRequest, res: Response) => {
}
};
// V1 레이아웃 조회 (component_url + custom_config 기반)
export const getLayoutV1 = async (req: AuthenticatedRequest, res: Response) => {
try {
const { screenId } = req.params;
const { companyCode } = req.user as any;
const layout = await screenManagementService.getLayoutV1(
parseInt(screenId),
companyCode
);
res.json({ success: true, data: layout });
} catch (error) {
console.error("V3 레이아웃 조회 실패:", error);
res
.status(500)
.json({ success: false, message: "V3 레이아웃 조회에 실패했습니다." });
}
};
// V2 레이아웃 조회 (1 레코드 방식 - url + overrides)
export const getLayoutV2 = async (req: AuthenticatedRequest, res: Response) => {
try {
const { screenId } = req.params;
const { companyCode } = req.user as any;
const layout = await screenManagementService.getLayoutV2(
parseInt(screenId),
companyCode
);
res.json({ success: true, data: layout });
} catch (error) {
console.error("V2 레이아웃 조회 실패:", error);
res
.status(500)
.json({ success: false, message: "V2 레이아웃 조회에 실패했습니다." });
}
};
// V2 레이아웃 저장 (1 레코드 방식 - url + overrides)
export const saveLayoutV2 = async (req: AuthenticatedRequest, res: Response) => {
try {
const { screenId } = req.params;
const { companyCode } = req.user as any;
const layoutData = req.body;
await screenManagementService.saveLayoutV2(
parseInt(screenId),
layoutData,
companyCode
);
res.json({ success: true, message: "V2 레이아웃이 저장되었습니다." });
} catch (error) {
console.error("V2 레이아웃 저장 실패:", error);
res
.status(500)
.json({ success: false, message: "V2 레이아웃 저장에 실패했습니다." });
}
};
// 화면 코드 자동 생성
export const generateScreenCode = async (
req: AuthenticatedRequest,

View File

@@ -1682,14 +1682,11 @@ export async function getCategoryColumnsByCompany(
) AS "tableLabel",
ttc.column_name AS "columnName",
COALESCE(
cl.column_label,
ttc.column_label,
initcap(replace(ttc.column_name, '_', ' '))
) AS "columnLabel",
ttc.input_type AS "inputType"
FROM table_type_columns ttc
LEFT JOIN column_labels cl
ON ttc.table_name = cl.table_name
AND ttc.column_name = cl.column_name
LEFT JOIN table_labels tl
ON ttc.table_name = tl.table_name
WHERE ttc.input_type = 'category'
@@ -1712,14 +1709,11 @@ export async function getCategoryColumnsByCompany(
) AS "tableLabel",
ttc.column_name AS "columnName",
COALESCE(
cl.column_label,
ttc.column_label,
initcap(replace(ttc.column_name, '_', ' '))
) AS "columnLabel",
ttc.input_type AS "inputType"
FROM table_type_columns ttc
LEFT JOIN column_labels cl
ON ttc.table_name = cl.table_name
AND ttc.column_name = cl.column_name
LEFT JOIN table_labels tl
ON ttc.table_name = tl.table_name
WHERE ttc.input_type = 'category'
@@ -1806,14 +1800,11 @@ export async function getCategoryColumnsByMenu(
) AS "tableLabel",
ttc.column_name AS "columnName",
COALESCE(
cl.column_label,
ttc.column_label,
initcap(replace(ttc.column_name, '_', ' '))
) AS "columnLabel",
ttc.input_type AS "inputType"
FROM table_type_columns ttc
LEFT JOIN column_labels cl
ON ttc.table_name = cl.table_name
AND ttc.column_name = cl.column_name
LEFT JOIN table_labels tl
ON ttc.table_name = tl.table_name
WHERE ttc.input_type = 'category'
@@ -1836,14 +1827,11 @@ export async function getCategoryColumnsByMenu(
) AS "tableLabel",
ttc.column_name AS "columnName",
COALESCE(
cl.column_label,
ttc.column_label,
initcap(replace(ttc.column_name, '_', ' '))
) AS "columnLabel",
ttc.input_type AS "inputType"
FROM table_type_columns ttc
LEFT JOIN column_labels cl
ON ttc.table_name = cl.table_name
AND ttc.column_name = cl.column_name
LEFT JOIN table_labels tl
ON ttc.table_name = tl.table_name
WHERE ttc.input_type = 'category'
@@ -2228,7 +2216,7 @@ export async function multiTableSave(
/**
* 두 테이블 간 엔티티 관계 조회
* column_labels의 entity/category 타입 설정을 기반으로 두 테이블 간의 관계를 조회
* table_type_columns의 entity/category 타입 설정을 기반으로 두 테이블 간의 관계를 조회
*/
export async function getTableEntityRelations(
req: AuthenticatedRequest,
@@ -2253,11 +2241,12 @@ export async function getTableEntityRelations(
table_name,
column_name,
column_label,
web_type,
input_type as web_type,
detail_settings
FROM column_labels
FROM table_type_columns
WHERE table_name IN ($1, $2)
AND web_type IN ('entity', 'category')
AND input_type IN ('entity', 'category')
AND company_code = '*'
`;
const result = await query(columnLabelsQuery, [leftTable, rightTable]);
@@ -2332,7 +2321,7 @@ export async function getTableEntityRelations(
* 현재 테이블을 참조(FK로 연결)하는 테이블 목록 조회
* GET /api/table-management/columns/:tableName/referenced-by
*
* column_labels에서 reference_table이 현재 테이블인 레코드를 찾아서
* table_type_columns에서 reference_table이 현재 테이블인 레코드를 찾아서
* 해당 테이블과 FK 컬럼 정보를 반환합니다.
*/
export async function getReferencedByTables(
@@ -2359,21 +2348,22 @@ export async function getReferencedByTables(
return;
}
// column_labels에서 reference_table이 현재 테이블인 레코드 조회
// table_type_columns에서 reference_table이 현재 테이블인 레코드 조회
// input_type이 'entity'인 것만 조회 (실제 FK 관계)
const sqlQuery = `
SELECT DISTINCT
cl.table_name,
cl.column_name,
cl.column_label,
cl.reference_table,
cl.reference_column,
cl.display_column,
cl.table_name as table_label
FROM column_labels cl
WHERE cl.reference_table = $1
AND cl.input_type = 'entity'
ORDER BY cl.table_name, cl.column_name
ttc.table_name,
ttc.column_name,
ttc.column_label,
ttc.reference_table,
ttc.reference_column,
ttc.display_column,
ttc.table_name as table_label
FROM table_type_columns ttc
WHERE ttc.reference_table = $1
AND ttc.input_type = 'entity'
AND ttc.company_code = '*'
ORDER BY ttc.table_name, ttc.column_name
`;
const result = await query(sqlQuery, [tableName]);