fix: 화면 복제 기능 개선 및 관련 버그 수정
- 화면 복제 기능을 개선하여 DB 구조 개편 후의 효율적인 화면 관리를 지원합니다. - 그룹 복제 시 버튼의 `targetScreenId`가 새 화면으로 매핑되지 않는 버그를 수정하였습니다. - 관련된 서비스 및 쿼리에서 `table_type_columns`를 사용하여 라벨 정보를 조회하도록 변경하였습니다. - 여러 컨트롤러 및 서비스에서 `column_labels` 대신 `table_type_columns`를 참조하도록 업데이트하였습니다.
This commit is contained in:
@@ -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]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 설정을 확인해주세요.`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user