feat: 카테고리 컬럼 메뉴별 매핑 기능 구현
- category_column_mapping 테이블 생성 (마이그레이션 054) - 테이블 타입 관리에서 2레벨 메뉴 선택 기능 추가 - 카테고리 컬럼 조회 시 현재 메뉴 및 상위 메뉴 매핑 자동 조회 - 캐시 무효화 로직 개선 - 메뉴별 카테고리 설정 저장 및 불러오기 기능 구현
This commit is contained in:
parent
9dc8a51f4c
commit
36bff64145
|
|
@ -471,3 +471,33 @@ export const deleteColumnMapping = async (req: AuthenticatedRequest, res: Respon
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 2레벨 메뉴 목록 조회
|
||||
*
|
||||
* GET /api/categories/second-level-menus
|
||||
*
|
||||
* 카테고리 컬럼 매핑 생성 시 메뉴 선택용
|
||||
* 2레벨 메뉴를 선택하면 해당 메뉴의 모든 하위 메뉴에서 사용 가능
|
||||
*/
|
||||
export const getSecondLevelMenus = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
|
||||
logger.info("2레벨 메뉴 목록 조회", { companyCode });
|
||||
|
||||
const menus = await tableCategoryValueService.getSecondLevelMenus(companyCode);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: menus,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error(`2레벨 메뉴 목록 조회 실패: ${error.message}`);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "2레벨 메뉴 목록 조회 중 오류가 발생했습니다",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1657,37 +1657,108 @@ export async function getCategoryColumnsByMenu(
|
|||
return;
|
||||
}
|
||||
|
||||
// 3. 테이블들의 카테고리 타입 컬럼 조회 (테이블 라벨 포함)
|
||||
logger.info("🔍 카테고리 컬럼 쿼리 준비", { tableNames, companyCode });
|
||||
|
||||
const columnsQuery = `
|
||||
SELECT
|
||||
ttc.table_name AS "tableName",
|
||||
COALESCE(
|
||||
tl.table_label,
|
||||
initcap(replace(ttc.table_name, '_', ' '))
|
||||
) AS "tableLabel",
|
||||
ttc.column_name AS "columnName",
|
||||
COALESCE(
|
||||
cl.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.table_name = ANY($1)
|
||||
AND ttc.company_code = $2
|
||||
AND ttc.input_type = 'category'
|
||||
ORDER BY ttc.table_name, ttc.column_name
|
||||
`;
|
||||
|
||||
logger.info("🔍 카테고리 컬럼 쿼리 실행 중...");
|
||||
const columnsResult = await pool.query(columnsQuery, [tableNames, companyCode]);
|
||||
logger.info("✅ 카테고리 컬럼 쿼리 완료", { rowCount: columnsResult.rows.length });
|
||||
// 3. category_column_mapping 테이블 존재 여부 확인
|
||||
const tableExistsResult = await pool.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'category_column_mapping'
|
||||
) as table_exists
|
||||
`);
|
||||
const mappingTableExists = tableExistsResult.rows[0]?.table_exists === true;
|
||||
|
||||
let columnsResult;
|
||||
|
||||
if (mappingTableExists) {
|
||||
// 🆕 category_column_mapping을 사용한 필터링
|
||||
logger.info("🔍 category_column_mapping 기반 카테고리 컬럼 조회", { menuObjid, companyCode });
|
||||
|
||||
// 현재 메뉴와 모든 상위 메뉴의 objid 조회 (재귀)
|
||||
const ancestorMenuQuery = `
|
||||
WITH RECURSIVE menu_hierarchy AS (
|
||||
-- 현재 메뉴
|
||||
SELECT objid, parent_obj_id, menu_type
|
||||
FROM menu_info
|
||||
WHERE objid = $1
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- 부모 메뉴 재귀 조회
|
||||
SELECT m.objid, m.parent_obj_id, m.menu_type
|
||||
FROM menu_info m
|
||||
INNER JOIN menu_hierarchy mh ON m.objid = mh.parent_obj_id
|
||||
WHERE m.parent_obj_id != 0 -- 최상위 메뉴(parent_obj_id=0) 제외
|
||||
)
|
||||
SELECT ARRAY_AGG(objid) as menu_objids
|
||||
FROM menu_hierarchy
|
||||
`;
|
||||
|
||||
const ancestorMenuResult = await pool.query(ancestorMenuQuery, [parseInt(menuObjid)]);
|
||||
const ancestorMenuObjids = ancestorMenuResult.rows[0]?.menu_objids || [parseInt(menuObjid)];
|
||||
|
||||
|
||||
const columnsQuery = `
|
||||
SELECT DISTINCT
|
||||
ttc.table_name AS "tableName",
|
||||
COALESCE(
|
||||
tl.table_label,
|
||||
initcap(replace(ttc.table_name, '_', ' '))
|
||||
) AS "tableLabel",
|
||||
ccm.logical_column_name AS "columnName",
|
||||
COALESCE(
|
||||
cl.column_label,
|
||||
initcap(replace(ccm.logical_column_name, '_', ' '))
|
||||
) AS "columnLabel",
|
||||
ttc.input_type AS "inputType"
|
||||
FROM category_column_mapping ccm
|
||||
INNER JOIN table_type_columns ttc
|
||||
ON ccm.table_name = ttc.table_name
|
||||
AND ccm.physical_column_name = ttc.column_name
|
||||
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 ccm.table_name = ANY($1)
|
||||
AND ccm.company_code = $2
|
||||
AND ccm.menu_objid = ANY($3)
|
||||
AND ttc.input_type = 'category'
|
||||
ORDER BY ttc.table_name, ccm.logical_column_name
|
||||
`;
|
||||
|
||||
columnsResult = await pool.query(columnsQuery, [tableNames, companyCode, ancestorMenuObjids]);
|
||||
logger.info("✅ category_column_mapping 기반 조회 완료", { rowCount: columnsResult.rows.length });
|
||||
} else {
|
||||
// 🔄 기존 방식: table_type_columns에서 모든 카테고리 컬럼 조회
|
||||
logger.info("🔍 레거시 방식: table_type_columns 기반 카테고리 컬럼 조회", { tableNames, companyCode });
|
||||
|
||||
const columnsQuery = `
|
||||
SELECT
|
||||
ttc.table_name AS "tableName",
|
||||
COALESCE(
|
||||
tl.table_label,
|
||||
initcap(replace(ttc.table_name, '_', ' '))
|
||||
) AS "tableLabel",
|
||||
ttc.column_name AS "columnName",
|
||||
COALESCE(
|
||||
cl.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.table_name = ANY($1)
|
||||
AND ttc.company_code = $2
|
||||
AND ttc.input_type = 'category'
|
||||
ORDER BY ttc.table_name, ttc.column_name
|
||||
`;
|
||||
|
||||
columnsResult = await pool.query(columnsQuery, [tableNames, companyCode]);
|
||||
logger.info("✅ 레거시 방식 조회 완료", { rowCount: columnsResult.rows.length });
|
||||
}
|
||||
|
||||
logger.info("✅ 카테고리 컬럼 조회 완료", {
|
||||
columnCount: columnsResult.rows.length
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
createColumnMapping,
|
||||
getLogicalColumns,
|
||||
deleteColumnMapping,
|
||||
getSecondLevelMenus,
|
||||
} from "../controllers/tableCategoryValueController";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
|
||||
|
|
@ -44,6 +45,9 @@ router.post("/values/reorder", reorderCategoryValues);
|
|||
// 컬럼 매핑 관련 라우트 (논리명 ↔ 물리명)
|
||||
// ================================================
|
||||
|
||||
// 2레벨 메뉴 목록 조회 (메뉴 선택용)
|
||||
router.get("/second-level-menus", getSecondLevelMenus);
|
||||
|
||||
// 컬럼 매핑 조회
|
||||
router.get("/column-mapping/:tableName/:menuObjid", getColumnMapping);
|
||||
|
||||
|
|
|
|||
|
|
@ -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}개 컬럼`
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { apiClient } from "@/lib/api/client";
|
|||
import { commonCodeApi } from "@/lib/api/commonCode";
|
||||
import { entityJoinApi, ReferenceTableColumn } from "@/lib/api/entityJoin";
|
||||
import { ddlApi } from "@/lib/api/ddl";
|
||||
import { getSecondLevelMenus, createColumnMapping } from "@/lib/api/tableCategoryValue";
|
||||
import { CreateTableModal } from "@/components/admin/CreateTableModal";
|
||||
import { AddColumnModal } from "@/components/admin/AddColumnModal";
|
||||
import { DDLLogViewer } from "@/components/admin/DDLLogViewer";
|
||||
|
|
@ -54,6 +55,14 @@ interface ColumnTypeInfo {
|
|||
referenceTable?: string;
|
||||
referenceColumn?: string;
|
||||
displayColumn?: string; // 🎯 Entity 조인에서 표시할 컬럼명
|
||||
categoryMenus?: number[]; // 🆕 Category 타입: 선택된 2레벨 메뉴 OBJID 배열
|
||||
}
|
||||
|
||||
interface SecondLevelMenu {
|
||||
menuObjid: number;
|
||||
menuName: string;
|
||||
parentMenuName: string;
|
||||
screenCode?: string;
|
||||
}
|
||||
|
||||
export default function TableManagementPage() {
|
||||
|
|
@ -89,6 +98,9 @@ export default function TableManagementPage() {
|
|||
const [duplicateModalMode, setDuplicateModalMode] = useState<"create" | "duplicate">("create");
|
||||
const [duplicateSourceTable, setDuplicateSourceTable] = useState<string | null>(null);
|
||||
|
||||
// 🆕 Category 타입용: 2레벨 메뉴 목록
|
||||
const [secondLevelMenus, setSecondLevelMenus] = useState<SecondLevelMenu[]>([]);
|
||||
|
||||
// 로그 뷰어 상태
|
||||
const [logViewerOpen, setLogViewerOpen] = useState(false);
|
||||
const [logViewerTableName, setLogViewerTableName] = useState<string>("");
|
||||
|
|
@ -224,6 +236,22 @@ export default function TableManagementPage() {
|
|||
}
|
||||
};
|
||||
|
||||
// 🆕 2레벨 메뉴 목록 로드
|
||||
const loadSecondLevelMenus = async () => {
|
||||
try {
|
||||
const response = await getSecondLevelMenus();
|
||||
if (response.success && response.data) {
|
||||
setSecondLevelMenus(response.data);
|
||||
} else {
|
||||
console.warn("⚠️ 2레벨 메뉴 로드 실패:", response);
|
||||
setSecondLevelMenus([]); // 빈 배열로 설정하여 로딩 상태 해제
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 2레벨 메뉴 로드 에러:", error);
|
||||
setSecondLevelMenus([]); // 에러 발생 시에도 빈 배열로 설정
|
||||
}
|
||||
};
|
||||
|
||||
// 테이블 목록 로드
|
||||
const loadTables = async () => {
|
||||
setLoading(true);
|
||||
|
|
@ -257,10 +285,17 @@ export default function TableManagementPage() {
|
|||
if (response.data.success) {
|
||||
const data = response.data.data;
|
||||
|
||||
console.log("📥 원본 API 응답:", {
|
||||
hasColumns: !!(data.columns || data),
|
||||
firstColumn: (data.columns || data)[0],
|
||||
statusColumn: (data.columns || data).find((col: any) => col.columnName === "status"),
|
||||
});
|
||||
|
||||
// 컬럼 데이터에 기본값 설정
|
||||
const processedColumns = (data.columns || data).map((col: any) => ({
|
||||
...col,
|
||||
inputType: col.inputType || "text", // 기본값: text
|
||||
categoryMenus: col.categoryMenus || [], // 카테고리 메뉴 매핑 정보
|
||||
}));
|
||||
|
||||
if (page === 1) {
|
||||
|
|
@ -438,12 +473,72 @@ export default function TableManagementPage() {
|
|||
|
||||
// console.log("저장할 컬럼 설정:", columnSetting);
|
||||
|
||||
console.log("💾 저장할 컬럼 정보:", {
|
||||
columnName: column.columnName,
|
||||
inputType: column.inputType,
|
||||
categoryMenus: column.categoryMenus,
|
||||
hasCategoryMenus: !!column.categoryMenus,
|
||||
categoryMenusLength: column.categoryMenus?.length || 0,
|
||||
});
|
||||
|
||||
const response = await apiClient.post(`/table-management/tables/${selectedTable}/columns/settings`, [
|
||||
columnSetting,
|
||||
]);
|
||||
|
||||
if (response.data.success) {
|
||||
toast.success("컬럼 설정이 성공적으로 저장되었습니다.");
|
||||
console.log("✅ 컬럼 설정 저장 성공");
|
||||
|
||||
// 🆕 Category 타입인 경우 컬럼 매핑 생성
|
||||
console.log("🔍 카테고리 조건 체크:", {
|
||||
isCategory: column.inputType === "category",
|
||||
hasCategoryMenus: !!column.categoryMenus,
|
||||
length: column.categoryMenus?.length || 0,
|
||||
});
|
||||
|
||||
if (column.inputType === "category" && column.categoryMenus && column.categoryMenus.length > 0) {
|
||||
console.log("📥 카테고리 메뉴 매핑 시작:", {
|
||||
columnName: column.columnName,
|
||||
categoryMenus: column.categoryMenus,
|
||||
count: column.categoryMenus.length,
|
||||
});
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const menuObjid of column.categoryMenus) {
|
||||
try {
|
||||
const mappingResponse = await createColumnMapping({
|
||||
tableName: selectedTable,
|
||||
logicalColumnName: column.columnName,
|
||||
physicalColumnName: column.columnName,
|
||||
menuObjid,
|
||||
description: `${column.displayName} (메뉴별 카테고리)`,
|
||||
});
|
||||
|
||||
if (mappingResponse.success) {
|
||||
successCount++;
|
||||
} else {
|
||||
console.error("❌ 매핑 생성 실패:", mappingResponse);
|
||||
failCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ 메뉴 ${menuObjid}에 대한 매핑 생성 실패:`, error);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (successCount > 0 && failCount === 0) {
|
||||
toast.success(`컬럼 설정 및 ${successCount}개 메뉴 매핑이 저장되었습니다.`);
|
||||
} else if (successCount > 0 && failCount > 0) {
|
||||
toast.warning(`컬럼 설정 저장 성공. ${successCount}개 메뉴 매핑 성공, ${failCount}개 실패.`);
|
||||
} else if (failCount > 0) {
|
||||
toast.error(`컬럼 설정 저장 성공. 메뉴 매핑 생성 실패.`);
|
||||
}
|
||||
} else {
|
||||
toast.success("컬럼 설정이 성공적으로 저장되었습니다.");
|
||||
}
|
||||
|
||||
// 원본 데이터 업데이트
|
||||
setOriginalColumns((prev) => prev.map((col) => (col.columnName === column.columnName ? column : col)));
|
||||
|
||||
|
|
@ -501,14 +596,78 @@ export default function TableManagementPage() {
|
|||
);
|
||||
|
||||
if (response.data.success) {
|
||||
// 🆕 Category 타입 컬럼들의 메뉴 매핑 생성
|
||||
const categoryColumns = columns.filter(
|
||||
(col) => col.inputType === "category" && col.categoryMenus && col.categoryMenus.length > 0
|
||||
);
|
||||
|
||||
console.log("📥 전체 저장: 카테고리 컬럼 확인", {
|
||||
totalColumns: columns.length,
|
||||
categoryColumns: categoryColumns.length,
|
||||
categoryColumnsData: categoryColumns.map((col) => ({
|
||||
columnName: col.columnName,
|
||||
categoryMenus: col.categoryMenus,
|
||||
})),
|
||||
});
|
||||
|
||||
if (categoryColumns.length > 0) {
|
||||
let totalSuccessCount = 0;
|
||||
let totalFailCount = 0;
|
||||
|
||||
for (const column of categoryColumns) {
|
||||
for (const menuObjid of column.categoryMenus!) {
|
||||
try {
|
||||
console.log("🔄 매핑 API 호출:", {
|
||||
tableName: selectedTable,
|
||||
columnName: column.columnName,
|
||||
menuObjid,
|
||||
});
|
||||
|
||||
const mappingResponse = await createColumnMapping({
|
||||
tableName: selectedTable,
|
||||
logicalColumnName: column.columnName,
|
||||
physicalColumnName: column.columnName,
|
||||
menuObjid,
|
||||
description: `${column.displayName} (메뉴별 카테고리)`,
|
||||
});
|
||||
|
||||
console.log("✅ 매핑 API 응답:", mappingResponse);
|
||||
|
||||
if (mappingResponse.success) {
|
||||
totalSuccessCount++;
|
||||
} else {
|
||||
console.error("❌ 매핑 생성 실패:", mappingResponse);
|
||||
totalFailCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ 메뉴 ${menuObjid}에 대한 매핑 생성 실패:`, error);
|
||||
totalFailCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("📊 전체 매핑 결과:", { totalSuccessCount, totalFailCount });
|
||||
|
||||
if (totalSuccessCount > 0) {
|
||||
toast.success(
|
||||
`테이블 설정 및 ${totalSuccessCount}개 카테고리 메뉴 매핑이 저장되었습니다.`
|
||||
);
|
||||
} else if (totalFailCount > 0) {
|
||||
toast.warning(`테이블 설정은 저장되었으나 ${totalFailCount}개 메뉴 매핑 생성 실패.`);
|
||||
} else {
|
||||
toast.success(`테이블 '${selectedTable}' 설정이 모두 저장되었습니다.`);
|
||||
}
|
||||
} else {
|
||||
toast.success(`테이블 '${selectedTable}' 설정이 모두 저장되었습니다.`);
|
||||
}
|
||||
|
||||
// 저장 성공 후 원본 데이터 업데이트
|
||||
setOriginalColumns([...columns]);
|
||||
toast.success(`테이블 '${selectedTable}' 설정이 모두 저장되었습니다.`);
|
||||
|
||||
// 테이블 목록 새로고침 (라벨 변경 반영)
|
||||
loadTables();
|
||||
|
||||
// 저장 후 데이터 확인을 위해 다시 로드
|
||||
// 저장 후 데이터 다시 로드
|
||||
setTimeout(() => {
|
||||
loadColumnTypes(selectedTable, 1, pageSize);
|
||||
}, 1000);
|
||||
|
|
@ -539,6 +698,7 @@ export default function TableManagementPage() {
|
|||
useEffect(() => {
|
||||
loadTables();
|
||||
loadCommonCodeCategories();
|
||||
loadSecondLevelMenus();
|
||||
}, []);
|
||||
|
||||
// 🎯 컬럼 로드 후 이미 설정된 참조 테이블들의 컬럼 정보 로드
|
||||
|
|
@ -1023,10 +1183,61 @@ export default function TableManagementPage() {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
{/* 입력 타입이 'category'인 경우 안내 메시지 */}
|
||||
{/* 입력 타입이 'category'인 경우 2레벨 메뉴 다중 선택 */}
|
||||
{column.inputType === "category" && (
|
||||
<div className="flex items-center h-8 text-xs text-muted-foreground">
|
||||
메뉴별 카테고리 값이 자동으로 표시됩니다
|
||||
<div className="space-y-2">
|
||||
<label className="text-muted-foreground mb-1 block text-xs">
|
||||
적용할 메뉴 (2레벨)
|
||||
</label>
|
||||
<div className="border rounded-lg p-3 space-y-2 max-h-48 overflow-y-auto">
|
||||
{secondLevelMenus.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
2레벨 메뉴가 없습니다. 메뉴를 선택하지 않으면 모든 메뉴에서 사용 가능합니다.
|
||||
</p>
|
||||
) : (
|
||||
secondLevelMenus.map((menu) => {
|
||||
// menuObjid를 숫자로 변환하여 비교
|
||||
const menuObjidNum = Number(menu.menuObjid);
|
||||
const isChecked = (column.categoryMenus || []).includes(menuObjidNum);
|
||||
|
||||
return (
|
||||
<div key={menu.menuObjid} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`category-menu-${column.columnName}-${menu.menuObjid}`}
|
||||
checked={isChecked}
|
||||
onChange={(e) => {
|
||||
const currentMenus = column.categoryMenus || [];
|
||||
const newMenus = e.target.checked
|
||||
? [...currentMenus, menuObjidNum]
|
||||
: currentMenus.filter((id) => id !== menuObjidNum);
|
||||
|
||||
setColumns((prev) =>
|
||||
prev.map((col) =>
|
||||
col.columnName === column.columnName
|
||||
? { ...col, categoryMenus: newMenus }
|
||||
: col
|
||||
)
|
||||
);
|
||||
}}
|
||||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`category-menu-${column.columnName}-${menu.menuObjid}`}
|
||||
className="text-xs cursor-pointer flex-1"
|
||||
>
|
||||
{menu.parentMenuName} → {menu.menuName}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
{column.categoryMenus && column.categoryMenus.length > 0 && (
|
||||
<p className="text-primary text-xs">
|
||||
{column.categoryMenus.length}개 메뉴 선택됨
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 입력 타입이 'entity'인 경우 참조 테이블 선택 */}
|
||||
|
|
|
|||
|
|
@ -21,15 +21,22 @@ import {
|
|||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Plus } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { createColumnMapping } from "@/lib/api/tableCategoryValue";
|
||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
|
||||
interface SecondLevelMenu {
|
||||
menuObjid: number;
|
||||
menuName: string;
|
||||
parentMenuName: string;
|
||||
screenCode?: string;
|
||||
}
|
||||
|
||||
interface AddCategoryColumnDialogProps {
|
||||
tableName: string;
|
||||
menuObjid: number;
|
||||
menuName: string;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -37,27 +44,31 @@ interface AddCategoryColumnDialogProps {
|
|||
* 카테고리 컬럼 추가 다이얼로그
|
||||
*
|
||||
* 논리적 컬럼명과 물리적 컬럼명을 매핑하여 메뉴별로 독립적인 카테고리 관리 가능
|
||||
*
|
||||
* 2레벨 메뉴를 선택하면 해당 메뉴의 모든 하위 메뉴에서 사용 가능
|
||||
*/
|
||||
export function AddCategoryColumnDialog({
|
||||
tableName,
|
||||
menuObjid,
|
||||
menuName,
|
||||
onSuccess,
|
||||
}: AddCategoryColumnDialogProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [physicalColumns, setPhysicalColumns] = useState<string[]>([]);
|
||||
const [secondLevelMenus, setSecondLevelMenus] = useState<SecondLevelMenu[]>([]);
|
||||
const [selectedMenus, setSelectedMenus] = useState<number[]>([]);
|
||||
const [logicalColumnName, setLogicalColumnName] = useState("");
|
||||
const [physicalColumnName, setPhysicalColumnName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
|
||||
// 테이블의 실제 컬럼 목록 조회
|
||||
// 다이얼로그 열릴 때 데이터 로드
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
loadPhysicalColumns();
|
||||
loadSecondLevelMenus();
|
||||
}
|
||||
}, [open, tableName]);
|
||||
|
||||
// 테이블의 실제 컬럼 목록 조회
|
||||
const loadPhysicalColumns = async () => {
|
||||
try {
|
||||
const response = await tableManagementApi.getTableColumns(tableName);
|
||||
|
|
@ -70,6 +81,32 @@ export function AddCategoryColumnDialog({
|
|||
}
|
||||
};
|
||||
|
||||
// 2레벨 메뉴 목록 조회
|
||||
const loadSecondLevelMenus = async () => {
|
||||
try {
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
data: SecondLevelMenu[];
|
||||
}>("table-categories/second-level-menus");
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
setSecondLevelMenus(response.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("2레벨 메뉴 목록 조회 실패:", error);
|
||||
toast.error("메뉴 목록을 불러올 수 없습니다");
|
||||
}
|
||||
};
|
||||
|
||||
// 메뉴 선택/해제
|
||||
const toggleMenu = (menuObjid: number) => {
|
||||
setSelectedMenus((prev) =>
|
||||
prev.includes(menuObjid)
|
||||
? prev.filter((id) => id !== menuObjid)
|
||||
: [...prev, menuObjid]
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
// 입력 검증
|
||||
if (!logicalColumnName.trim()) {
|
||||
|
|
@ -82,24 +119,42 @@ export function AddCategoryColumnDialog({
|
|||
return;
|
||||
}
|
||||
|
||||
if (selectedMenus.length === 0) {
|
||||
toast.error("최소 하나 이상의 메뉴를 선택해주세요");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await createColumnMapping({
|
||||
tableName,
|
||||
logicalColumnName: logicalColumnName.trim(),
|
||||
physicalColumnName,
|
||||
menuObjid,
|
||||
description: description.trim() || undefined,
|
||||
});
|
||||
// 선택된 각 메뉴에 대해 매핑 생성
|
||||
const promises = selectedMenus.map((menuObjid) =>
|
||||
createColumnMapping({
|
||||
tableName,
|
||||
logicalColumnName: logicalColumnName.trim(),
|
||||
physicalColumnName,
|
||||
menuObjid,
|
||||
description: description.trim() || undefined,
|
||||
})
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
toast.success("논리적 컬럼이 추가되었습니다");
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// 모든 요청이 성공했는지 확인
|
||||
const failedCount = results.filter((r) => !r.success).length;
|
||||
|
||||
if (failedCount === 0) {
|
||||
toast.success(`논리적 컬럼이 ${selectedMenus.length}개 메뉴에 추가되었습니다`);
|
||||
setOpen(false);
|
||||
resetForm();
|
||||
onSuccess();
|
||||
} else if (failedCount < results.length) {
|
||||
toast.warning(
|
||||
`${results.length - failedCount}개 메뉴에 추가 성공, ${failedCount}개 실패`
|
||||
);
|
||||
onSuccess();
|
||||
} else {
|
||||
toast.error(response.error || "컬럼 매핑 생성에 실패했습니다");
|
||||
toast.error("모든 메뉴에 대한 매핑 생성에 실패했습니다");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("컬럼 매핑 생성 실패:", error);
|
||||
|
|
@ -113,6 +168,7 @@ export function AddCategoryColumnDialog({
|
|||
setLogicalColumnName("");
|
||||
setPhysicalColumnName("");
|
||||
setDescription("");
|
||||
setSelectedMenus([]);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -130,21 +186,11 @@ export function AddCategoryColumnDialog({
|
|||
카테고리 컬럼 추가
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
같은 물리적 컬럼을 여러 메뉴에서 다른 카테고리로 사용할 수 있습니다
|
||||
2레벨 메뉴를 선택하면 해당 메뉴의 모든 하위 메뉴에서 사용할 수 있습니다
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
{/* 적용 메뉴 (읽기 전용) */}
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">적용 메뉴</Label>
|
||||
<Input
|
||||
value={menuName}
|
||||
disabled
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 실제 컬럼 선택 */}
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">
|
||||
|
|
@ -179,10 +225,47 @@ export function AddCategoryColumnDialog({
|
|||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||
이 메뉴에서만 사용할 고유한 이름을 입력하세요
|
||||
선택한 메뉴들에서 사용할 고유한 이름을 입력하세요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 적용할 2레벨 메뉴 선택 (체크박스) */}
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">
|
||||
적용할 메뉴 선택 (2레벨) *
|
||||
</Label>
|
||||
<div className="border rounded-lg p-3 sm:p-4 space-y-2 max-h-48 overflow-y-auto mt-2">
|
||||
{secondLevelMenus.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">로딩 중...</p>
|
||||
) : (
|
||||
secondLevelMenus.map((menu) => (
|
||||
<div key={menu.menuObjid} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`menu-${menu.menuObjid}`}
|
||||
checked={selectedMenus.includes(menu.menuObjid)}
|
||||
onCheckedChange={() => toggleMenu(menu.menuObjid)}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`menu-${menu.menuObjid}`}
|
||||
className="text-xs sm:text-sm cursor-pointer flex-1"
|
||||
>
|
||||
{menu.parentMenuName} → {menu.menuName}
|
||||
</label>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||
선택한 메뉴의 모든 하위 메뉴에서 이 카테고리를 사용할 수 있습니다
|
||||
</p>
|
||||
{selectedMenus.length > 0 && (
|
||||
<p className="text-primary mt-1 text-[10px] sm:text-xs">
|
||||
{selectedMenus.length}개 메뉴 선택됨
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 설명 (선택사항) */}
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">설명</Label>
|
||||
|
|
@ -207,7 +290,7 @@ export function AddCategoryColumnDialog({
|
|||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!logicalColumnName || !physicalColumnName || loading}
|
||||
disabled={!logicalColumnName || !physicalColumnName || selectedMenus.length === 0 || loading}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
>
|
||||
{loading ? "추가 중..." : "추가"}
|
||||
|
|
|
|||
|
|
@ -237,3 +237,28 @@ export async function deleteColumnMapping(mappingId: number) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 2레벨 메뉴 목록 조회
|
||||
*
|
||||
* 카테고리 컬럼 매핑 생성 시 메뉴 선택용
|
||||
*
|
||||
* @returns 2레벨 메뉴 목록
|
||||
*/
|
||||
export async function getSecondLevelMenus() {
|
||||
try {
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
data: Array<{
|
||||
menuObjid: number;
|
||||
menuName: string;
|
||||
parentMenuName: string;
|
||||
screenCode?: string;
|
||||
}>;
|
||||
}>("/table-categories/second-level-menus");
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error("2레벨 메뉴 목록 조회 실패:", error);
|
||||
return { success: false, error: error.message, data: [] };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue