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 objid = Date.now(); // 고유 ID 생성
|
||||||
const companyCode = requestCompanyCode || userCompanyCode;
|
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>(
|
const [savedMenu] = await query<any>(
|
||||||
`INSERT INTO menu_info (
|
`INSERT INTO menu_info (
|
||||||
objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng,
|
objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng,
|
||||||
seq, menu_url, menu_desc, writer, regdate, status,
|
seq, menu_url, menu_desc, writer, regdate, status,
|
||||||
system_name, company_code, lang_key, lang_key_desc
|
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)
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
objid,
|
objid,
|
||||||
|
|
@ -1141,7 +1145,7 @@ export async function saveMenu(
|
||||||
menuData.menuNameKor,
|
menuData.menuNameKor,
|
||||||
menuData.menuNameEng || null,
|
menuData.menuNameEng || null,
|
||||||
menuData.seq ? Number(menuData.seq) : null,
|
menuData.seq ? Number(menuData.seq) : null,
|
||||||
menuData.menuUrl || null,
|
menuUrl,
|
||||||
menuData.menuDesc || null,
|
menuData.menuDesc || null,
|
||||||
req.user?.userId || "admin",
|
req.user?.userId || "admin",
|
||||||
new Date(),
|
new Date(),
|
||||||
|
|
@ -1150,6 +1154,7 @@ export async function saveMenu(
|
||||||
companyCode,
|
companyCode,
|
||||||
menuData.langKey || null,
|
menuData.langKey || null,
|
||||||
menuData.langKeyDesc || null,
|
menuData.langKeyDesc || null,
|
||||||
|
screenCode,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1274,6 +1279,10 @@ export async function updateMenu(
|
||||||
|
|
||||||
const companyCode = requestCompanyCode;
|
const companyCode = requestCompanyCode;
|
||||||
|
|
||||||
|
// menu_url이 비어있으면 screen_code도 null로 설정
|
||||||
|
const menuUrl = menuData.menuUrl || null;
|
||||||
|
const screenCode = menuUrl ? menuData.screenCode || null : null;
|
||||||
|
|
||||||
// Raw Query를 사용한 메뉴 수정
|
// Raw Query를 사용한 메뉴 수정
|
||||||
const [updatedMenu] = await query<any>(
|
const [updatedMenu] = await query<any>(
|
||||||
`UPDATE menu_info SET
|
`UPDATE menu_info SET
|
||||||
|
|
@ -1288,8 +1297,9 @@ export async function updateMenu(
|
||||||
system_name = $9,
|
system_name = $9,
|
||||||
company_code = $10,
|
company_code = $10,
|
||||||
lang_key = $11,
|
lang_key = $11,
|
||||||
lang_key_desc = $12
|
lang_key_desc = $12,
|
||||||
WHERE objid = $13
|
screen_code = $13
|
||||||
|
WHERE objid = $14
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
menuData.menuType ? Number(menuData.menuType) : null,
|
menuData.menuType ? Number(menuData.menuType) : null,
|
||||||
|
|
@ -1297,17 +1307,29 @@ export async function updateMenu(
|
||||||
menuData.menuNameKor,
|
menuData.menuNameKor,
|
||||||
menuData.menuNameEng || null,
|
menuData.menuNameEng || null,
|
||||||
menuData.seq ? Number(menuData.seq) : null,
|
menuData.seq ? Number(menuData.seq) : null,
|
||||||
menuData.menuUrl || null,
|
menuUrl,
|
||||||
menuData.menuDesc || null,
|
menuData.menuDesc || null,
|
||||||
menuData.status || "active",
|
menuData.status || "active",
|
||||||
menuData.systemName || null,
|
menuData.systemName || null,
|
||||||
companyCode,
|
companyCode,
|
||||||
menuData.langKey || null,
|
menuData.langKey || null,
|
||||||
menuData.langKeyDesc || null,
|
menuData.langKeyDesc || null,
|
||||||
|
screenCode,
|
||||||
Number(menuId),
|
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 });
|
logger.info("메뉴 수정 성공", { updatedMenu });
|
||||||
|
|
||||||
const response: ApiResponse<any> = {
|
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,8 +1657,79 @@ export async function getCategoryColumnsByMenu(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 테이블들의 카테고리 타입 컬럼 조회 (테이블 라벨 포함)
|
// 3. category_column_mapping 테이블 존재 여부 확인
|
||||||
logger.info("🔍 카테고리 컬럼 쿼리 준비", { tableNames, companyCode });
|
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 = `
|
const columnsQuery = `
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -1685,9 +1756,9 @@ export async function getCategoryColumnsByMenu(
|
||||||
ORDER BY ttc.table_name, ttc.column_name
|
ORDER BY ttc.table_name, ttc.column_name
|
||||||
`;
|
`;
|
||||||
|
|
||||||
logger.info("🔍 카테고리 컬럼 쿼리 실행 중...");
|
columnsResult = await pool.query(columnsQuery, [tableNames, companyCode]);
|
||||||
const columnsResult = await pool.query(columnsQuery, [tableNames, companyCode]);
|
logger.info("✅ 레거시 방식 조회 완료", { rowCount: columnsResult.rows.length });
|
||||||
logger.info("✅ 카테고리 컬럼 쿼리 완료", { rowCount: columnsResult.rows.length });
|
}
|
||||||
|
|
||||||
logger.info("✅ 카테고리 컬럼 조회 완료", {
|
logger.info("✅ 카테고리 컬럼 조회 완료", {
|
||||||
columnCount: columnsResult.rows.length
|
columnCount: columnsResult.rows.length
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
createColumnMapping,
|
createColumnMapping,
|
||||||
getLogicalColumns,
|
getLogicalColumns,
|
||||||
deleteColumnMapping,
|
deleteColumnMapping,
|
||||||
|
getSecondLevelMenus,
|
||||||
} from "../controllers/tableCategoryValueController";
|
} from "../controllers/tableCategoryValueController";
|
||||||
import { authenticateToken } from "../middleware/authMiddleware";
|
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);
|
router.get("/column-mapping/:tableName/:menuObjid", getColumnMapping);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -223,12 +223,14 @@ export class EntityJoinService {
|
||||||
const aliasMap = new Map<string, string>();
|
const aliasMap = new Map<string, string>();
|
||||||
const usedAliasesForColumns = new Set<string>();
|
const usedAliasesForColumns = new Set<string>();
|
||||||
|
|
||||||
// joinConfigs를 참조 테이블별로 중복 제거하여 별칭 생성
|
// joinConfigs를 참조 테이블 + 소스 컬럼별로 중복 제거하여 별칭 생성
|
||||||
|
// (table_column_category_values는 같은 테이블이라도 sourceColumn마다 별도 JOIN 필요)
|
||||||
const uniqueReferenceTableConfigs = joinConfigs.reduce((acc, config) => {
|
const uniqueReferenceTableConfigs = joinConfigs.reduce((acc, config) => {
|
||||||
if (
|
if (
|
||||||
!acc.some(
|
!acc.some(
|
||||||
(existingConfig) =>
|
(existingConfig) =>
|
||||||
existingConfig.referenceTable === config.referenceTable
|
existingConfig.referenceTable === config.referenceTable &&
|
||||||
|
existingConfig.sourceColumn === config.sourceColumn
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
acc.push(config);
|
acc.push(config);
|
||||||
|
|
@ -237,7 +239,7 @@ export class EntityJoinService {
|
||||||
}, [] as EntityJoinConfig[]);
|
}, [] as EntityJoinConfig[]);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`🔧 별칭 생성 시작: ${uniqueReferenceTableConfigs.length}개 고유 테이블`
|
`🔧 별칭 생성 시작: ${uniqueReferenceTableConfigs.length}개 고유 테이블+컬럼 조합`
|
||||||
);
|
);
|
||||||
|
|
||||||
uniqueReferenceTableConfigs.forEach((config) => {
|
uniqueReferenceTableConfigs.forEach((config) => {
|
||||||
|
|
@ -250,13 +252,16 @@ export class EntityJoinService {
|
||||||
counter++;
|
counter++;
|
||||||
}
|
}
|
||||||
usedAliasesForColumns.add(alias);
|
usedAliasesForColumns.add(alias);
|
||||||
aliasMap.set(config.referenceTable, alias);
|
// 같은 테이블이라도 sourceColumn이 다르면 별도 별칭 생성 (table_column_category_values 대응)
|
||||||
logger.info(`🔧 별칭 생성: ${config.referenceTable} → ${alias}`);
|
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
||||||
|
aliasMap.set(aliasKey, alias);
|
||||||
|
logger.info(`🔧 별칭 생성: ${config.referenceTable}.${config.sourceColumn} → ${alias}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const joinColumns = joinConfigs
|
const joinColumns = joinConfigs
|
||||||
.map((config) => {
|
.map((config) => {
|
||||||
const alias = aliasMap.get(config.referenceTable);
|
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
||||||
|
const alias = aliasMap.get(aliasKey);
|
||||||
const displayColumns = config.displayColumns || [
|
const displayColumns = config.displayColumns || [
|
||||||
config.displayColumn,
|
config.displayColumn,
|
||||||
];
|
];
|
||||||
|
|
@ -346,14 +351,16 @@ export class EntityJoinService {
|
||||||
// FROM 절 (메인 테이블)
|
// FROM 절 (메인 테이블)
|
||||||
const fromClause = `FROM ${tableName} main`;
|
const fromClause = `FROM ${tableName} main`;
|
||||||
|
|
||||||
// LEFT JOIN 절들 (위에서 생성한 별칭 매핑 사용, 중복 테이블 제거)
|
// LEFT JOIN 절들 (위에서 생성한 별칭 매핑 사용, 각 sourceColumn마다 별도 JOIN)
|
||||||
const joinClauses = uniqueReferenceTableConfigs
|
const joinClauses = uniqueReferenceTableConfigs
|
||||||
.map((config) => {
|
.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') {
|
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}`;
|
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 aliasMap = new Map<string, string>();
|
||||||
const usedAliases = new Set<string>();
|
const usedAliases = new Set<string>();
|
||||||
|
|
||||||
// joinConfigs를 참조 테이블별로 중복 제거하여 별칭 생성
|
// joinConfigs를 참조 테이블 + 소스 컬럼별로 중복 제거하여 별칭 생성
|
||||||
const uniqueReferenceTableConfigs = joinConfigs.reduce((acc, config) => {
|
const uniqueReferenceTableConfigs = joinConfigs.reduce((acc, config) => {
|
||||||
if (
|
if (
|
||||||
!acc.some(
|
!acc.some(
|
||||||
(existingConfig) =>
|
(existingConfig) =>
|
||||||
existingConfig.referenceTable === config.referenceTable
|
existingConfig.referenceTable === config.referenceTable &&
|
||||||
|
existingConfig.sourceColumn === config.sourceColumn
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
acc.push(config);
|
acc.push(config);
|
||||||
|
|
@ -561,13 +569,22 @@ export class EntityJoinService {
|
||||||
counter++;
|
counter++;
|
||||||
}
|
}
|
||||||
usedAliases.add(alias);
|
usedAliases.add(alias);
|
||||||
aliasMap.set(config.referenceTable, alias);
|
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
||||||
|
aliasMap.set(aliasKey, alias);
|
||||||
});
|
});
|
||||||
|
|
||||||
// JOIN 절들 (COUNT에서는 SELECT 컬럼 불필요)
|
// JOIN 절들 (COUNT에서는 SELECT 컬럼 불필요)
|
||||||
const joinClauses = uniqueReferenceTableConfigs
|
const joinClauses = uniqueReferenceTableConfigs
|
||||||
.map((config) => {
|
.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}`;
|
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn}`;
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
|
||||||
|
|
@ -1514,6 +1514,7 @@ export class ScreenManagementService {
|
||||||
throw new Error("이미 할당된 화면입니다.");
|
throw new Error("이미 할당된 화면입니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// screen_menu_assignments에 할당 추가
|
||||||
await query(
|
await query(
|
||||||
`INSERT INTO screen_menu_assignments (
|
`INSERT INTO screen_menu_assignments (
|
||||||
screen_id, menu_objid, company_code, display_order, created_by
|
screen_id, menu_objid, company_code, display_order, created_by
|
||||||
|
|
@ -1526,6 +1527,40 @@ export class ScreenManagementService {
|
||||||
assignmentData.createdBy || null,
|
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,
|
menuObjid: number,
|
||||||
companyCode: string
|
companyCode: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
// screen_menu_assignments에서 할당 삭제
|
||||||
await query(
|
await query(
|
||||||
`DELETE FROM screen_menu_assignments
|
`DELETE FROM screen_menu_assignments
|
||||||
WHERE screen_id = $1 AND menu_objid = $2 AND company_code = $3`,
|
WHERE screen_id = $1 AND menu_objid = $2 AND company_code = $3`,
|
||||||
[screenId, menuObjid, companyCode]
|
[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;
|
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();
|
export default new TableCategoryValueService();
|
||||||
|
|
|
||||||
|
|
@ -249,8 +249,52 @@ export class TableManagementService {
|
||||||
[tableName, size, offset]
|
[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 직렬화 문제 해결
|
// BigInt를 Number로 변환하여 JSON 직렬화 문제 해결
|
||||||
const columns: ColumnTypeInfo[] = rawColumns.map((column) => ({
|
const columns: ColumnTypeInfo[] = rawColumns.map((column) => {
|
||||||
|
const baseColumn = {
|
||||||
...column,
|
...column,
|
||||||
maxLength: column.maxLength ? Number(column.maxLength) : null,
|
maxLength: column.maxLength ? Number(column.maxLength) : null,
|
||||||
numericPrecision: column.numericPrecision
|
numericPrecision: column.numericPrecision
|
||||||
|
|
@ -263,7 +307,20 @@ export class TableManagementService {
|
||||||
column.webType === "text"
|
column.webType === "text"
|
||||||
? this.inferWebType(column.dataType)
|
? this.inferWebType(column.dataType)
|
||||||
: column.webType,
|
: 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);
|
const totalPages = Math.ceil(total / size);
|
||||||
|
|
||||||
|
|
@ -484,7 +541,7 @@ export class TableManagementService {
|
||||||
cache.deleteByPattern(`table_columns:${tableName}:`);
|
cache.deleteByPattern(`table_columns:${tableName}:`);
|
||||||
cache.delete(CacheKeys.TABLE_COLUMN_COUNT(tableName));
|
cache.delete(CacheKeys.TABLE_COLUMN_COUNT(tableName));
|
||||||
|
|
||||||
logger.info(`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}, company: ${companyCode}`);
|
logger.info(`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`전체 컬럼 설정 일괄 업데이트 중 오류 발생: ${tableName}`,
|
`전체 컬럼 설정 일괄 업데이트 중 오류 발생: ${tableName}`,
|
||||||
|
|
@ -2347,12 +2404,17 @@ export class TableManagementService {
|
||||||
whereClause
|
whereClause
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ⚠️ SQL 쿼리 로깅 (디버깅용)
|
||||||
|
logger.info(`🔍 [executeJoinQuery] 실행할 SQL:\n${dataQuery}`);
|
||||||
|
|
||||||
// 병렬 실행
|
// 병렬 실행
|
||||||
const [dataResult, countResult] = await Promise.all([
|
const [dataResult, countResult] = await Promise.all([
|
||||||
query(dataQuery),
|
query(dataQuery),
|
||||||
query(countQuery),
|
query(countQuery),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
logger.info(`✅ [executeJoinQuery] 조회 완료: ${dataResult?.length}개 행`);
|
||||||
|
|
||||||
const data = Array.isArray(dataResult) ? dataResult : [];
|
const data = Array.isArray(dataResult) ? dataResult : [];
|
||||||
const total =
|
const total =
|
||||||
Array.isArray(countResult) && countResult.length > 0
|
Array.isArray(countResult) && countResult.length > 0
|
||||||
|
|
@ -3152,7 +3214,54 @@ export class TableManagementService {
|
||||||
[tableName, companyCode]
|
[tableName, companyCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
const inputTypes: ColumnTypeInfo[] = rawInputTypes.map((col) => ({
|
// 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,
|
tableName: tableName,
|
||||||
columnName: col.columnName,
|
columnName: col.columnName,
|
||||||
displayName: col.displayName,
|
displayName: col.displayName,
|
||||||
|
|
@ -3164,7 +3273,24 @@ export class TableManagementService {
|
||||||
isPrimaryKey: false,
|
isPrimaryKey: false,
|
||||||
displayOrder: 0,
|
displayOrder: 0,
|
||||||
isVisible: true,
|
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(
|
logger.info(
|
||||||
`컬럼 입력타입 정보 조회 완료: ${tableName}, company: ${companyCode}, ${inputTypes.length}개 컬럼`
|
`컬럼 입력타입 정보 조회 완료: ${tableName}, company: ${companyCode}, ${inputTypes.length}개 컬럼`
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { apiClient } from "@/lib/api/client";
|
||||||
import { commonCodeApi } from "@/lib/api/commonCode";
|
import { commonCodeApi } from "@/lib/api/commonCode";
|
||||||
import { entityJoinApi, ReferenceTableColumn } from "@/lib/api/entityJoin";
|
import { entityJoinApi, ReferenceTableColumn } from "@/lib/api/entityJoin";
|
||||||
import { ddlApi } from "@/lib/api/ddl";
|
import { ddlApi } from "@/lib/api/ddl";
|
||||||
|
import { getSecondLevelMenus, createColumnMapping } from "@/lib/api/tableCategoryValue";
|
||||||
import { CreateTableModal } from "@/components/admin/CreateTableModal";
|
import { CreateTableModal } from "@/components/admin/CreateTableModal";
|
||||||
import { AddColumnModal } from "@/components/admin/AddColumnModal";
|
import { AddColumnModal } from "@/components/admin/AddColumnModal";
|
||||||
import { DDLLogViewer } from "@/components/admin/DDLLogViewer";
|
import { DDLLogViewer } from "@/components/admin/DDLLogViewer";
|
||||||
|
|
@ -54,6 +55,14 @@ interface ColumnTypeInfo {
|
||||||
referenceTable?: string;
|
referenceTable?: string;
|
||||||
referenceColumn?: string;
|
referenceColumn?: string;
|
||||||
displayColumn?: string; // 🎯 Entity 조인에서 표시할 컬럼명
|
displayColumn?: string; // 🎯 Entity 조인에서 표시할 컬럼명
|
||||||
|
categoryMenus?: number[]; // 🆕 Category 타입: 선택된 2레벨 메뉴 OBJID 배열
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SecondLevelMenu {
|
||||||
|
menuObjid: number;
|
||||||
|
menuName: string;
|
||||||
|
parentMenuName: string;
|
||||||
|
screenCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TableManagementPage() {
|
export default function TableManagementPage() {
|
||||||
|
|
@ -89,6 +98,9 @@ export default function TableManagementPage() {
|
||||||
const [duplicateModalMode, setDuplicateModalMode] = useState<"create" | "duplicate">("create");
|
const [duplicateModalMode, setDuplicateModalMode] = useState<"create" | "duplicate">("create");
|
||||||
const [duplicateSourceTable, setDuplicateSourceTable] = useState<string | null>(null);
|
const [duplicateSourceTable, setDuplicateSourceTable] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 🆕 Category 타입용: 2레벨 메뉴 목록
|
||||||
|
const [secondLevelMenus, setSecondLevelMenus] = useState<SecondLevelMenu[]>([]);
|
||||||
|
|
||||||
// 로그 뷰어 상태
|
// 로그 뷰어 상태
|
||||||
const [logViewerOpen, setLogViewerOpen] = useState(false);
|
const [logViewerOpen, setLogViewerOpen] = useState(false);
|
||||||
const [logViewerTableName, setLogViewerTableName] = useState<string>("");
|
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 () => {
|
const loadTables = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -257,10 +285,17 @@ export default function TableManagementPage() {
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
const data = response.data.data;
|
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) => ({
|
const processedColumns = (data.columns || data).map((col: any) => ({
|
||||||
...col,
|
...col,
|
||||||
inputType: col.inputType || "text", // 기본값: text
|
inputType: col.inputType || "text", // 기본값: text
|
||||||
|
categoryMenus: col.categoryMenus || [], // 카테고리 메뉴 매핑 정보
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (page === 1) {
|
if (page === 1) {
|
||||||
|
|
@ -438,12 +473,72 @@ export default function TableManagementPage() {
|
||||||
|
|
||||||
// console.log("저장할 컬럼 설정:", columnSetting);
|
// 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`, [
|
const response = await apiClient.post(`/table-management/tables/${selectedTable}/columns/settings`, [
|
||||||
columnSetting,
|
columnSetting,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.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("컬럼 설정이 성공적으로 저장되었습니다.");
|
toast.success("컬럼 설정이 성공적으로 저장되었습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
// 원본 데이터 업데이트
|
// 원본 데이터 업데이트
|
||||||
setOriginalColumns((prev) => prev.map((col) => (col.columnName === column.columnName ? column : col)));
|
setOriginalColumns((prev) => prev.map((col) => (col.columnName === column.columnName ? column : col)));
|
||||||
|
|
||||||
|
|
@ -501,14 +596,78 @@ export default function TableManagementPage() {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.data.success) {
|
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]);
|
setOriginalColumns([...columns]);
|
||||||
toast.success(`테이블 '${selectedTable}' 설정이 모두 저장되었습니다.`);
|
|
||||||
|
|
||||||
// 테이블 목록 새로고침 (라벨 변경 반영)
|
// 테이블 목록 새로고침 (라벨 변경 반영)
|
||||||
loadTables();
|
loadTables();
|
||||||
|
|
||||||
// 저장 후 데이터 확인을 위해 다시 로드
|
// 저장 후 데이터 다시 로드
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loadColumnTypes(selectedTable, 1, pageSize);
|
loadColumnTypes(selectedTable, 1, pageSize);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
@ -539,6 +698,7 @@ export default function TableManagementPage() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadTables();
|
loadTables();
|
||||||
loadCommonCodeCategories();
|
loadCommonCodeCategories();
|
||||||
|
loadSecondLevelMenus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 🎯 컬럼 로드 후 이미 설정된 참조 테이블들의 컬럼 정보 로드
|
// 🎯 컬럼 로드 후 이미 설정된 참조 테이블들의 컬럼 정보 로드
|
||||||
|
|
@ -1023,10 +1183,61 @@ export default function TableManagementPage() {
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
{/* 입력 타입이 'category'인 경우 안내 메시지 */}
|
{/* 입력 타입이 'category'인 경우 2레벨 메뉴 다중 선택 */}
|
||||||
{column.inputType === "category" && (
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 입력 타입이 'entity'인 경우 참조 테이블 선택 */}
|
{/* 입력 타입이 'entity'인 경우 참조 테이블 선택 */}
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
menuUrl: screenUrl,
|
menuUrl: screenUrl,
|
||||||
|
screenCode: screen.screenCode, // 화면 코드도 함께 저장
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// console.log("🖥️ 화면 선택 완료:", {
|
// console.log("🖥️ 화면 선택 완료:", {
|
||||||
|
|
@ -207,10 +208,11 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
if (type === "direct") {
|
if (type === "direct") {
|
||||||
// 직접 입력 모드로 변경 시 선택된 화면 초기화
|
// 직접 입력 모드로 변경 시 선택된 화면 초기화
|
||||||
setSelectedScreen(null);
|
setSelectedScreen(null);
|
||||||
// URL 필드도 초기화 (사용자가 직접 입력할 수 있도록)
|
// URL 필드와 screenCode 초기화 (사용자가 직접 입력할 수 있도록)
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
menuUrl: "",
|
menuUrl: "",
|
||||||
|
screenCode: undefined, // 화면 코드도 함께 초기화
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
// 화면 할당 모드로 변경 시
|
// 화면 할당 모드로 변경 시
|
||||||
|
|
@ -230,12 +232,14 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
menuUrl: screenUrl,
|
menuUrl: screenUrl,
|
||||||
|
screenCode: selectedScreen.screenCode, // 화면 코드도 함께 유지
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
// 선택된 화면이 없으면 URL만 초기화
|
// 선택된 화면이 없으면 URL과 screenCode 초기화
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
menuUrl: "",
|
menuUrl: "",
|
||||||
|
screenCode: undefined,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -575,6 +575,8 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
console.log("🔍 데이터 조회 시작:", { tableName: component.tableName, page, pageSize });
|
||||||
|
|
||||||
const result = await tableTypeApi.getTableData(component.tableName, {
|
const result = await tableTypeApi.getTableData(component.tableName, {
|
||||||
page,
|
page,
|
||||||
size: pageSize,
|
size: pageSize,
|
||||||
|
|
@ -582,6 +584,13 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
autoFilter: component.autoFilter, // 🆕 자동 필터 설정 전달
|
autoFilter: component.autoFilter, // 🆕 자동 필터 설정 전달
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("✅ 데이터 조회 완료:", {
|
||||||
|
tableName: component.tableName,
|
||||||
|
dataLength: result.data.length,
|
||||||
|
total: result.total,
|
||||||
|
page: result.page
|
||||||
|
});
|
||||||
|
|
||||||
setData(result.data);
|
setData(result.data);
|
||||||
setTotal(result.total);
|
setTotal(result.total);
|
||||||
setTotalPages(result.totalPages);
|
setTotalPages(result.totalPages);
|
||||||
|
|
@ -1952,7 +1961,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
// 실제 웹 타입으로 스위치 (input_type="category"도 포함됨)
|
// 실제 웹 타입으로 스위치 (input_type="category"도 포함됨)
|
||||||
switch (actualWebType) {
|
switch (actualWebType) {
|
||||||
case "category": {
|
case "category": {
|
||||||
// 카테고리 타입: 배지로 표시
|
// 카테고리 타입: 배지로 표시 (배지 없음 옵션 지원)
|
||||||
if (!value) return "";
|
if (!value) return "";
|
||||||
|
|
||||||
const mapping = categoryMappings[column.columnName];
|
const mapping = categoryMappings[column.columnName];
|
||||||
|
|
@ -1962,6 +1971,11 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
const displayLabel = categoryData?.label || String(value);
|
const displayLabel = categoryData?.label || String(value);
|
||||||
const displayColor = categoryData?.color || "#64748b"; // 기본 slate 색상
|
const displayColor = categoryData?.color || "#64748b"; // 기본 slate 색상
|
||||||
|
|
||||||
|
// 배지 없음 옵션: color가 "none"이면 텍스트만 표시
|
||||||
|
if (displayColor === "none") {
|
||||||
|
return <span className="text-sm">{displayLabel}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -21,15 +21,22 @@ import {
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { createColumnMapping } from "@/lib/api/tableCategoryValue";
|
import { createColumnMapping } from "@/lib/api/tableCategoryValue";
|
||||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
|
interface SecondLevelMenu {
|
||||||
|
menuObjid: number;
|
||||||
|
menuName: string;
|
||||||
|
parentMenuName: string;
|
||||||
|
screenCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface AddCategoryColumnDialogProps {
|
interface AddCategoryColumnDialogProps {
|
||||||
tableName: string;
|
tableName: string;
|
||||||
menuObjid: number;
|
|
||||||
menuName: string;
|
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,27 +44,31 @@ interface AddCategoryColumnDialogProps {
|
||||||
* 카테고리 컬럼 추가 다이얼로그
|
* 카테고리 컬럼 추가 다이얼로그
|
||||||
*
|
*
|
||||||
* 논리적 컬럼명과 물리적 컬럼명을 매핑하여 메뉴별로 독립적인 카테고리 관리 가능
|
* 논리적 컬럼명과 물리적 컬럼명을 매핑하여 메뉴별로 독립적인 카테고리 관리 가능
|
||||||
|
*
|
||||||
|
* 2레벨 메뉴를 선택하면 해당 메뉴의 모든 하위 메뉴에서 사용 가능
|
||||||
*/
|
*/
|
||||||
export function AddCategoryColumnDialog({
|
export function AddCategoryColumnDialog({
|
||||||
tableName,
|
tableName,
|
||||||
menuObjid,
|
|
||||||
menuName,
|
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}: AddCategoryColumnDialogProps) {
|
}: AddCategoryColumnDialogProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [physicalColumns, setPhysicalColumns] = useState<string[]>([]);
|
const [physicalColumns, setPhysicalColumns] = useState<string[]>([]);
|
||||||
|
const [secondLevelMenus, setSecondLevelMenus] = useState<SecondLevelMenu[]>([]);
|
||||||
|
const [selectedMenus, setSelectedMenus] = useState<number[]>([]);
|
||||||
const [logicalColumnName, setLogicalColumnName] = useState("");
|
const [logicalColumnName, setLogicalColumnName] = useState("");
|
||||||
const [physicalColumnName, setPhysicalColumnName] = useState("");
|
const [physicalColumnName, setPhysicalColumnName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
|
|
||||||
// 테이블의 실제 컬럼 목록 조회
|
// 다이얼로그 열릴 때 데이터 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
loadPhysicalColumns();
|
loadPhysicalColumns();
|
||||||
|
loadSecondLevelMenus();
|
||||||
}
|
}
|
||||||
}, [open, tableName]);
|
}, [open, tableName]);
|
||||||
|
|
||||||
|
// 테이블의 실제 컬럼 목록 조회
|
||||||
const loadPhysicalColumns = async () => {
|
const loadPhysicalColumns = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await tableManagementApi.getTableColumns(tableName);
|
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 () => {
|
const handleSave = async () => {
|
||||||
// 입력 검증
|
// 입력 검증
|
||||||
if (!logicalColumnName.trim()) {
|
if (!logicalColumnName.trim()) {
|
||||||
|
|
@ -82,24 +119,42 @@ export function AddCategoryColumnDialog({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedMenus.length === 0) {
|
||||||
|
toast.error("최소 하나 이상의 메뉴를 선택해주세요");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await createColumnMapping({
|
// 선택된 각 메뉴에 대해 매핑 생성
|
||||||
|
const promises = selectedMenus.map((menuObjid) =>
|
||||||
|
createColumnMapping({
|
||||||
tableName,
|
tableName,
|
||||||
logicalColumnName: logicalColumnName.trim(),
|
logicalColumnName: logicalColumnName.trim(),
|
||||||
physicalColumnName,
|
physicalColumnName,
|
||||||
menuObjid,
|
menuObjid,
|
||||||
description: description.trim() || undefined,
|
description: description.trim() || undefined,
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (response.success) {
|
const results = await Promise.all(promises);
|
||||||
toast.success("논리적 컬럼이 추가되었습니다");
|
|
||||||
|
// 모든 요청이 성공했는지 확인
|
||||||
|
const failedCount = results.filter((r) => !r.success).length;
|
||||||
|
|
||||||
|
if (failedCount === 0) {
|
||||||
|
toast.success(`논리적 컬럼이 ${selectedMenus.length}개 메뉴에 추가되었습니다`);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
resetForm();
|
resetForm();
|
||||||
onSuccess();
|
onSuccess();
|
||||||
|
} else if (failedCount < results.length) {
|
||||||
|
toast.warning(
|
||||||
|
`${results.length - failedCount}개 메뉴에 추가 성공, ${failedCount}개 실패`
|
||||||
|
);
|
||||||
|
onSuccess();
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.error || "컬럼 매핑 생성에 실패했습니다");
|
toast.error("모든 메뉴에 대한 매핑 생성에 실패했습니다");
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("컬럼 매핑 생성 실패:", error);
|
console.error("컬럼 매핑 생성 실패:", error);
|
||||||
|
|
@ -113,6 +168,7 @@ export function AddCategoryColumnDialog({
|
||||||
setLogicalColumnName("");
|
setLogicalColumnName("");
|
||||||
setPhysicalColumnName("");
|
setPhysicalColumnName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
|
setSelectedMenus([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -130,21 +186,11 @@ export function AddCategoryColumnDialog({
|
||||||
카테고리 컬럼 추가
|
카테고리 컬럼 추가
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-xs sm:text-sm">
|
<DialogDescription className="text-xs sm:text-sm">
|
||||||
같은 물리적 컬럼을 여러 메뉴에서 다른 카테고리로 사용할 수 있습니다
|
2레벨 메뉴를 선택하면 해당 메뉴의 모든 하위 메뉴에서 사용할 수 있습니다
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-3 sm:space-y-4">
|
<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>
|
<div>
|
||||||
<Label className="text-xs sm:text-sm">
|
<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"
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||||
이 메뉴에서만 사용할 고유한 이름을 입력하세요
|
선택한 메뉴들에서 사용할 고유한 이름을 입력하세요
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<Label className="text-xs sm:text-sm">설명</Label>
|
<Label className="text-xs sm:text-sm">설명</Label>
|
||||||
|
|
@ -207,7 +290,7 @@ export function AddCategoryColumnDialog({
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSave}
|
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"
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||||
>
|
>
|
||||||
{loading ? "추가 중..." : "추가"}
|
{loading ? "추가 중..." : "추가"}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export const CategoryValueAddDialog: React.FC<
|
||||||
> = ({ open, onOpenChange, onAdd, columnLabel }) => {
|
> = ({ open, onOpenChange, onAdd, columnLabel }) => {
|
||||||
const [valueLabel, setValueLabel] = useState("");
|
const [valueLabel, setValueLabel] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [color, setColor] = useState("#3b82f6");
|
const [color, setColor] = useState("none");
|
||||||
|
|
||||||
// 라벨에서 코드 자동 생성
|
// 라벨에서 코드 자동 생성
|
||||||
const generateCode = (label: string): string => {
|
const generateCode = (label: string): string => {
|
||||||
|
|
@ -91,7 +91,7 @@ export const CategoryValueAddDialog: React.FC<
|
||||||
// 초기화
|
// 초기화
|
||||||
setValueLabel("");
|
setValueLabel("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
setColor("#3b82f6");
|
setColor("none");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -123,7 +123,8 @@ export const CategoryValueAddDialog: React.FC<
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs sm:text-sm">배지 색상</Label>
|
<Label className="text-xs sm:text-sm">배지 색상</Label>
|
||||||
<div className="mt-1.5 flex items-center gap-3">
|
<div className="mt-1.5 space-y-2">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
<div className="grid grid-cols-9 gap-2">
|
<div className="grid grid-cols-9 gap-2">
|
||||||
{DEFAULT_COLORS.map((c) => (
|
{DEFAULT_COLORS.map((c) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -138,9 +139,25 @@ export const CategoryValueAddDialog: React.FC<
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{color && color !== "none" ? (
|
||||||
<Badge style={{ backgroundColor: color, borderColor: color }} className="text-white">
|
<Badge style={{ backgroundColor: color, borderColor: color }} className="text-white">
|
||||||
미리보기
|
미리보기
|
||||||
</Badge>
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">배지 없음</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,12 +51,12 @@ export const CategoryValueEditDialog: React.FC<
|
||||||
> = ({ open, onOpenChange, value, onUpdate, columnLabel }) => {
|
> = ({ open, onOpenChange, value, onUpdate, columnLabel }) => {
|
||||||
const [valueLabel, setValueLabel] = useState(value.valueLabel);
|
const [valueLabel, setValueLabel] = useState(value.valueLabel);
|
||||||
const [description, setDescription] = useState(value.description || "");
|
const [description, setDescription] = useState(value.description || "");
|
||||||
const [color, setColor] = useState(value.color || "#3b82f6");
|
const [color, setColor] = useState(value.color || "none");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValueLabel(value.valueLabel);
|
setValueLabel(value.valueLabel);
|
||||||
setDescription(value.description || "");
|
setDescription(value.description || "");
|
||||||
setColor(value.color || "#3b82f6");
|
setColor(value.color || "none");
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
|
|
@ -100,7 +100,8 @@ export const CategoryValueEditDialog: React.FC<
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs sm:text-sm">배지 색상</Label>
|
<Label className="text-xs sm:text-sm">배지 색상</Label>
|
||||||
<div className="mt-1.5 flex items-center gap-3">
|
<div className="mt-1.5 space-y-2">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
<div className="grid grid-cols-9 gap-2">
|
<div className="grid grid-cols-9 gap-2">
|
||||||
{DEFAULT_COLORS.map((c) => (
|
{DEFAULT_COLORS.map((c) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -115,9 +116,25 @@ export const CategoryValueEditDialog: React.FC<
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{color && color !== "none" ? (
|
||||||
<Badge style={{ backgroundColor: color, borderColor: color }} className="text-white">
|
<Badge style={{ backgroundColor: color, borderColor: color }} className="text-white">
|
||||||
미리보기
|
미리보기
|
||||||
</Badge>
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">배지 없음</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -349,13 +349,18 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-1 items-center gap-2">
|
<div className="flex flex-1 items-center gap-2">
|
||||||
{/* 색상 표시 (앞쪽으로 이동) */}
|
{/* 색상 표시 (배지 없음 옵션 지원) */}
|
||||||
{value.color && (
|
{value.color && value.color !== "none" && (
|
||||||
<div
|
<div
|
||||||
className="h-4 w-4 rounded-full border flex-shrink-0"
|
className="h-4 w-4 rounded-full border flex-shrink-0"
|
||||||
style={{ backgroundColor: value.color }}
|
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" : ""}`}>
|
<span className={`text-sm font-medium ${isInactive ? "line-through" : ""}`}>
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ export interface MenuFormData {
|
||||||
status: string;
|
status: string;
|
||||||
companyCode: string;
|
companyCode: string;
|
||||||
langKey?: string; // 다국어 키 추가
|
langKey?: string; // 다국어 키 추가
|
||||||
|
screenCode?: string; // 화면 코드 추가
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LangKey {
|
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 () => {
|
const fetchTableDataInternal = useCallback(async () => {
|
||||||
|
console.log("📡 [TableList] fetchTableDataInternal 호출됨", {
|
||||||
|
tableName: tableConfig.selectedTable,
|
||||||
|
isDesignMode,
|
||||||
|
currentPage,
|
||||||
|
});
|
||||||
|
|
||||||
if (!tableConfig.selectedTable || isDesignMode) {
|
if (!tableConfig.selectedTable || isDesignMode) {
|
||||||
setData([]);
|
setData([]);
|
||||||
setTotalPages(0);
|
setTotalPages(0);
|
||||||
|
|
@ -809,11 +815,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 테이블명 확인 로그 (개발 시에만)
|
|
||||||
// console.log("🔍 fetchTableDataInternal - selectedTable:", tableConfig.selectedTable);
|
|
||||||
// console.log("🔍 selectedTable 타입:", typeof tableConfig.selectedTable);
|
|
||||||
// console.log("🔍 전체 tableConfig:", tableConfig);
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
|
@ -834,6 +835,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
referenceTable: col.additionalJoinInfo!.referenceTable,
|
referenceTable: col.additionalJoinInfo!.referenceTable,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
console.log("🔍 [TableList] API 호출 시작", {
|
||||||
|
tableName: tableConfig.selectedTable,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
});
|
||||||
|
|
||||||
// 🎯 항상 entityJoinApi 사용 (writer 컬럼 자동 조인 지원)
|
// 🎯 항상 entityJoinApi 사용 (writer 컬럼 자동 조인 지원)
|
||||||
const response = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, {
|
const response = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, {
|
||||||
page,
|
page,
|
||||||
|
|
@ -845,6 +854,17 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
additionalJoinColumns: entityJoinColumns.length > 0 ? entityJoinColumns : undefined,
|
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 || []);
|
setData(response.data || []);
|
||||||
setTotalPages(response.totalPages || 0);
|
setTotalPages(response.totalPages || 0);
|
||||||
setTotalItems(response.total || 0);
|
setTotalItems(response.total || 0);
|
||||||
|
|
@ -1354,28 +1374,22 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 카테고리 타입: 배지로 표시
|
// 카테고리 타입: 배지로 표시 (배지 없음 옵션 지원)
|
||||||
if (inputType === "category") {
|
if (inputType === "category") {
|
||||||
if (!value) return "";
|
if (!value) return "";
|
||||||
|
|
||||||
const mapping = categoryMappings[column.columnName];
|
const mapping = categoryMappings[column.columnName];
|
||||||
const categoryData = mapping?.[String(value)];
|
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 displayLabel = categoryData?.label || String(value);
|
||||||
const displayColor = categoryData?.color || "#64748b"; // 기본 slate 색상
|
const displayColor = categoryData?.color || "#64748b"; // 기본 slate 색상
|
||||||
|
|
||||||
|
// 배지 없음 옵션: color가 "none"이면 텍스트만 표시
|
||||||
|
if (displayColor === "none") {
|
||||||
|
return <span className="text-sm">{displayLabel}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
const { Badge } = require("@/components/ui/badge");
|
const { Badge } = require("@/components/ui/badge");
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
|
|
@ -1716,6 +1730,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
}, [tableConfig.selectedTable, fetchColumnLabels, fetchTableLabel]);
|
}, [tableConfig.selectedTable, fetchColumnLabels, fetchTableLabel]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log("🔍 [TableList] useEffect 실행 - 데이터 조회 트리거", {
|
||||||
|
isDesignMode,
|
||||||
|
tableName: tableConfig.selectedTable,
|
||||||
|
currentPage,
|
||||||
|
sortColumn,
|
||||||
|
sortDirection,
|
||||||
|
});
|
||||||
|
|
||||||
if (!isDesignMode && tableConfig.selectedTable) {
|
if (!isDesignMode && tableConfig.selectedTable) {
|
||||||
fetchTableDataDebounced();
|
fetchTableDataDebounced();
|
||||||
}
|
}
|
||||||
|
|
@ -1730,7 +1752,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
refreshKey,
|
refreshKey,
|
||||||
refreshTrigger, // 강제 새로고침 트리거
|
refreshTrigger, // 강제 새로고침 트리거
|
||||||
isDesignMode,
|
isDesignMode,
|
||||||
fetchTableDataDebounced,
|
// fetchTableDataDebounced 제거: useCallback 재생성으로 인한 무한 루프 방지
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -2157,9 +2179,18 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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) => {
|
groupedData.map((group) => {
|
||||||
|
console.log("📊 [TableList] 그룹 렌더링:", group.groupKey, group.count);
|
||||||
const isCollapsed = collapsedGroups.has(group.groupKey);
|
const isCollapsed = collapsedGroups.has(group.groupKey);
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={group.groupKey}>
|
<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
|
<tr
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue