Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary, ; especially if it merges an updated upstream into a topic branch. ; ; Lines starting with ';' will be ignored, and an empty message aborts ; the commit.
This commit is contained in:
commit
0382c94d73
|
|
@ -1418,69 +1418,31 @@ export async function updateMenu(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 삭제
|
* 재귀적으로 모든 하위 메뉴 ID를 수집하는 헬퍼 함수
|
||||||
*/
|
*/
|
||||||
export async function deleteMenu(
|
async function collectAllChildMenuIds(parentObjid: number): Promise<number[]> {
|
||||||
req: AuthenticatedRequest,
|
const allIds: number[] = [];
|
||||||
res: Response
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
const { menuId } = req.params;
|
|
||||||
logger.info(`메뉴 삭제 요청: menuId = ${menuId}`, { user: req.user });
|
|
||||||
|
|
||||||
// 사용자의 company_code 확인
|
// 직접 자식 메뉴들 조회
|
||||||
if (!req.user?.companyCode) {
|
const children = await query<any>(
|
||||||
res.status(400).json({
|
`SELECT objid FROM menu_info WHERE parent_obj_id = $1`,
|
||||||
success: false,
|
[parentObjid]
|
||||||
message: "사용자의 회사 코드를 찾을 수 없습니다.",
|
|
||||||
error: "Missing company_code",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userCompanyCode = req.user.companyCode;
|
|
||||||
const userType = req.user.userType;
|
|
||||||
|
|
||||||
// 삭제하려는 메뉴 조회
|
|
||||||
const currentMenu = await queryOne<any>(
|
|
||||||
`SELECT objid, company_code FROM menu_info WHERE objid = $1`,
|
|
||||||
[Number(menuId)]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!currentMenu) {
|
for (const child of children) {
|
||||||
res.status(404).json({
|
allIds.push(child.objid);
|
||||||
success: false,
|
// 자식의 자식들도 재귀적으로 수집
|
||||||
message: `메뉴를 찾을 수 없습니다: ${menuId}`,
|
const grandChildren = await collectAllChildMenuIds(child.objid);
|
||||||
error: "Menu not found",
|
allIds.push(...grandChildren);
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 공통 메뉴(company_code = '*')는 최고 관리자만 삭제 가능
|
return allIds;
|
||||||
if (currentMenu.company_code === "*") {
|
|
||||||
if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") {
|
|
||||||
res.status(403).json({
|
|
||||||
success: false,
|
|
||||||
message: "공통 메뉴는 최고 관리자만 삭제할 수 있습니다.",
|
|
||||||
error: "Unauthorized to delete common menu",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (userCompanyCode !== "*") {
|
|
||||||
// 회사 관리자는 자기 회사 메뉴만 삭제 가능
|
|
||||||
if (currentMenu.company_code !== userCompanyCode) {
|
|
||||||
res.status(403).json({
|
|
||||||
success: false,
|
|
||||||
message: "해당 회사의 메뉴를 삭제할 권한이 없습니다.",
|
|
||||||
error: "Unauthorized to delete menu for this company",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 외래키 제약 조건이 있는 관련 테이블 데이터 먼저 정리
|
/**
|
||||||
const menuObjid = Number(menuId);
|
* 메뉴 및 관련 데이터 정리 헬퍼 함수
|
||||||
|
*/
|
||||||
|
async function cleanupMenuRelatedData(menuObjid: number): Promise<void> {
|
||||||
// 1. category_column_mapping에서 menu_objid를 NULL로 설정
|
// 1. category_column_mapping에서 menu_objid를 NULL로 설정
|
||||||
await query(
|
await query(
|
||||||
`UPDATE category_column_mapping SET menu_objid = NULL WHERE menu_objid = $1`,
|
`UPDATE category_column_mapping SET menu_objid = NULL WHERE menu_objid = $1`,
|
||||||
|
|
@ -1517,28 +1479,118 @@ export async function deleteMenu(
|
||||||
[menuObjid]
|
[menuObjid]
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info("메뉴 관련 데이터 정리 완료", { menuObjid });
|
// 7. screen_groups에서 menu_objid를 NULL로 설정
|
||||||
|
await query(
|
||||||
// Raw Query를 사용한 메뉴 삭제
|
`UPDATE screen_groups SET menu_objid = NULL WHERE menu_objid = $1`,
|
||||||
const [deletedMenu] = await query<any>(
|
|
||||||
`DELETE FROM menu_info WHERE objid = $1 RETURNING *`,
|
|
||||||
[menuObjid]
|
[menuObjid]
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("메뉴 삭제 성공", { deletedMenu });
|
/**
|
||||||
|
* 메뉴 삭제
|
||||||
|
*/
|
||||||
|
export async function deleteMenu(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { menuId } = req.params;
|
||||||
|
logger.info(`메뉴 삭제 요청: menuId = ${menuId}`, { user: req.user });
|
||||||
|
|
||||||
|
// 사용자의 company_code 확인
|
||||||
|
if (!req.user?.companyCode) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "사용자의 회사 코드를 찾을 수 없습니다.",
|
||||||
|
error: "Missing company_code",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userCompanyCode = req.user.companyCode;
|
||||||
|
const userType = req.user.userType;
|
||||||
|
|
||||||
|
// 삭제하려는 메뉴 조회
|
||||||
|
const currentMenu = await queryOne<any>(
|
||||||
|
`SELECT objid, company_code, menu_name_kor FROM menu_info WHERE objid = $1`,
|
||||||
|
[Number(menuId)]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!currentMenu) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: `메뉴를 찾을 수 없습니다: ${menuId}`,
|
||||||
|
error: "Menu not found",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 공통 메뉴(company_code = '*')는 최고 관리자만 삭제 가능
|
||||||
|
if (currentMenu.company_code === "*") {
|
||||||
|
if (userCompanyCode !== "*" || userType !== "SUPER_ADMIN") {
|
||||||
|
res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: "공통 메뉴는 최고 관리자만 삭제할 수 있습니다.",
|
||||||
|
error: "Unauthorized to delete common menu",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (userCompanyCode !== "*") {
|
||||||
|
// 회사 관리자는 자기 회사 메뉴만 삭제 가능
|
||||||
|
if (currentMenu.company_code !== userCompanyCode) {
|
||||||
|
res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: "해당 회사의 메뉴를 삭제할 권한이 없습니다.",
|
||||||
|
error: "Unauthorized to delete menu for this company",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuObjid = Number(menuId);
|
||||||
|
|
||||||
|
// 하위 메뉴들 재귀적으로 수집
|
||||||
|
const childMenuIds = await collectAllChildMenuIds(menuObjid);
|
||||||
|
const allMenuIdsToDelete = [menuObjid, ...childMenuIds];
|
||||||
|
|
||||||
|
logger.info(`메뉴 삭제 대상: 본인(${menuObjid}) + 하위 메뉴 ${childMenuIds.length}개`, {
|
||||||
|
menuName: currentMenu.menu_name_kor,
|
||||||
|
totalCount: allMenuIdsToDelete.length,
|
||||||
|
childMenuIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 모든 삭제 대상 메뉴에 대해 관련 데이터 정리
|
||||||
|
for (const objid of allMenuIdsToDelete) {
|
||||||
|
await cleanupMenuRelatedData(objid);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("메뉴 관련 데이터 정리 완료", {
|
||||||
|
menuObjid,
|
||||||
|
totalCleaned: allMenuIdsToDelete.length
|
||||||
|
});
|
||||||
|
|
||||||
|
// 하위 메뉴부터 역순으로 삭제 (외래키 제약 회피)
|
||||||
|
// 가장 깊은 하위부터 삭제해야 하므로 역순으로
|
||||||
|
const reversedIds = [...allMenuIdsToDelete].reverse();
|
||||||
|
|
||||||
|
for (const objid of reversedIds) {
|
||||||
|
await query(`DELETE FROM menu_info WHERE objid = $1`, [objid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("메뉴 삭제 성공", {
|
||||||
|
deletedMenuObjid: menuObjid,
|
||||||
|
deletedMenuName: currentMenu.menu_name_kor,
|
||||||
|
totalDeleted: allMenuIdsToDelete.length,
|
||||||
|
});
|
||||||
|
|
||||||
const response: ApiResponse<any> = {
|
const response: ApiResponse<any> = {
|
||||||
success: true,
|
success: true,
|
||||||
message: "메뉴가 성공적으로 삭제되었습니다.",
|
message: `메뉴가 성공적으로 삭제되었습니다. (하위 메뉴 ${childMenuIds.length}개 포함)`,
|
||||||
data: {
|
data: {
|
||||||
objid: deletedMenu.objid.toString(),
|
objid: menuObjid.toString(),
|
||||||
menuNameKor: deletedMenu.menu_name_kor,
|
menuNameKor: currentMenu.menu_name_kor,
|
||||||
menuNameEng: deletedMenu.menu_name_eng,
|
deletedCount: allMenuIdsToDelete.length,
|
||||||
menuUrl: deletedMenu.menu_url,
|
deletedChildCount: childMenuIds.length,
|
||||||
menuDesc: deletedMenu.menu_desc,
|
|
||||||
status: deletedMenu.status,
|
|
||||||
writer: deletedMenu.writer,
|
|
||||||
regdate: new Date(deletedMenu.regdate).toISOString(),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1623,18 +1675,49 @@ export async function deleteMenusBatch(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 모든 삭제 대상 메뉴 ID 수집 (하위 메뉴 포함)
|
||||||
|
const allMenuIdsToDelete = new Set<number>();
|
||||||
|
|
||||||
|
for (const menuId of menuIds) {
|
||||||
|
const objid = Number(menuId);
|
||||||
|
allMenuIdsToDelete.add(objid);
|
||||||
|
|
||||||
|
// 하위 메뉴들 재귀적으로 수집
|
||||||
|
const childMenuIds = await collectAllChildMenuIds(objid);
|
||||||
|
childMenuIds.forEach(id => allMenuIdsToDelete.add(Number(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const allIdsArray = Array.from(allMenuIdsToDelete);
|
||||||
|
|
||||||
|
logger.info(`메뉴 일괄 삭제 대상: 선택 ${menuIds.length}개 + 하위 메뉴 포함 총 ${allIdsArray.length}개`, {
|
||||||
|
selectedMenuIds: menuIds,
|
||||||
|
totalWithChildren: allIdsArray.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 모든 삭제 대상 메뉴에 대해 관련 데이터 정리
|
||||||
|
for (const objid of allIdsArray) {
|
||||||
|
await cleanupMenuRelatedData(objid);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("메뉴 관련 데이터 정리 완료", {
|
||||||
|
totalCleaned: allIdsArray.length
|
||||||
|
});
|
||||||
|
|
||||||
// Raw Query를 사용한 메뉴 일괄 삭제
|
// Raw Query를 사용한 메뉴 일괄 삭제
|
||||||
let deletedCount = 0;
|
let deletedCount = 0;
|
||||||
let failedCount = 0;
|
let failedCount = 0;
|
||||||
const deletedMenus: any[] = [];
|
const deletedMenus: any[] = [];
|
||||||
const failedMenuIds: string[] = [];
|
const failedMenuIds: string[] = [];
|
||||||
|
|
||||||
|
// 하위 메뉴부터 삭제하기 위해 역순으로 정렬
|
||||||
|
const reversedIds = [...allIdsArray].reverse();
|
||||||
|
|
||||||
// 각 메뉴 ID에 대해 삭제 시도
|
// 각 메뉴 ID에 대해 삭제 시도
|
||||||
for (const menuId of menuIds) {
|
for (const menuObjid of reversedIds) {
|
||||||
try {
|
try {
|
||||||
const result = await query<any>(
|
const result = await query<any>(
|
||||||
`DELETE FROM menu_info WHERE objid = $1 RETURNING *`,
|
`DELETE FROM menu_info WHERE objid = $1 RETURNING *`,
|
||||||
[Number(menuId)]
|
[menuObjid]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
|
|
@ -1645,20 +1728,20 @@ export async function deleteMenusBatch(
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
failedCount++;
|
failedCount++;
|
||||||
failedMenuIds.push(menuId);
|
failedMenuIds.push(String(menuObjid));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`메뉴 삭제 실패 (ID: ${menuId}):`, error);
|
logger.error(`메뉴 삭제 실패 (ID: ${menuObjid}):`, error);
|
||||||
failedCount++;
|
failedCount++;
|
||||||
failedMenuIds.push(menuId);
|
failedMenuIds.push(String(menuObjid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("메뉴 일괄 삭제 완료", {
|
logger.info("메뉴 일괄 삭제 완료", {
|
||||||
total: menuIds.length,
|
requested: menuIds.length,
|
||||||
|
totalWithChildren: allIdsArray.length,
|
||||||
deletedCount,
|
deletedCount,
|
||||||
failedCount,
|
failedCount,
|
||||||
deletedMenus,
|
|
||||||
failedMenuIds,
|
failedMenuIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2090,7 +2090,7 @@ export class MenuCopyService {
|
||||||
menu.menu_url,
|
menu.menu_url,
|
||||||
menu.menu_desc,
|
menu.menu_desc,
|
||||||
userId,
|
userId,
|
||||||
menu.status,
|
'active', // 복제된 메뉴는 항상 활성화 상태
|
||||||
menu.system_name,
|
menu.system_name,
|
||||||
targetCompanyCode, // 새 회사 코드
|
targetCompanyCode, // 새 회사 코드
|
||||||
menu.lang_key,
|
menu.lang_key,
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ export async function syncScreenGroupsToMenu(
|
||||||
const newObjid = Date.now();
|
const newObjid = Date.now();
|
||||||
const createRootQuery = `
|
const createRootQuery = `
|
||||||
INSERT INTO menu_info (objid, parent_obj_id, menu_name_kor, menu_name_eng, seq, menu_type, company_code, writer, regdate, status)
|
INSERT INTO menu_info (objid, parent_obj_id, menu_name_kor, menu_name_eng, seq, menu_type, company_code, writer, regdate, status)
|
||||||
VALUES ($1, 0, '사용자', 'User', 1, 1, $2, $3, NOW(), 'Y')
|
VALUES ($1, 0, '사용자', 'User', 1, 1, $2, $3, NOW(), 'active')
|
||||||
RETURNING objid
|
RETURNING objid
|
||||||
`;
|
`;
|
||||||
const createRootResult = await client.query(createRootQuery, [newObjid, companyCode, userId]);
|
const createRootResult = await client.query(createRootQuery, [newObjid, companyCode, userId]);
|
||||||
|
|
@ -159,12 +159,36 @@ export async function syncScreenGroupsToMenu(
|
||||||
groupIdToName.set(g.id, g.group_name?.trim().toLowerCase() || '');
|
groupIdToName.set(g.id, g.group_name?.trim().toLowerCase() || '');
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. 각 screen_group 처리
|
// 5. 최상위 회사 폴더 ID 찾기 (level 0, parent_group_id IS NULL)
|
||||||
|
// 이 폴더는 메뉴로 생성하지 않고, 하위 폴더들을 사용자 루트 바로 아래에 배치
|
||||||
|
const topLevelCompanyFolderIds = new Set<number>();
|
||||||
|
for (const group of screenGroupsResult.rows) {
|
||||||
|
if (group.group_level === 0 && group.parent_group_id === null) {
|
||||||
|
topLevelCompanyFolderIds.add(group.id);
|
||||||
|
// 최상위 폴더 → 사용자 루트에 매핑 (하위 폴더의 부모로 사용)
|
||||||
|
groupToMenuMap.set(group.id, userMenuRootObjid!);
|
||||||
|
logger.info("최상위 회사 폴더 스킵", { groupId: group.id, groupName: group.group_name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 각 screen_group 처리
|
||||||
for (const group of screenGroupsResult.rows) {
|
for (const group of screenGroupsResult.rows) {
|
||||||
const groupId = group.id;
|
const groupId = group.id;
|
||||||
const groupName = group.group_name?.trim();
|
const groupName = group.group_name?.trim();
|
||||||
const groupNameLower = groupName?.toLowerCase() || '';
|
const groupNameLower = groupName?.toLowerCase() || '';
|
||||||
|
|
||||||
|
// 최상위 회사 폴더는 메뉴로 생성하지 않고 스킵
|
||||||
|
if (topLevelCompanyFolderIds.has(groupId)) {
|
||||||
|
result.skipped++;
|
||||||
|
result.details.push({
|
||||||
|
action: 'skipped',
|
||||||
|
sourceName: groupName,
|
||||||
|
sourceId: groupId,
|
||||||
|
reason: '최상위 회사 폴더 (메뉴 생성 스킵)',
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// 이미 연결된 경우 - 실제로 메뉴가 존재하는지 확인
|
// 이미 연결된 경우 - 실제로 메뉴가 존재하는지 확인
|
||||||
if (group.menu_objid) {
|
if (group.menu_objid) {
|
||||||
const menuExists = existingMenuObjids.has(Number(group.menu_objid));
|
const menuExists = existingMenuObjids.has(Number(group.menu_objid));
|
||||||
|
|
@ -237,11 +261,17 @@ export async function syncScreenGroupsToMenu(
|
||||||
const newObjid = Date.now() + groupId; // 고유 ID 보장
|
const newObjid = Date.now() + groupId; // 고유 ID 보장
|
||||||
|
|
||||||
// 부모 메뉴 objid 결정
|
// 부모 메뉴 objid 결정
|
||||||
|
// 우선순위: groupToMenuMap > parent_menu_objid (존재 확인 필수)
|
||||||
let parentMenuObjid = userMenuRootObjid;
|
let parentMenuObjid = userMenuRootObjid;
|
||||||
if (group.parent_group_id && group.parent_menu_objid) {
|
if (group.parent_group_id && groupToMenuMap.has(group.parent_group_id)) {
|
||||||
parentMenuObjid = Number(group.parent_menu_objid);
|
// 현재 트랜잭션에서 생성된 부모 메뉴 사용
|
||||||
} else if (group.parent_group_id && groupToMenuMap.has(group.parent_group_id)) {
|
|
||||||
parentMenuObjid = groupToMenuMap.get(group.parent_group_id)!;
|
parentMenuObjid = groupToMenuMap.get(group.parent_group_id)!;
|
||||||
|
} else if (group.parent_group_id && group.parent_menu_objid) {
|
||||||
|
// 기존 parent_menu_objid가 실제로 존재하는지 확인
|
||||||
|
const parentMenuExists = existingMenuObjids.has(Number(group.parent_menu_objid));
|
||||||
|
if (parentMenuExists) {
|
||||||
|
parentMenuObjid = Number(group.parent_menu_objid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 같은 부모 아래에서 가장 높은 seq 조회 후 +1
|
// 같은 부모 아래에서 가장 높은 seq 조회 후 +1
|
||||||
|
|
@ -261,7 +291,7 @@ export async function syncScreenGroupsToMenu(
|
||||||
INSERT INTO menu_info (
|
INSERT INTO menu_info (
|
||||||
objid, parent_obj_id, menu_name_kor, menu_name_eng,
|
objid, parent_obj_id, menu_name_kor, menu_name_eng,
|
||||||
seq, menu_type, company_code, writer, regdate, status, screen_group_id, menu_desc
|
seq, menu_type, company_code, writer, regdate, status, screen_group_id, menu_desc
|
||||||
) VALUES ($1, $2, $3, $4, $5, 1, $6, $7, NOW(), 'Y', $8, $9)
|
) VALUES ($1, $2, $3, $4, $5, 1, $6, $7, NOW(), 'active', $8, $9)
|
||||||
RETURNING objid
|
RETURNING objid
|
||||||
`;
|
`;
|
||||||
await client.query(insertMenuQuery, [
|
await client.query(insertMenuQuery, [
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,7 @@ export function ScreenGroupTreeView({
|
||||||
const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
|
const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
const [syncDirection, setSyncDirection] = useState<"screen-to-menu" | "menu-to-screen" | "all" | null>(null);
|
const [syncDirection, setSyncDirection] = useState<"screen-to-menu" | "menu-to-screen" | "all" | null>(null);
|
||||||
|
const [syncProgress, setSyncProgress] = useState<{ message: string; detail?: string } | null>(null);
|
||||||
|
|
||||||
// 회사 선택 (최고 관리자용)
|
// 회사 선택 (최고 관리자용)
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
@ -328,14 +329,31 @@ export function ScreenGroupTreeView({
|
||||||
|
|
||||||
setIsSyncing(true);
|
setIsSyncing(true);
|
||||||
setSyncDirection(direction);
|
setSyncDirection(direction);
|
||||||
|
setSyncProgress({
|
||||||
|
message: direction === "screen-to-menu"
|
||||||
|
? "화면관리 → 메뉴 동기화 중..."
|
||||||
|
: "메뉴 → 화면관리 동기화 중...",
|
||||||
|
detail: "데이터를 분석하고 있습니다..."
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setSyncProgress({
|
||||||
|
message: direction === "screen-to-menu"
|
||||||
|
? "화면관리 → 메뉴 동기화 중..."
|
||||||
|
: "메뉴 → 화면관리 동기화 중...",
|
||||||
|
detail: "동기화 작업을 수행하고 있습니다..."
|
||||||
|
});
|
||||||
|
|
||||||
const response = direction === "screen-to-menu"
|
const response = direction === "screen-to-menu"
|
||||||
? await syncScreenGroupsToMenu(targetCompanyCode)
|
? await syncScreenGroupsToMenu(targetCompanyCode)
|
||||||
: await syncMenuToScreenGroups(targetCompanyCode);
|
: await syncMenuToScreenGroups(targetCompanyCode);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
setSyncProgress({
|
||||||
|
message: "동기화 완료!",
|
||||||
|
detail: `생성 ${data?.created || 0}개, 연결 ${data?.linked || 0}개, 스킵 ${data?.skipped || 0}개`
|
||||||
|
});
|
||||||
toast.success(
|
toast.success(
|
||||||
`동기화 완료: 생성 ${data?.created || 0}개, 연결 ${data?.linked || 0}개, 스킵 ${data?.skipped || 0}개`
|
`동기화 완료: 생성 ${data?.created || 0}개, 연결 ${data?.linked || 0}개, 스킵 ${data?.skipped || 0}개`
|
||||||
);
|
);
|
||||||
|
|
@ -347,13 +365,17 @@ export function ScreenGroupTreeView({
|
||||||
setSyncStatus(statusResponse.data);
|
setSyncStatus(statusResponse.data);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
setSyncProgress(null);
|
||||||
toast.error(`동기화 실패: ${response.error || "알 수 없는 오류"}`);
|
toast.error(`동기화 실패: ${response.error || "알 수 없는 오류"}`);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
setSyncProgress(null);
|
||||||
toast.error(`동기화 실패: ${error.message}`);
|
toast.error(`동기화 실패: ${error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSyncing(false);
|
setIsSyncing(false);
|
||||||
setSyncDirection(null);
|
setSyncDirection(null);
|
||||||
|
// 3초 후 진행 메시지 초기화
|
||||||
|
setTimeout(() => setSyncProgress(null), 3000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -366,27 +388,42 @@ export function ScreenGroupTreeView({
|
||||||
|
|
||||||
setIsSyncing(true);
|
setIsSyncing(true);
|
||||||
setSyncDirection("all");
|
setSyncDirection("all");
|
||||||
|
setSyncProgress({
|
||||||
|
message: "전체 회사 동기화 중...",
|
||||||
|
detail: "모든 회사의 데이터를 분석하고 있습니다..."
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setSyncProgress({
|
||||||
|
message: "전체 회사 동기화 중...",
|
||||||
|
detail: "양방향 동기화 작업을 수행하고 있습니다..."
|
||||||
|
});
|
||||||
|
|
||||||
const response = await syncAllCompanies();
|
const response = await syncAllCompanies();
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
setSyncProgress({
|
||||||
|
message: "전체 동기화 완료!",
|
||||||
|
detail: `${data.totalCompanies}개 회사, 생성 ${data.totalCreated}개, 연결 ${data.totalLinked}개`
|
||||||
|
});
|
||||||
toast.success(
|
toast.success(
|
||||||
`전체 동기화 완료: ${data.totalCompanies}개 회사, 생성 ${data.totalCreated}개, 연결 ${data.totalLinked}개`
|
`전체 동기화 완료: ${data.totalCompanies}개 회사, 생성 ${data.totalCreated}개, 연결 ${data.totalLinked}개`
|
||||||
);
|
);
|
||||||
// 그룹 데이터 새로고침
|
// 그룹 데이터 새로고침
|
||||||
await loadGroupsData();
|
await loadGroupsData();
|
||||||
// 동기화 다이얼로그 닫기
|
|
||||||
setIsSyncDialogOpen(false);
|
|
||||||
} else {
|
} else {
|
||||||
|
setSyncProgress(null);
|
||||||
toast.error(`전체 동기화 실패: ${response.error || "알 수 없는 오류"}`);
|
toast.error(`전체 동기화 실패: ${response.error || "알 수 없는 오류"}`);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
setSyncProgress(null);
|
||||||
toast.error(`전체 동기화 실패: ${error.message}`);
|
toast.error(`전체 동기화 실패: ${error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSyncing(false);
|
setIsSyncing(false);
|
||||||
setSyncDirection(null);
|
setSyncDirection(null);
|
||||||
|
// 3초 후 진행 메시지 초기화
|
||||||
|
setTimeout(() => setSyncProgress(null), 3000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -979,6 +1016,7 @@ export function ScreenGroupTreeView({
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
그룹 추가
|
그룹 추가
|
||||||
</Button>
|
</Button>
|
||||||
|
{isSuperAdmin && (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleOpenSyncDialog}
|
onClick={handleOpenSyncDialog}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -988,6 +1026,7 @@ export function ScreenGroupTreeView({
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
메뉴 동기화
|
메뉴 동기화
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 트리 목록 */}
|
{/* 트리 목록 */}
|
||||||
|
|
@ -1816,7 +1855,23 @@ export function ScreenGroupTreeView({
|
||||||
|
|
||||||
{/* 메뉴-화면그룹 동기화 다이얼로그 */}
|
{/* 메뉴-화면그룹 동기화 다이얼로그 */}
|
||||||
<Dialog open={isSyncDialogOpen} onOpenChange={setIsSyncDialogOpen}>
|
<Dialog open={isSyncDialogOpen} onOpenChange={setIsSyncDialogOpen}>
|
||||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
<DialogContent className="max-w-[95vw] sm:max-w-[500px] overflow-hidden">
|
||||||
|
{/* 동기화 진행 중 오버레이 (삭제와 동일한 스타일) */}
|
||||||
|
{isSyncing && (
|
||||||
|
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center rounded-lg bg-background/90 backdrop-blur-sm">
|
||||||
|
<Loader2 className="h-10 w-10 animate-spin text-primary" />
|
||||||
|
<p className="mt-4 text-sm font-medium">{syncProgress?.message || "동기화 중..."}</p>
|
||||||
|
{syncProgress?.detail && (
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">{syncProgress.detail}</p>
|
||||||
|
)}
|
||||||
|
<div className="mt-3 h-2 w-48 overflow-hidden rounded-full bg-secondary">
|
||||||
|
<div
|
||||||
|
className="h-full bg-primary animate-pulse"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-base sm:text-lg">메뉴-화면 동기화</DialogTitle>
|
<DialogTitle className="text-base sm:text-lg">메뉴-화면 동기화</DialogTitle>
|
||||||
<DialogDescription className="text-xs sm:text-sm">
|
<DialogDescription className="text-xs sm:text-sm">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue