Compare commits
5 Commits
93bf83375e
...
a828f54663
| Author | SHA1 | Date |
|---|---|---|
|
|
a828f54663 | |
|
|
f73f788b0a | |
|
|
36bff64145 | |
|
|
9dc8a51f4c | |
|
|
3ddca95af5 |
|
|
@ -1127,12 +1127,16 @@ export async function saveMenu(
|
|||
const objid = Date.now(); // 고유 ID 생성
|
||||
const companyCode = requestCompanyCode || userCompanyCode;
|
||||
|
||||
// menu_url이 비어있으면 screen_code도 null로 설정
|
||||
const menuUrl = menuData.menuUrl || null;
|
||||
const screenCode = menuUrl ? menuData.screenCode || null : null;
|
||||
|
||||
const [savedMenu] = await query<any>(
|
||||
`INSERT INTO menu_info (
|
||||
objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng,
|
||||
seq, menu_url, menu_desc, writer, regdate, status,
|
||||
system_name, company_code, lang_key, lang_key_desc
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
system_name, company_code, lang_key, lang_key_desc, screen_code
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||
RETURNING *`,
|
||||
[
|
||||
objid,
|
||||
|
|
@ -1141,7 +1145,7 @@ export async function saveMenu(
|
|||
menuData.menuNameKor,
|
||||
menuData.menuNameEng || null,
|
||||
menuData.seq ? Number(menuData.seq) : null,
|
||||
menuData.menuUrl || null,
|
||||
menuUrl,
|
||||
menuData.menuDesc || null,
|
||||
req.user?.userId || "admin",
|
||||
new Date(),
|
||||
|
|
@ -1150,6 +1154,7 @@ export async function saveMenu(
|
|||
companyCode,
|
||||
menuData.langKey || null,
|
||||
menuData.langKeyDesc || null,
|
||||
screenCode,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -1274,6 +1279,10 @@ export async function updateMenu(
|
|||
|
||||
const companyCode = requestCompanyCode;
|
||||
|
||||
// menu_url이 비어있으면 screen_code도 null로 설정
|
||||
const menuUrl = menuData.menuUrl || null;
|
||||
const screenCode = menuUrl ? menuData.screenCode || null : null;
|
||||
|
||||
// Raw Query를 사용한 메뉴 수정
|
||||
const [updatedMenu] = await query<any>(
|
||||
`UPDATE menu_info SET
|
||||
|
|
@ -1288,8 +1297,9 @@ export async function updateMenu(
|
|||
system_name = $9,
|
||||
company_code = $10,
|
||||
lang_key = $11,
|
||||
lang_key_desc = $12
|
||||
WHERE objid = $13
|
||||
lang_key_desc = $12,
|
||||
screen_code = $13
|
||||
WHERE objid = $14
|
||||
RETURNING *`,
|
||||
[
|
||||
menuData.menuType ? Number(menuData.menuType) : null,
|
||||
|
|
@ -1297,17 +1307,29 @@ export async function updateMenu(
|
|||
menuData.menuNameKor,
|
||||
menuData.menuNameEng || null,
|
||||
menuData.seq ? Number(menuData.seq) : null,
|
||||
menuData.menuUrl || null,
|
||||
menuUrl,
|
||||
menuData.menuDesc || null,
|
||||
menuData.status || "active",
|
||||
menuData.systemName || null,
|
||||
companyCode,
|
||||
menuData.langKey || null,
|
||||
menuData.langKeyDesc || null,
|
||||
screenCode,
|
||||
Number(menuId),
|
||||
]
|
||||
);
|
||||
|
||||
// menu_url이 비어있으면 화면 할당도 해제 (screen_menu_assignments의 is_active를 'N'으로)
|
||||
if (!menuUrl) {
|
||||
await query(
|
||||
`UPDATE screen_menu_assignments
|
||||
SET is_active = 'N', updated_date = NOW()
|
||||
WHERE menu_objid = $1 AND company_code = $2`,
|
||||
[Number(menuId), companyCode]
|
||||
);
|
||||
logger.info("화면 할당 비활성화", { menuId, companyCode });
|
||||
}
|
||||
|
||||
logger.info("메뉴 수정 성공", { updatedMenu });
|
||||
|
||||
const response: ApiResponse<any> = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -223,12 +223,14 @@ export class EntityJoinService {
|
|||
const aliasMap = new Map<string, string>();
|
||||
const usedAliasesForColumns = new Set<string>();
|
||||
|
||||
// joinConfigs를 참조 테이블별로 중복 제거하여 별칭 생성
|
||||
// joinConfigs를 참조 테이블 + 소스 컬럼별로 중복 제거하여 별칭 생성
|
||||
// (table_column_category_values는 같은 테이블이라도 sourceColumn마다 별도 JOIN 필요)
|
||||
const uniqueReferenceTableConfigs = joinConfigs.reduce((acc, config) => {
|
||||
if (
|
||||
!acc.some(
|
||||
(existingConfig) =>
|
||||
existingConfig.referenceTable === config.referenceTable
|
||||
existingConfig.referenceTable === config.referenceTable &&
|
||||
existingConfig.sourceColumn === config.sourceColumn
|
||||
)
|
||||
) {
|
||||
acc.push(config);
|
||||
|
|
@ -237,7 +239,7 @@ export class EntityJoinService {
|
|||
}, [] as EntityJoinConfig[]);
|
||||
|
||||
logger.info(
|
||||
`🔧 별칭 생성 시작: ${uniqueReferenceTableConfigs.length}개 고유 테이블`
|
||||
`🔧 별칭 생성 시작: ${uniqueReferenceTableConfigs.length}개 고유 테이블+컬럼 조합`
|
||||
);
|
||||
|
||||
uniqueReferenceTableConfigs.forEach((config) => {
|
||||
|
|
@ -250,13 +252,16 @@ export class EntityJoinService {
|
|||
counter++;
|
||||
}
|
||||
usedAliasesForColumns.add(alias);
|
||||
aliasMap.set(config.referenceTable, alias);
|
||||
logger.info(`🔧 별칭 생성: ${config.referenceTable} → ${alias}`);
|
||||
// 같은 테이블이라도 sourceColumn이 다르면 별도 별칭 생성 (table_column_category_values 대응)
|
||||
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
||||
aliasMap.set(aliasKey, alias);
|
||||
logger.info(`🔧 별칭 생성: ${config.referenceTable}.${config.sourceColumn} → ${alias}`);
|
||||
});
|
||||
|
||||
const joinColumns = joinConfigs
|
||||
.map((config) => {
|
||||
const alias = aliasMap.get(config.referenceTable);
|
||||
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
||||
const alias = aliasMap.get(aliasKey);
|
||||
const displayColumns = config.displayColumns || [
|
||||
config.displayColumn,
|
||||
];
|
||||
|
|
@ -346,14 +351,16 @@ export class EntityJoinService {
|
|||
// FROM 절 (메인 테이블)
|
||||
const fromClause = `FROM ${tableName} main`;
|
||||
|
||||
// LEFT JOIN 절들 (위에서 생성한 별칭 매핑 사용, 중복 테이블 제거)
|
||||
// LEFT JOIN 절들 (위에서 생성한 별칭 매핑 사용, 각 sourceColumn마다 별도 JOIN)
|
||||
const joinClauses = uniqueReferenceTableConfigs
|
||||
.map((config) => {
|
||||
const alias = aliasMap.get(config.referenceTable);
|
||||
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
||||
const alias = aliasMap.get(aliasKey);
|
||||
|
||||
// table_column_category_values는 특별한 조인 조건 필요
|
||||
// table_column_category_values는 특별한 조인 조건 필요 (회사별 필터링만)
|
||||
if (config.referenceTable === 'table_column_category_values') {
|
||||
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn} AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}'`;
|
||||
// 멀티테넌시: 회사 데이터만 사용 (공통 데이터 제외)
|
||||
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn} AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}' AND ${alias}.company_code = main.company_code AND ${alias}.is_active = true`;
|
||||
}
|
||||
|
||||
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn}`;
|
||||
|
|
@ -538,12 +545,13 @@ export class EntityJoinService {
|
|||
const aliasMap = new Map<string, string>();
|
||||
const usedAliases = new Set<string>();
|
||||
|
||||
// joinConfigs를 참조 테이블별로 중복 제거하여 별칭 생성
|
||||
// joinConfigs를 참조 테이블 + 소스 컬럼별로 중복 제거하여 별칭 생성
|
||||
const uniqueReferenceTableConfigs = joinConfigs.reduce((acc, config) => {
|
||||
if (
|
||||
!acc.some(
|
||||
(existingConfig) =>
|
||||
existingConfig.referenceTable === config.referenceTable
|
||||
existingConfig.referenceTable === config.referenceTable &&
|
||||
existingConfig.sourceColumn === config.sourceColumn
|
||||
)
|
||||
) {
|
||||
acc.push(config);
|
||||
|
|
@ -561,13 +569,22 @@ export class EntityJoinService {
|
|||
counter++;
|
||||
}
|
||||
usedAliases.add(alias);
|
||||
aliasMap.set(config.referenceTable, alias);
|
||||
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
||||
aliasMap.set(aliasKey, alias);
|
||||
});
|
||||
|
||||
// JOIN 절들 (COUNT에서는 SELECT 컬럼 불필요)
|
||||
const joinClauses = uniqueReferenceTableConfigs
|
||||
.map((config) => {
|
||||
const alias = aliasMap.get(config.referenceTable);
|
||||
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
||||
const alias = aliasMap.get(aliasKey);
|
||||
|
||||
// table_column_category_values는 특별한 조인 조건 필요 (회사별 필터링만)
|
||||
if (config.referenceTable === 'table_column_category_values') {
|
||||
// 멀티테넌시: 회사 데이터만 사용 (공통 데이터 제외)
|
||||
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn} AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}' AND ${alias}.company_code = main.company_code AND ${alias}.is_active = true`;
|
||||
}
|
||||
|
||||
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
@ -2347,11 +2404,16 @@ export class TableManagementService {
|
|||
whereClause
|
||||
);
|
||||
|
||||
// ⚠️ SQL 쿼리 로깅 (디버깅용)
|
||||
logger.info(`🔍 [executeJoinQuery] 실행할 SQL:\n${dataQuery}`);
|
||||
|
||||
// 병렬 실행
|
||||
const [dataResult, countResult] = await Promise.all([
|
||||
query(dataQuery),
|
||||
query(countQuery),
|
||||
]);
|
||||
|
||||
logger.info(`✅ [executeJoinQuery] 조회 완료: ${dataResult?.length}개 행`);
|
||||
|
||||
const data = Array.isArray(dataResult) ? dataResult : [];
|
||||
const total =
|
||||
|
|
@ -3152,19 +3214,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'인 경우 참조 테이블 선택 */}
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
setFormData((prev) => ({
|
||||
...prev,
|
||||
menuUrl: screenUrl,
|
||||
screenCode: screen.screenCode, // 화면 코드도 함께 저장
|
||||
}));
|
||||
|
||||
// console.log("🖥️ 화면 선택 완료:", {
|
||||
|
|
@ -207,10 +208,11 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
if (type === "direct") {
|
||||
// 직접 입력 모드로 변경 시 선택된 화면 초기화
|
||||
setSelectedScreen(null);
|
||||
// URL 필드도 초기화 (사용자가 직접 입력할 수 있도록)
|
||||
// URL 필드와 screenCode 초기화 (사용자가 직접 입력할 수 있도록)
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
menuUrl: "",
|
||||
screenCode: undefined, // 화면 코드도 함께 초기화
|
||||
}));
|
||||
} else {
|
||||
// 화면 할당 모드로 변경 시
|
||||
|
|
@ -230,12 +232,14 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
setFormData((prev) => ({
|
||||
...prev,
|
||||
menuUrl: screenUrl,
|
||||
screenCode: selectedScreen.screenCode, // 화면 코드도 함께 유지
|
||||
}));
|
||||
} else {
|
||||
// 선택된 화면이 없으면 URL만 초기화
|
||||
// 선택된 화면이 없으면 URL과 screenCode 초기화
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
menuUrl: "",
|
||||
screenCode: undefined,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -575,6 +575,8 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log("🔍 데이터 조회 시작:", { tableName: component.tableName, page, pageSize });
|
||||
|
||||
const result = await tableTypeApi.getTableData(component.tableName, {
|
||||
page,
|
||||
size: pageSize,
|
||||
|
|
@ -582,6 +584,13 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
autoFilter: component.autoFilter, // 🆕 자동 필터 설정 전달
|
||||
});
|
||||
|
||||
console.log("✅ 데이터 조회 완료:", {
|
||||
tableName: component.tableName,
|
||||
dataLength: result.data.length,
|
||||
total: result.total,
|
||||
page: result.page
|
||||
});
|
||||
|
||||
setData(result.data);
|
||||
setTotal(result.total);
|
||||
setTotalPages(result.totalPages);
|
||||
|
|
@ -1952,7 +1961,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
// 실제 웹 타입으로 스위치 (input_type="category"도 포함됨)
|
||||
switch (actualWebType) {
|
||||
case "category": {
|
||||
// 카테고리 타입: 배지로 표시
|
||||
// 카테고리 타입: 배지로 표시 (배지 없음 옵션 지원)
|
||||
if (!value) return "";
|
||||
|
||||
const mapping = categoryMappings[column.columnName];
|
||||
|
|
@ -1962,6 +1971,11 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
const displayLabel = categoryData?.label || String(value);
|
||||
const displayColor = categoryData?.color || "#64748b"; // 기본 slate 색상
|
||||
|
||||
// 배지 없음 옵션: color가 "none"이면 텍스트만 표시
|
||||
if (displayColor === "none") {
|
||||
return <span className="text-sm">{displayLabel}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -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 ? "추가 중..." : "추가"}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export const CategoryValueAddDialog: React.FC<
|
|||
> = ({ open, onOpenChange, onAdd, columnLabel }) => {
|
||||
const [valueLabel, setValueLabel] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [color, setColor] = useState("#3b82f6");
|
||||
const [color, setColor] = useState("none");
|
||||
|
||||
// 라벨에서 코드 자동 생성
|
||||
const generateCode = (label: string): string => {
|
||||
|
|
@ -91,7 +91,7 @@ export const CategoryValueAddDialog: React.FC<
|
|||
// 초기화
|
||||
setValueLabel("");
|
||||
setDescription("");
|
||||
setColor("#3b82f6");
|
||||
setColor("none");
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -123,24 +123,41 @@ export const CategoryValueAddDialog: React.FC<
|
|||
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">배지 색상</Label>
|
||||
<div className="mt-1.5 flex items-center gap-3">
|
||||
<div className="grid grid-cols-9 gap-2">
|
||||
{DEFAULT_COLORS.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
onClick={() => setColor(c)}
|
||||
className={`h-7 w-7 rounded-md border-2 transition-all ${
|
||||
color === c ? "border-foreground scale-110" : "border-transparent hover:scale-105"
|
||||
}`}
|
||||
style={{ backgroundColor: c }}
|
||||
title={c}
|
||||
/>
|
||||
))}
|
||||
<div className="mt-1.5 space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="grid grid-cols-9 gap-2">
|
||||
{DEFAULT_COLORS.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
onClick={() => setColor(c)}
|
||||
className={`h-7 w-7 rounded-md border-2 transition-all ${
|
||||
color === c ? "border-foreground scale-110" : "border-transparent hover:scale-105"
|
||||
}`}
|
||||
style={{ backgroundColor: c }}
|
||||
title={c}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{color && color !== "none" ? (
|
||||
<Badge style={{ backgroundColor: color, borderColor: color }} className="text-white">
|
||||
미리보기
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">배지 없음</span>
|
||||
)}
|
||||
</div>
|
||||
<Badge style={{ backgroundColor: color, borderColor: color }} className="text-white">
|
||||
미리보기
|
||||
</Badge>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setColor("none")}
|
||||
className={`text-xs px-3 py-1.5 rounded-md border transition-colors ${
|
||||
color === "none"
|
||||
? "border-primary bg-primary/10 text-primary font-medium"
|
||||
: "border-border hover:bg-accent"
|
||||
}`}
|
||||
>
|
||||
배지 없음
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -51,12 +51,12 @@ export const CategoryValueEditDialog: React.FC<
|
|||
> = ({ open, onOpenChange, value, onUpdate, columnLabel }) => {
|
||||
const [valueLabel, setValueLabel] = useState(value.valueLabel);
|
||||
const [description, setDescription] = useState(value.description || "");
|
||||
const [color, setColor] = useState(value.color || "#3b82f6");
|
||||
const [color, setColor] = useState(value.color || "none");
|
||||
|
||||
useEffect(() => {
|
||||
setValueLabel(value.valueLabel);
|
||||
setDescription(value.description || "");
|
||||
setColor(value.color || "#3b82f6");
|
||||
setColor(value.color || "none");
|
||||
}, [value]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
|
|
@ -100,24 +100,41 @@ export const CategoryValueEditDialog: React.FC<
|
|||
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">배지 색상</Label>
|
||||
<div className="mt-1.5 flex items-center gap-3">
|
||||
<div className="grid grid-cols-9 gap-2">
|
||||
{DEFAULT_COLORS.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
onClick={() => setColor(c)}
|
||||
className={`h-7 w-7 rounded-md border-2 transition-all ${
|
||||
color === c ? "border-foreground scale-110" : "border-transparent hover:scale-105"
|
||||
}`}
|
||||
style={{ backgroundColor: c }}
|
||||
title={c}
|
||||
/>
|
||||
))}
|
||||
<div className="mt-1.5 space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="grid grid-cols-9 gap-2">
|
||||
{DEFAULT_COLORS.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
onClick={() => setColor(c)}
|
||||
className={`h-7 w-7 rounded-md border-2 transition-all ${
|
||||
color === c ? "border-foreground scale-110" : "border-transparent hover:scale-105"
|
||||
}`}
|
||||
style={{ backgroundColor: c }}
|
||||
title={c}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{color && color !== "none" ? (
|
||||
<Badge style={{ backgroundColor: color, borderColor: color }} className="text-white">
|
||||
미리보기
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">배지 없음</span>
|
||||
)}
|
||||
</div>
|
||||
<Badge style={{ backgroundColor: color, borderColor: color }} className="text-white">
|
||||
미리보기
|
||||
</Badge>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setColor("none")}
|
||||
className={`text-xs px-3 py-1.5 rounded-md border transition-colors ${
|
||||
color === "none"
|
||||
? "border-primary bg-primary/10 text-primary font-medium"
|
||||
: "border-border hover:bg-accent"
|
||||
}`}
|
||||
>
|
||||
배지 없음
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -349,13 +349,18 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
|||
/>
|
||||
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
{/* 색상 표시 (앞쪽으로 이동) */}
|
||||
{value.color && (
|
||||
{/* 색상 표시 (배지 없음 옵션 지원) */}
|
||||
{value.color && value.color !== "none" && (
|
||||
<div
|
||||
className="h-4 w-4 rounded-full border flex-shrink-0"
|
||||
style={{ backgroundColor: value.color }}
|
||||
/>
|
||||
)}
|
||||
{value.color === "none" && (
|
||||
<span className="text-[10px] text-muted-foreground px-1.5 py-0.5 bg-muted rounded">
|
||||
배지 없음
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* 라벨 */}
|
||||
<span className={`text-sm font-medium ${isInactive ? "line-through" : ""}`}>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export interface MenuFormData {
|
|||
status: string;
|
||||
companyCode: string;
|
||||
langKey?: string; // 다국어 키 추가
|
||||
screenCode?: string; // 화면 코드 추가
|
||||
}
|
||||
|
||||
export interface LangKey {
|
||||
|
|
|
|||
|
|
@ -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: [] };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -802,6 +802,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
// ========================================
|
||||
|
||||
const fetchTableDataInternal = useCallback(async () => {
|
||||
console.log("📡 [TableList] fetchTableDataInternal 호출됨", {
|
||||
tableName: tableConfig.selectedTable,
|
||||
isDesignMode,
|
||||
currentPage,
|
||||
});
|
||||
|
||||
if (!tableConfig.selectedTable || isDesignMode) {
|
||||
setData([]);
|
||||
setTotalPages(0);
|
||||
|
|
@ -809,11 +815,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
// 테이블명 확인 로그 (개발 시에만)
|
||||
// console.log("🔍 fetchTableDataInternal - selectedTable:", tableConfig.selectedTable);
|
||||
// console.log("🔍 selectedTable 타입:", typeof tableConfig.selectedTable);
|
||||
// console.log("🔍 전체 tableConfig:", tableConfig);
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
|
|
@ -834,6 +835,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
referenceTable: col.additionalJoinInfo!.referenceTable,
|
||||
}));
|
||||
|
||||
console.log("🔍 [TableList] API 호출 시작", {
|
||||
tableName: tableConfig.selectedTable,
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
});
|
||||
|
||||
// 🎯 항상 entityJoinApi 사용 (writer 컬럼 자동 조인 지원)
|
||||
const response = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, {
|
||||
page,
|
||||
|
|
@ -845,6 +854,17 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
additionalJoinColumns: entityJoinColumns.length > 0 ? entityJoinColumns : undefined,
|
||||
});
|
||||
|
||||
// 실제 데이터의 item_number만 추출하여 중복 확인
|
||||
const itemNumbers = (response.data || []).map((item: any) => item.item_number);
|
||||
const uniqueItemNumbers = [...new Set(itemNumbers)];
|
||||
|
||||
console.log("✅ [TableList] API 응답 받음");
|
||||
console.log(` - dataLength: ${response.data?.length || 0}`);
|
||||
console.log(` - total: ${response.total}`);
|
||||
console.log(` - itemNumbers: ${JSON.stringify(itemNumbers)}`);
|
||||
console.log(` - uniqueItemNumbers: ${JSON.stringify(uniqueItemNumbers)}`);
|
||||
console.log(` - isDuplicated: ${itemNumbers.length !== uniqueItemNumbers.length}`);
|
||||
|
||||
setData(response.data || []);
|
||||
setTotalPages(response.totalPages || 0);
|
||||
setTotalItems(response.total || 0);
|
||||
|
|
@ -1354,28 +1374,22 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
// 카테고리 타입: 배지로 표시
|
||||
// 카테고리 타입: 배지로 표시 (배지 없음 옵션 지원)
|
||||
if (inputType === "category") {
|
||||
if (!value) return "";
|
||||
|
||||
const mapping = categoryMappings[column.columnName];
|
||||
const categoryData = mapping?.[String(value)];
|
||||
|
||||
// console.log(`🎨 [카테고리 배지] ${column.columnName}:`, {
|
||||
// value,
|
||||
// stringValue: String(value),
|
||||
// mapping,
|
||||
// categoryData,
|
||||
// hasMapping: !!mapping,
|
||||
// hasCategoryData: !!categoryData,
|
||||
// allCategoryMappings: categoryMappings, // 전체 매핑 확인
|
||||
// categoryMappingsKeys: Object.keys(categoryMappings),
|
||||
// });
|
||||
|
||||
// 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값과 기본색상
|
||||
const displayLabel = categoryData?.label || String(value);
|
||||
const displayColor = categoryData?.color || "#64748b"; // 기본 slate 색상
|
||||
|
||||
// 배지 없음 옵션: color가 "none"이면 텍스트만 표시
|
||||
if (displayColor === "none") {
|
||||
return <span className="text-sm">{displayLabel}</span>;
|
||||
}
|
||||
|
||||
const { Badge } = require("@/components/ui/badge");
|
||||
return (
|
||||
<Badge
|
||||
|
|
@ -1716,6 +1730,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
}, [tableConfig.selectedTable, fetchColumnLabels, fetchTableLabel]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("🔍 [TableList] useEffect 실행 - 데이터 조회 트리거", {
|
||||
isDesignMode,
|
||||
tableName: tableConfig.selectedTable,
|
||||
currentPage,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
});
|
||||
|
||||
if (!isDesignMode && tableConfig.selectedTable) {
|
||||
fetchTableDataDebounced();
|
||||
}
|
||||
|
|
@ -1730,7 +1752,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
refreshKey,
|
||||
refreshTrigger, // 강제 새로고침 트리거
|
||||
isDesignMode,
|
||||
fetchTableDataDebounced,
|
||||
// fetchTableDataDebounced 제거: useCallback 재생성으로 인한 무한 루프 방지
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -2157,9 +2179,18 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : groupByColumns.length > 0 && groupedData.length > 0 ? (
|
||||
) : (() => {
|
||||
console.log("🔍 [TableList] 렌더링 조건 체크", {
|
||||
groupByColumns: groupByColumns.length,
|
||||
groupedDataLength: groupedData.length,
|
||||
willRenderGrouped: groupByColumns.length > 0 && groupedData.length > 0,
|
||||
dataLength: data.length,
|
||||
});
|
||||
return groupByColumns.length > 0 && groupedData.length > 0;
|
||||
})() ? (
|
||||
// 그룹화된 렌더링
|
||||
groupedData.map((group) => {
|
||||
console.log("📊 [TableList] 그룹 렌더링:", group.groupKey, group.count);
|
||||
const isCollapsed = collapsedGroups.has(group.groupKey);
|
||||
return (
|
||||
<React.Fragment key={group.groupKey}>
|
||||
|
|
@ -2252,7 +2283,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
})
|
||||
) : (
|
||||
// 일반 렌더링 (그룹 없음)
|
||||
data.map((row, index) => (
|
||||
(() => {
|
||||
console.log("📋 [TableList] 일반 렌더링 시작:", data.length, "개 행");
|
||||
return data;
|
||||
})().map((row, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className={cn(
|
||||
|
|
|
|||
Loading…
Reference in New Issue