feat: 화면 관리 기능 개선 (복제/삭제/그룹 관리/테이블 설정)
- 화면 관리 시스템의 복제, 삭제, 수정 및 테이블 설정 기능을 전면 개선 - 그룹 삭제 시 하위 그룹과의 연관성 정리 및 로딩 프로그레스 바 추가 - 화면 수정 기능 추가: 이름, 그룹, 역할, 정렬 순서 변경 - 테이블 설정 모달에 관련 기능 추가 및 데이터 일관성 유지 - 메뉴-화면 그룹 동기화 API 추가 및 관련 상태 관리 기능 구현 - 검색어 필터링 로직 개선: 다중 키워드 지원 - 관련 파일 및 진행 상태 업데이트
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import { Request, Response } from "express";
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
import {
|
||||
syncScreenGroupsToMenu,
|
||||
syncMenuToScreenGroups,
|
||||
getSyncStatus,
|
||||
syncAllCompanies,
|
||||
} from "../services/menuScreenSyncService";
|
||||
|
||||
// pool 인스턴스 가져오기
|
||||
const pool = getPool();
|
||||
@@ -294,10 +300,35 @@ export const updateScreenGroup = async (req: Request, res: Response) => {
|
||||
|
||||
// 화면 그룹 삭제
|
||||
export const deleteScreenGroup = async (req: Request, res: Response) => {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = (req.user as any).companyCode;
|
||||
|
||||
await client.query('BEGIN');
|
||||
|
||||
// 1. 삭제할 그룹과 하위 그룹 ID 수집 (CASCADE 삭제 대상)
|
||||
const childGroupsResult = await client.query(`
|
||||
WITH RECURSIVE child_groups AS (
|
||||
SELECT id FROM screen_groups WHERE id = $1
|
||||
UNION ALL
|
||||
SELECT sg.id FROM screen_groups sg
|
||||
JOIN child_groups cg ON sg.parent_group_id = cg.id
|
||||
)
|
||||
SELECT id FROM child_groups
|
||||
`, [id]);
|
||||
const groupIdsToDelete = childGroupsResult.rows.map((r: any) => r.id);
|
||||
|
||||
// 2. menu_info에서 삭제될 screen_group 참조를 NULL로 정리
|
||||
if (groupIdsToDelete.length > 0) {
|
||||
await client.query(`
|
||||
UPDATE menu_info
|
||||
SET screen_group_id = NULL
|
||||
WHERE screen_group_id = ANY($1::int[])
|
||||
`, [groupIdsToDelete]);
|
||||
}
|
||||
|
||||
// 3. screen_groups 삭제
|
||||
let query = `DELETE FROM screen_groups WHERE id = $1`;
|
||||
const params: any[] = [id];
|
||||
|
||||
@@ -308,18 +339,24 @@ export const deleteScreenGroup = async (req: Request, res: Response) => {
|
||||
|
||||
query += " RETURNING id";
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
const result = await client.query(query, params);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(404).json({ success: false, message: "화면 그룹을 찾을 수 없거나 권한이 없습니다." });
|
||||
}
|
||||
|
||||
logger.info("화면 그룹 삭제", { companyCode, groupId: id });
|
||||
await client.query('COMMIT');
|
||||
|
||||
logger.info("화면 그룹 삭제", { companyCode, groupId: id, cleanedRefs: groupIdsToDelete.length });
|
||||
|
||||
res.json({ success: true, message: "화면 그룹이 삭제되었습니다." });
|
||||
} catch (error: any) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error("화면 그룹 삭제 실패:", error);
|
||||
res.status(500).json({ success: false, message: "화면 그룹 삭제에 실패했습니다.", error: error.message });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2014,3 +2051,202 @@ export const getScreenSubTables = async (req: Request, res: Response) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 메뉴-화면그룹 동기화 API
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 화면관리 → 메뉴 동기화
|
||||
* screen_groups를 menu_info로 동기화
|
||||
*/
|
||||
export const syncScreenGroupsToMenuController = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userCompanyCode = (req.user as any).companyCode;
|
||||
const userId = (req.user as any).userId;
|
||||
const { targetCompanyCode } = req.body;
|
||||
|
||||
// 최고 관리자가 특정 회사를 지정한 경우 해당 회사로
|
||||
let companyCode = userCompanyCode;
|
||||
if (userCompanyCode === "*" && targetCompanyCode) {
|
||||
companyCode = targetCompanyCode;
|
||||
}
|
||||
|
||||
// 최고 관리자(*)는 회사를 지정해야 함
|
||||
if (companyCode === "*") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "동기화할 회사를 선택해주세요.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("화면관리 → 메뉴 동기화 요청", { companyCode, userId });
|
||||
|
||||
const result = await syncScreenGroupsToMenu(companyCode, userId);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "동기화 중 오류가 발생했습니다.",
|
||||
errors: result.errors,
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `동기화 완료: 생성 ${result.created}개, 연결 ${result.linked}개, 스킵 ${result.skipped}개`,
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("화면관리 → 메뉴 동기화 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "동기화에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 메뉴 → 화면관리 동기화
|
||||
* menu_info를 screen_groups로 동기화
|
||||
*/
|
||||
export const syncMenuToScreenGroupsController = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userCompanyCode = (req.user as any).companyCode;
|
||||
const userId = (req.user as any).userId;
|
||||
const { targetCompanyCode } = req.body;
|
||||
|
||||
// 최고 관리자가 특정 회사를 지정한 경우 해당 회사로
|
||||
let companyCode = userCompanyCode;
|
||||
if (userCompanyCode === "*" && targetCompanyCode) {
|
||||
companyCode = targetCompanyCode;
|
||||
}
|
||||
|
||||
// 최고 관리자(*)는 회사를 지정해야 함
|
||||
if (companyCode === "*") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "동기화할 회사를 선택해주세요.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("메뉴 → 화면관리 동기화 요청", { companyCode, userId });
|
||||
|
||||
const result = await syncMenuToScreenGroups(companyCode, userId);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "동기화 중 오류가 발생했습니다.",
|
||||
errors: result.errors,
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `동기화 완료: 생성 ${result.created}개, 연결 ${result.linked}개, 스킵 ${result.skipped}개`,
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("메뉴 → 화면관리 동기화 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "동기화에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 동기화 상태 조회
|
||||
*/
|
||||
export const getSyncStatusController = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userCompanyCode = (req.user as any).companyCode;
|
||||
const { targetCompanyCode } = req.query;
|
||||
|
||||
// 최고 관리자가 특정 회사를 지정한 경우 해당 회사로
|
||||
let companyCode = userCompanyCode;
|
||||
if (userCompanyCode === "*" && targetCompanyCode) {
|
||||
companyCode = targetCompanyCode as string;
|
||||
}
|
||||
|
||||
// 최고 관리자(*)는 회사를 지정해야 함
|
||||
if (companyCode === "*") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "조회할 회사를 선택해주세요.",
|
||||
});
|
||||
}
|
||||
|
||||
const status = await getSyncStatus(companyCode);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: status,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("동기화 상태 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "동기화 상태 조회에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 전체 회사 동기화
|
||||
* 모든 회사에 대해 양방향 동기화 수행 (최고 관리자만)
|
||||
*/
|
||||
export const syncAllCompaniesController = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userCompanyCode = (req.user as any).companyCode;
|
||||
const userId = (req.user as any).userId;
|
||||
|
||||
// 최고 관리자만 전체 동기화 가능
|
||||
if (userCompanyCode !== "*") {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: "전체 동기화는 최고 관리자만 수행할 수 있습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("전체 회사 동기화 요청", { userId });
|
||||
|
||||
const result = await syncAllCompanies(userId);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "전체 동기화 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 결과 요약
|
||||
const totalCreated = result.results.reduce((sum, r) => sum + r.created, 0);
|
||||
const totalLinked = result.results.reduce((sum, r) => sum + r.linked, 0);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `전체 동기화 완료: ${result.totalCompanies}개 회사 중 ${result.successCount}개 성공`,
|
||||
data: {
|
||||
totalCompanies: result.totalCompanies,
|
||||
successCount: result.successCount,
|
||||
failedCount: result.failedCount,
|
||||
totalCreated,
|
||||
totalLinked,
|
||||
details: result.results,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("전체 회사 동기화 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "전체 동기화에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user