feat: 카테고리 컬럼 메뉴별 매핑 기능 구현
- category_column_mapping 테이블 생성 (마이그레이션 054) - 테이블 타입 관리에서 2레벨 메뉴 선택 기능 추가 - 카테고리 컬럼 조회 시 현재 메뉴 및 상위 메뉴 매핑 자동 조회 - 캐시 무효화 로직 개선 - 메뉴별 카테고리 설정 저장 및 불러오기 기능 구현
This commit is contained in:
@@ -1514,6 +1514,7 @@ export class ScreenManagementService {
|
||||
throw new Error("이미 할당된 화면입니다.");
|
||||
}
|
||||
|
||||
// screen_menu_assignments에 할당 추가
|
||||
await query(
|
||||
`INSERT INTO screen_menu_assignments (
|
||||
screen_id, menu_objid, company_code, display_order, created_by
|
||||
@@ -1526,6 +1527,40 @@ export class ScreenManagementService {
|
||||
assignmentData.createdBy || null,
|
||||
]
|
||||
);
|
||||
|
||||
// 화면 정보 조회 (screen_code 가져오기)
|
||||
const screen = await queryOne<{ screen_code: string }>(
|
||||
`SELECT screen_code FROM screen_definitions WHERE screen_id = $1`,
|
||||
[screenId]
|
||||
);
|
||||
|
||||
if (screen) {
|
||||
// menu_info 테이블도 함께 업데이트 (menu_url과 screen_code 설정)
|
||||
// 관리자 메뉴인지 확인
|
||||
const menu = await queryOne<{ menu_type: string }>(
|
||||
`SELECT menu_type FROM menu_info WHERE objid = $1`,
|
||||
[assignmentData.menuObjid]
|
||||
);
|
||||
|
||||
const isAdminMenu = menu && (menu.menu_type === "0" || menu.menu_type === "admin");
|
||||
const menuUrl = isAdminMenu
|
||||
? `/screens/${screenId}?mode=admin`
|
||||
: `/screens/${screenId}`;
|
||||
|
||||
await query(
|
||||
`UPDATE menu_info
|
||||
SET menu_url = $1, screen_code = $2
|
||||
WHERE objid = $3`,
|
||||
[menuUrl, screen.screen_code, assignmentData.menuObjid]
|
||||
);
|
||||
|
||||
logger.info("화면 할당 완료 (menu_info 업데이트)", {
|
||||
screenId,
|
||||
menuObjid: assignmentData.menuObjid,
|
||||
menuUrl,
|
||||
screenCode: screen.screen_code,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1589,11 +1624,26 @@ export class ScreenManagementService {
|
||||
menuObjid: number,
|
||||
companyCode: string
|
||||
): Promise<void> {
|
||||
// screen_menu_assignments에서 할당 삭제
|
||||
await query(
|
||||
`DELETE FROM screen_menu_assignments
|
||||
WHERE screen_id = $1 AND menu_objid = $2 AND company_code = $3`,
|
||||
[screenId, menuObjid, companyCode]
|
||||
);
|
||||
|
||||
// menu_info 테이블도 함께 업데이트 (menu_url과 screen_code 제거)
|
||||
await query(
|
||||
`UPDATE menu_info
|
||||
SET menu_url = NULL, screen_code = NULL
|
||||
WHERE objid = $1`,
|
||||
[menuObjid]
|
||||
);
|
||||
|
||||
logger.info("화면 할당 해제 완료 (menu_info 업데이트)", {
|
||||
screenId,
|
||||
menuObjid,
|
||||
companyCode,
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
|
||||
@@ -973,6 +973,96 @@ class TableCategoryValueService {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 2레벨 메뉴 목록 조회
|
||||
*
|
||||
* 카테고리 컬럼 매핑 생성 시 메뉴 선택용
|
||||
*
|
||||
* @param companyCode - 회사 코드
|
||||
* @returns 2레벨 메뉴 목록
|
||||
*/
|
||||
async getSecondLevelMenus(companyCode: string): Promise<any[]> {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
logger.info("2레벨 메뉴 목록 조회", { companyCode });
|
||||
|
||||
// menu_info 테이블에 company_code 컬럼이 있는지 확인
|
||||
const columnCheckQuery = `
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'menu_info' AND column_name = 'company_code'
|
||||
`;
|
||||
const columnCheck = await pool.query(columnCheckQuery);
|
||||
const hasCompanyCode = columnCheck.rows.length > 0;
|
||||
|
||||
logger.info("menu_info 테이블 company_code 컬럼 존재 여부", { hasCompanyCode });
|
||||
|
||||
let query: string;
|
||||
let params: any[];
|
||||
|
||||
if (!hasCompanyCode) {
|
||||
// company_code 컬럼이 없는 경우: 모든 2레벨 사용자 메뉴 조회
|
||||
query = `
|
||||
SELECT
|
||||
m1.objid as "menuObjid",
|
||||
m1.menu_name_kor as "menuName",
|
||||
m0.menu_name_kor as "parentMenuName",
|
||||
m1.screen_code as "screenCode"
|
||||
FROM menu_info m1
|
||||
INNER JOIN menu_info m0 ON m1.parent_obj_id = m0.objid
|
||||
WHERE m1.menu_type = 1
|
||||
AND m1.status = 'active'
|
||||
AND m0.parent_obj_id = 0
|
||||
ORDER BY m0.seq, m1.seq
|
||||
`;
|
||||
params = [];
|
||||
} else if (companyCode === "*") {
|
||||
// 최고 관리자: 모든 회사의 2레벨 사용자 메뉴 조회
|
||||
query = `
|
||||
SELECT
|
||||
m1.objid as "menuObjid",
|
||||
m1.menu_name_kor as "menuName",
|
||||
m0.menu_name_kor as "parentMenuName",
|
||||
m1.screen_code as "screenCode"
|
||||
FROM menu_info m1
|
||||
INNER JOIN menu_info m0 ON m1.parent_obj_id = m0.objid
|
||||
WHERE m1.menu_type = 1
|
||||
AND m1.status = 'active'
|
||||
AND m0.parent_obj_id = 0
|
||||
ORDER BY m0.seq, m1.seq
|
||||
`;
|
||||
params = [];
|
||||
} else {
|
||||
// 일반 회사: 자신의 회사 메뉴만 조회 (공통 메뉴 제외)
|
||||
query = `
|
||||
SELECT
|
||||
m1.objid as "menuObjid",
|
||||
m1.menu_name_kor as "menuName",
|
||||
m0.menu_name_kor as "parentMenuName",
|
||||
m1.screen_code as "screenCode"
|
||||
FROM menu_info m1
|
||||
INNER JOIN menu_info m0 ON m1.parent_obj_id = m0.objid
|
||||
WHERE m1.menu_type = 1
|
||||
AND m1.status = 'active'
|
||||
AND m0.parent_obj_id = 0
|
||||
AND m1.company_code = $1
|
||||
ORDER BY m0.seq, m1.seq
|
||||
`;
|
||||
params = [companyCode];
|
||||
}
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
logger.info(`2레벨 메뉴 ${result.rows.length}개 조회 완료`, { companyCode });
|
||||
|
||||
return result.rows;
|
||||
} catch (error: any) {
|
||||
logger.error(`2레벨 메뉴 목록 조회 실패: ${error.message}`, { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new TableCategoryValueService();
|
||||
|
||||
@@ -249,21 +249,78 @@ export class TableManagementService {
|
||||
[tableName, size, offset]
|
||||
);
|
||||
|
||||
// 🆕 category_column_mapping 조회
|
||||
const tableExistsResult = await query<any>(
|
||||
`SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'category_column_mapping'
|
||||
) as table_exists`
|
||||
);
|
||||
const mappingTableExists = tableExistsResult[0]?.table_exists === true;
|
||||
|
||||
let categoryMappings: Map<string, number[]> = new Map();
|
||||
if (mappingTableExists && companyCode) {
|
||||
logger.info("📥 getColumnList: 카테고리 매핑 조회 시작", { tableName, companyCode });
|
||||
|
||||
const mappings = await query<any>(
|
||||
`SELECT
|
||||
logical_column_name as "columnName",
|
||||
menu_objid as "menuObjid"
|
||||
FROM category_column_mapping
|
||||
WHERE table_name = $1
|
||||
AND company_code = $2`,
|
||||
[tableName, companyCode]
|
||||
);
|
||||
|
||||
logger.info("✅ getColumnList: 카테고리 매핑 조회 완료", {
|
||||
tableName,
|
||||
companyCode,
|
||||
mappingCount: mappings.length,
|
||||
mappings: mappings
|
||||
});
|
||||
|
||||
mappings.forEach((m: any) => {
|
||||
if (!categoryMappings.has(m.columnName)) {
|
||||
categoryMappings.set(m.columnName, []);
|
||||
}
|
||||
categoryMappings.get(m.columnName)!.push(Number(m.menuObjid));
|
||||
});
|
||||
|
||||
logger.info("✅ getColumnList: categoryMappings Map 생성 완료", {
|
||||
size: categoryMappings.size,
|
||||
entries: Array.from(categoryMappings.entries())
|
||||
});
|
||||
}
|
||||
|
||||
// BigInt를 Number로 변환하여 JSON 직렬화 문제 해결
|
||||
const columns: ColumnTypeInfo[] = rawColumns.map((column) => ({
|
||||
...column,
|
||||
maxLength: column.maxLength ? Number(column.maxLength) : null,
|
||||
numericPrecision: column.numericPrecision
|
||||
? Number(column.numericPrecision)
|
||||
: null,
|
||||
numericScale: column.numericScale ? Number(column.numericScale) : null,
|
||||
displayOrder: column.displayOrder ? Number(column.displayOrder) : null,
|
||||
// 자동 매핑: webType이 기본값('text')인 경우 DB 타입에 따라 자동 추론
|
||||
webType:
|
||||
column.webType === "text"
|
||||
? this.inferWebType(column.dataType)
|
||||
: column.webType,
|
||||
}));
|
||||
const columns: ColumnTypeInfo[] = rawColumns.map((column) => {
|
||||
const baseColumn = {
|
||||
...column,
|
||||
maxLength: column.maxLength ? Number(column.maxLength) : null,
|
||||
numericPrecision: column.numericPrecision
|
||||
? Number(column.numericPrecision)
|
||||
: null,
|
||||
numericScale: column.numericScale ? Number(column.numericScale) : null,
|
||||
displayOrder: column.displayOrder ? Number(column.displayOrder) : null,
|
||||
// 자동 매핑: webType이 기본값('text')인 경우 DB 타입에 따라 자동 추론
|
||||
webType:
|
||||
column.webType === "text"
|
||||
? this.inferWebType(column.dataType)
|
||||
: column.webType,
|
||||
};
|
||||
|
||||
// 카테고리 타입인 경우 categoryMenus 추가
|
||||
if (column.inputType === "category" && categoryMappings.has(column.columnName)) {
|
||||
const menus = categoryMappings.get(column.columnName);
|
||||
logger.info(`✅ getColumnList: 컬럼 ${column.columnName}에 카테고리 메뉴 추가`, { menus });
|
||||
return {
|
||||
...baseColumn,
|
||||
categoryMenus: menus,
|
||||
};
|
||||
}
|
||||
|
||||
return baseColumn;
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(total / size);
|
||||
|
||||
@@ -429,7 +486,7 @@ export class TableManagementService {
|
||||
// 캐시 무효화 - 해당 테이블의 컬럼 캐시 삭제
|
||||
cache.deleteByPattern(`table_columns:${tableName}:`);
|
||||
cache.delete(CacheKeys.TABLE_COLUMN_COUNT(tableName));
|
||||
|
||||
|
||||
logger.info(`컬럼 설정 업데이트 완료: ${tableName}.${columnName}`);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
@@ -484,7 +541,7 @@ export class TableManagementService {
|
||||
cache.deleteByPattern(`table_columns:${tableName}:`);
|
||||
cache.delete(CacheKeys.TABLE_COLUMN_COUNT(tableName));
|
||||
|
||||
logger.info(`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}, company: ${companyCode}`);
|
||||
logger.info(`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}`);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`전체 컬럼 설정 일괄 업데이트 중 오류 발생: ${tableName}`,
|
||||
@@ -3152,19 +3209,83 @@ export class TableManagementService {
|
||||
[tableName, companyCode]
|
||||
);
|
||||
|
||||
const inputTypes: ColumnTypeInfo[] = rawInputTypes.map((col) => ({
|
||||
tableName: tableName,
|
||||
columnName: col.columnName,
|
||||
displayName: col.displayName,
|
||||
dataType: col.dataType || "varchar",
|
||||
inputType: col.inputType,
|
||||
detailSettings: col.detailSettings,
|
||||
description: "", // 필수 필드 추가
|
||||
isNullable: col.isNullable === "Y" ? "Y" : "N", // 🔥 FIX: string 타입으로 변환
|
||||
isPrimaryKey: false,
|
||||
displayOrder: 0,
|
||||
isVisible: true,
|
||||
}));
|
||||
// category_column_mapping 테이블 존재 여부 확인
|
||||
const tableExistsResult = await query<any>(
|
||||
`SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'category_column_mapping'
|
||||
) as table_exists`
|
||||
);
|
||||
const mappingTableExists = tableExistsResult[0]?.table_exists === true;
|
||||
|
||||
// 카테고리 컬럼의 경우, 매핑된 메뉴 목록 조회
|
||||
let categoryMappings: Map<string, number[]> = new Map();
|
||||
if (mappingTableExists) {
|
||||
logger.info("카테고리 매핑 조회 시작", { tableName, companyCode });
|
||||
|
||||
const mappings = await query<any>(
|
||||
`SELECT
|
||||
logical_column_name as "columnName",
|
||||
menu_objid as "menuObjid"
|
||||
FROM category_column_mapping
|
||||
WHERE table_name = $1
|
||||
AND company_code = $2`,
|
||||
[tableName, companyCode]
|
||||
);
|
||||
|
||||
logger.info("카테고리 매핑 조회 완료", {
|
||||
tableName,
|
||||
companyCode,
|
||||
mappingCount: mappings.length,
|
||||
mappings: mappings
|
||||
});
|
||||
|
||||
mappings.forEach((m: any) => {
|
||||
if (!categoryMappings.has(m.columnName)) {
|
||||
categoryMappings.set(m.columnName, []);
|
||||
}
|
||||
categoryMappings.get(m.columnName)!.push(Number(m.menuObjid));
|
||||
});
|
||||
|
||||
logger.info("categoryMappings Map 생성 완료", {
|
||||
size: categoryMappings.size,
|
||||
entries: Array.from(categoryMappings.entries())
|
||||
});
|
||||
} else {
|
||||
logger.warn("category_column_mapping 테이블이 존재하지 않음");
|
||||
}
|
||||
|
||||
const inputTypes: ColumnTypeInfo[] = rawInputTypes.map((col) => {
|
||||
const baseInfo = {
|
||||
tableName: tableName,
|
||||
columnName: col.columnName,
|
||||
displayName: col.displayName,
|
||||
dataType: col.dataType || "varchar",
|
||||
inputType: col.inputType,
|
||||
detailSettings: col.detailSettings,
|
||||
description: "", // 필수 필드 추가
|
||||
isNullable: col.isNullable === "Y" ? "Y" : "N", // 🔥 FIX: string 타입으로 변환
|
||||
isPrimaryKey: false,
|
||||
displayOrder: 0,
|
||||
isVisible: true,
|
||||
};
|
||||
|
||||
// 카테고리 타입인 경우 categoryMenus 추가
|
||||
if (col.inputType === "category" && categoryMappings.has(col.columnName)) {
|
||||
const menus = categoryMappings.get(col.columnName);
|
||||
logger.info(`✅ 컬럼 ${col.columnName}에 카테고리 메뉴 추가`, { menus });
|
||||
return {
|
||||
...baseInfo,
|
||||
categoryMenus: menus,
|
||||
};
|
||||
}
|
||||
|
||||
if (col.inputType === "category") {
|
||||
logger.warn(`⚠️ 카테고리 컬럼 ${col.columnName}에 매핑 없음`);
|
||||
}
|
||||
|
||||
return baseInfo;
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`컬럼 입력타입 정보 조회 완료: ${tableName}, company: ${companyCode}, ${inputTypes.length}개 컬럼`
|
||||
|
||||
Reference in New Issue
Block a user