feat: 화면 관리 및 메뉴 동기화 기능 개선
- 화면 그룹 컨트롤러 기능 확장 - 메뉴 복사 서비스 개선 - 메뉴-화면 동기화 서비스 추가 - 번호 규칙 서비스 개선 - 화면 관리 서비스 확장 - CopyScreenModal 기능 개선 - DataFlowPanel, FieldJoinPanel 수정
This commit is contained in:
@@ -169,14 +169,22 @@ router.put("/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res:
|
||||
const { ruleId } = req.params;
|
||||
const updates = req.body;
|
||||
|
||||
logger.info("채번 규칙 수정 요청", { ruleId, companyCode, updates });
|
||||
|
||||
try {
|
||||
const updatedRule = await numberingRuleService.updateRule(ruleId, updates, companyCode);
|
||||
logger.info("채번 규칙 수정 성공", { ruleId, companyCode });
|
||||
return res.json({ success: true, data: updatedRule });
|
||||
} catch (error: any) {
|
||||
logger.error("채번 규칙 수정 실패", {
|
||||
ruleId,
|
||||
companyCode,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
if (error.message.includes("찾을 수 없거나")) {
|
||||
return res.status(404).json({ success: false, error: error.message });
|
||||
}
|
||||
logger.error("규칙 수정 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
@@ -257,4 +265,31 @@ router.post("/:ruleId/reset", authenticateToken, async (req: AuthenticatedReques
|
||||
}
|
||||
});
|
||||
|
||||
// 회사별 채번규칙 복제 (화면 복제 후 메뉴 동기화 완료 상태에서 호출)
|
||||
router.post("/copy-for-company", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||
const userCompanyCode = req.user!.companyCode;
|
||||
const { sourceCompanyCode, targetCompanyCode } = req.body;
|
||||
|
||||
// 최고 관리자만 회사간 복제 가능
|
||||
if (userCompanyCode !== "*") {
|
||||
return res.status(403).json({ success: false, error: "최고 관리자만 회사간 채번규칙 복제가 가능합니다." });
|
||||
}
|
||||
|
||||
if (!sourceCompanyCode || !targetCompanyCode) {
|
||||
return res.status(400).json({ success: false, error: "원본 회사 코드와 대상 회사 코드가 필요합니다." });
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info("회사별 채번규칙 복제 시작", { sourceCompanyCode, targetCompanyCode });
|
||||
|
||||
const result = await numberingRuleService.copyRulesForCompany(sourceCompanyCode, targetCompanyCode);
|
||||
|
||||
logger.info("회사별 채번규칙 복제 완료", { sourceCompanyCode, targetCompanyCode, result });
|
||||
return res.json({ success: true, data: result });
|
||||
} catch (error: any) {
|
||||
logger.error("회사별 채번규칙 복제 실패", { error: error.message, sourceCompanyCode, targetCompanyCode });
|
||||
return res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Request, Response } from "express";
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
import { MultiLangService } from "../services/multilangService";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import {
|
||||
syncScreenGroupsToMenu,
|
||||
syncMenuToScreenGroups,
|
||||
getSyncStatus,
|
||||
syncAllCompanies,
|
||||
} from "../services/menuScreenSyncService";
|
||||
|
||||
// pool 인스턴스 가져오기
|
||||
const pool = getPool();
|
||||
|
||||
// 다국어 서비스 인스턴스
|
||||
const multiLangService = new MultiLangService();
|
||||
|
||||
// ============================================================
|
||||
// 화면 그룹 (screen_groups) CRUD
|
||||
// ============================================================
|
||||
@@ -17,7 +19,7 @@ const multiLangService = new MultiLangService();
|
||||
// 화면 그룹 목록 조회
|
||||
export const getScreenGroups = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const { page = 1, size = 20, searchTerm } = req.query;
|
||||
const offset = (parseInt(page as string) - 1) * parseInt(size as string);
|
||||
|
||||
@@ -92,7 +94,7 @@ export const getScreenGroups = async (req: AuthenticatedRequest, res: Response)
|
||||
export const getScreenGroup = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user!.companyCode;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
let query = `
|
||||
SELECT sg.*,
|
||||
@@ -137,8 +139,8 @@ export const getScreenGroup = async (req: AuthenticatedRequest, res: Response) =
|
||||
// 화면 그룹 생성
|
||||
export const createScreenGroup = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const userCompanyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const userCompanyCode = req.user?.companyCode || "*";
|
||||
const userId = req.user?.userId || "";
|
||||
const { group_name, group_code, main_table_name, description, icon, display_order, is_active, parent_group_id, target_company_code } = req.body;
|
||||
|
||||
if (!group_name || !group_code) {
|
||||
@@ -196,47 +198,6 @@ export const createScreenGroup = async (req: AuthenticatedRequest, res: Response
|
||||
// 업데이트된 데이터 반환
|
||||
const updatedResult = await pool.query(`SELECT * FROM screen_groups WHERE id = $1`, [newGroupId]);
|
||||
|
||||
// 다국어 카테고리 자동 생성 (그룹 경로 기반)
|
||||
try {
|
||||
// 그룹 경로 조회 (상위 그룹 → 현재 그룹)
|
||||
const groupPathResult = await pool.query(
|
||||
`WITH RECURSIVE group_path AS (
|
||||
SELECT id, parent_group_id, group_name, group_level, 1 as depth
|
||||
FROM screen_groups
|
||||
WHERE id = $1
|
||||
UNION ALL
|
||||
SELECT g.id, g.parent_group_id, g.group_name, g.group_level, gp.depth + 1
|
||||
FROM screen_groups g
|
||||
INNER JOIN group_path gp ON g.id = gp.parent_group_id
|
||||
WHERE g.parent_group_id IS NOT NULL
|
||||
)
|
||||
SELECT group_name FROM group_path
|
||||
ORDER BY depth DESC`,
|
||||
[newGroupId]
|
||||
);
|
||||
|
||||
const groupPath = groupPathResult.rows.map((r: any) => r.group_name);
|
||||
|
||||
// 회사 이름 조회
|
||||
let companyName = "공통";
|
||||
if (finalCompanyCode !== "*") {
|
||||
const companyResult = await pool.query(
|
||||
`SELECT company_name FROM company_mng WHERE company_code = $1`,
|
||||
[finalCompanyCode]
|
||||
);
|
||||
if (companyResult.rows.length > 0) {
|
||||
companyName = companyResult.rows[0].company_name;
|
||||
}
|
||||
}
|
||||
|
||||
// 다국어 카테고리 생성
|
||||
await multiLangService.ensureScreenGroupCategory(finalCompanyCode, companyName, groupPath);
|
||||
logger.info("화면 그룹 다국어 카테고리 자동 생성 완료", { groupPath, companyCode: finalCompanyCode });
|
||||
} catch (multilangError: any) {
|
||||
// 다국어 카테고리 생성 실패해도 그룹 생성은 성공으로 처리
|
||||
logger.warn("화면 그룹 다국어 카테고리 생성 실패 (무시하고 계속):", multilangError.message);
|
||||
}
|
||||
|
||||
logger.info("화면 그룹 생성", { userCompanyCode, finalCompanyCode, groupId: newGroupId, groupName: group_name, parentGroupId: parent_group_id });
|
||||
|
||||
res.json({ success: true, data: updatedResult.rows[0], message: "화면 그룹이 생성되었습니다." });
|
||||
@@ -253,7 +214,7 @@ export const createScreenGroup = async (req: AuthenticatedRequest, res: Response
|
||||
export const updateScreenGroup = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const userCompanyCode = req.user!.companyCode;
|
||||
const userCompanyCode = req.user?.companyCode || "*";
|
||||
const { group_name, group_code, main_table_name, description, icon, display_order, is_active, parent_group_id, target_company_code } = req.body;
|
||||
|
||||
// 회사 코드 결정: 최고 관리자가 특정 회사를 선택한 경우 해당 회사로, 아니면 현재 그룹의 회사 유지
|
||||
@@ -340,10 +301,35 @@ export const updateScreenGroup = async (req: AuthenticatedRequest, res: Response
|
||||
|
||||
// 화면 그룹 삭제
|
||||
export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response) => {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user!.companyCode;
|
||||
const companyCode = req.user?.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];
|
||||
|
||||
@@ -354,18 +340,24 @@ export const deleteScreenGroup = async (req: AuthenticatedRequest, 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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -377,14 +369,19 @@ export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response
|
||||
// 그룹에 화면 추가
|
||||
export const addScreenToGroup = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { group_id, screen_id, screen_role, display_order, is_default } = req.body;
|
||||
const userCompanyCode = req.user?.companyCode || "*";
|
||||
const userId = req.user?.userId || "";
|
||||
const { group_id, screen_id, screen_role, display_order, is_default, target_company_code } = req.body;
|
||||
|
||||
if (!group_id || !screen_id) {
|
||||
return res.status(400).json({ success: false, message: "그룹 ID와 화면 ID는 필수입니다." });
|
||||
}
|
||||
|
||||
// 최고 관리자가 다른 회사로 복제할 때 target_company_code 사용
|
||||
const effectiveCompanyCode = (userCompanyCode === "*" && target_company_code)
|
||||
? target_company_code
|
||||
: userCompanyCode;
|
||||
|
||||
const query = `
|
||||
INSERT INTO screen_group_screens (group_id, screen_id, screen_role, display_order, is_default, company_code, writer)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
@@ -396,13 +393,13 @@ export const addScreenToGroup = async (req: AuthenticatedRequest, res: Response)
|
||||
screen_role || 'main',
|
||||
display_order || 0,
|
||||
is_default || 'N',
|
||||
companyCode === "*" ? "*" : companyCode,
|
||||
effectiveCompanyCode,
|
||||
userId
|
||||
];
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
logger.info("화면-그룹 연결 추가", { companyCode, groupId: group_id, screenId: screen_id });
|
||||
logger.info("화면-그룹 연결 추가", { companyCode: effectiveCompanyCode, groupId: group_id, screenId: screen_id });
|
||||
|
||||
res.json({ success: true, data: result.rows[0], message: "화면이 그룹에 추가되었습니다." });
|
||||
} catch (error: any) {
|
||||
@@ -418,7 +415,7 @@ export const addScreenToGroup = async (req: AuthenticatedRequest, res: Response)
|
||||
export const removeScreenFromGroup = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user!.companyCode;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
let query = `DELETE FROM screen_group_screens WHERE id = $1`;
|
||||
const params: any[] = [id];
|
||||
@@ -449,7 +446,7 @@ export const removeScreenFromGroup = async (req: AuthenticatedRequest, res: Resp
|
||||
export const updateScreenInGroup = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user!.companyCode;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const { screen_role, display_order, is_default } = req.body;
|
||||
|
||||
let query = `
|
||||
@@ -487,7 +484,7 @@ export const updateScreenInGroup = async (req: AuthenticatedRequest, res: Respon
|
||||
// 화면 필드 조인 목록 조회
|
||||
export const getFieldJoins = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const { screen_id } = req.query;
|
||||
|
||||
let query = `
|
||||
@@ -528,8 +525,8 @@ export const getFieldJoins = async (req: AuthenticatedRequest, res: Response) =>
|
||||
// 화면 필드 조인 생성
|
||||
export const createFieldJoin = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const userId = req.user?.userId || "";
|
||||
const {
|
||||
screen_id, layout_id, component_id, field_name,
|
||||
save_table, save_column, join_table, join_column, display_column,
|
||||
@@ -570,7 +567,7 @@ export const createFieldJoin = async (req: AuthenticatedRequest, res: Response)
|
||||
export const updateFieldJoin = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user!.companyCode;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const {
|
||||
layout_id, component_id, field_name,
|
||||
save_table, save_column, join_table, join_column, display_column,
|
||||
@@ -615,7 +612,7 @@ export const updateFieldJoin = async (req: AuthenticatedRequest, res: Response)
|
||||
export const deleteFieldJoin = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user!.companyCode;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
let query = `DELETE FROM screen_field_joins WHERE id = $1`;
|
||||
const params: any[] = [id];
|
||||
@@ -648,7 +645,7 @@ export const deleteFieldJoin = async (req: AuthenticatedRequest, res: Response)
|
||||
// 데이터 흐름 목록 조회
|
||||
export const getDataFlows = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const { group_id, source_screen_id } = req.query;
|
||||
|
||||
let query = `
|
||||
@@ -698,8 +695,8 @@ export const getDataFlows = async (req: AuthenticatedRequest, res: Response) =>
|
||||
// 데이터 흐름 생성
|
||||
export const createDataFlow = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const userId = req.user?.userId || "";
|
||||
const {
|
||||
group_id, source_screen_id, source_action, target_screen_id, target_action,
|
||||
data_mapping, flow_type, flow_label, condition_expression, is_active
|
||||
@@ -738,7 +735,7 @@ export const createDataFlow = async (req: AuthenticatedRequest, res: Response) =
|
||||
export const updateDataFlow = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user!.companyCode;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const {
|
||||
group_id, source_screen_id, source_action, target_screen_id, target_action,
|
||||
data_mapping, flow_type, flow_label, condition_expression, is_active
|
||||
@@ -781,7 +778,7 @@ export const updateDataFlow = async (req: AuthenticatedRequest, res: Response) =
|
||||
export const deleteDataFlow = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user!.companyCode;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
let query = `DELETE FROM screen_data_flows WHERE id = $1`;
|
||||
const params: any[] = [id];
|
||||
@@ -814,7 +811,7 @@ export const deleteDataFlow = async (req: AuthenticatedRequest, res: Response) =
|
||||
// 화면-테이블 관계 목록 조회
|
||||
export const getTableRelations = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const { screen_id, group_id } = req.query;
|
||||
|
||||
let query = `
|
||||
@@ -863,8 +860,8 @@ export const getTableRelations = async (req: AuthenticatedRequest, res: Response
|
||||
// 화면-테이블 관계 생성
|
||||
export const createTableRelation = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const userId = req.user?.userId || "";
|
||||
const { group_id, screen_id, table_name, relation_type, crud_operations, description, is_active } = req.body;
|
||||
|
||||
if (!screen_id || !table_name) {
|
||||
@@ -897,7 +894,7 @@ export const createTableRelation = async (req: AuthenticatedRequest, res: Respon
|
||||
export const updateTableRelation = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user!.companyCode;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const { group_id, table_name, relation_type, crud_operations, description, is_active } = req.body;
|
||||
|
||||
let query = `
|
||||
@@ -932,7 +929,7 @@ export const updateTableRelation = async (req: AuthenticatedRequest, res: Respon
|
||||
export const deleteTableRelation = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user!.companyCode;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
let query = `DELETE FROM screen_table_relations WHERE id = $1`;
|
||||
const params: any[] = [id];
|
||||
@@ -962,7 +959,7 @@ export const deleteTableRelation = async (req: AuthenticatedRequest, res: Respon
|
||||
// ============================================================
|
||||
|
||||
// 화면 레이아웃 요약 조회 (위젯 타입별 개수, 라벨 목록)
|
||||
export const getScreenLayoutSummary = async (req: Request, res: Response) => {
|
||||
export const getScreenLayoutSummary = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { screenId } = req.params;
|
||||
|
||||
@@ -1030,7 +1027,7 @@ export const getScreenLayoutSummary = async (req: Request, res: Response) => {
|
||||
};
|
||||
|
||||
// 여러 화면의 레이아웃 요약 일괄 조회 (미니어처 렌더링용 좌표 포함)
|
||||
export const getMultipleScreenLayoutSummary = async (req: Request, res: Response) => {
|
||||
export const getMultipleScreenLayoutSummary = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { screenIds } = req.body;
|
||||
|
||||
@@ -1230,7 +1227,7 @@ export const getMultipleScreenLayoutSummary = async (req: Request, res: Response
|
||||
// ============================================================
|
||||
|
||||
// 여러 화면의 서브 테이블 정보 조회 (메인 테이블 → 서브 테이블 관계)
|
||||
export const getScreenSubTables = async (req: Request, res: Response) => {
|
||||
export const getScreenSubTables = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { screenIds } = req.body;
|
||||
|
||||
@@ -2060,3 +2057,368 @@ export const getScreenSubTables = async (req: Request, res: Response) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 메뉴-화면그룹 동기화 API
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 화면관리 → 메뉴 동기화
|
||||
* screen_groups를 menu_info로 동기화
|
||||
*/
|
||||
export const syncScreenGroupsToMenuController = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const userCompanyCode = req.user?.companyCode || "*";
|
||||
const userId = req.user?.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: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const userCompanyCode = req.user?.companyCode || "*";
|
||||
const userId = req.user?.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: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const userCompanyCode = req.user?.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: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const userCompanyCode = req.user?.companyCode || "*";
|
||||
const userId = req.user?.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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* [PoC] screen_groups 기반 메뉴 트리 조회
|
||||
*
|
||||
* 기존 menu_info 대신 screen_groups를 사이드바 메뉴로 사용하기 위한 테스트 API
|
||||
* - screen_groups를 트리 구조로 반환
|
||||
* - 각 그룹에 연결된 기본 화면의 URL 포함
|
||||
* - menu_objid를 통해 권한 체크 가능
|
||||
*
|
||||
* DB 변경 없이 로직만 추가
|
||||
*/
|
||||
export const getMenuTreeFromScreenGroups = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const userCompanyCode = req.user?.companyCode || "*";
|
||||
const { targetCompanyCode } = req.query;
|
||||
|
||||
// 조회할 회사 코드 결정
|
||||
const companyCode = userCompanyCode === "*" && targetCompanyCode
|
||||
? String(targetCompanyCode)
|
||||
: userCompanyCode;
|
||||
|
||||
logger.info("[PoC] screen_groups 기반 메뉴 트리 조회", {
|
||||
userCompanyCode,
|
||||
targetCompanyCode: companyCode
|
||||
});
|
||||
|
||||
// 1. screen_groups 조회 (계층 구조 포함)
|
||||
const groupsQuery = `
|
||||
SELECT
|
||||
sg.id,
|
||||
sg.group_name,
|
||||
sg.group_code,
|
||||
sg.parent_group_id,
|
||||
sg.group_level,
|
||||
sg.display_order,
|
||||
sg.icon,
|
||||
sg.is_active,
|
||||
sg.menu_objid,
|
||||
sg.company_code,
|
||||
-- 기본 화면 정보 (URL 생성용)
|
||||
(
|
||||
SELECT json_build_object(
|
||||
'screen_id', sd.screen_id,
|
||||
'screen_name', sd.screen_name,
|
||||
'screen_code', sd.screen_code
|
||||
)
|
||||
FROM screen_group_screens sgs
|
||||
JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id
|
||||
WHERE sgs.group_id = sg.id AND sgs.company_code = sg.company_code
|
||||
ORDER BY
|
||||
CASE WHEN sgs.is_default = 'Y' THEN 0 ELSE 1 END,
|
||||
sgs.display_order ASC
|
||||
LIMIT 1
|
||||
) as default_screen,
|
||||
-- 하위 화면 개수
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM screen_group_screens sgs
|
||||
WHERE sgs.group_id = sg.id AND sgs.company_code = sg.company_code
|
||||
) as screen_count
|
||||
FROM screen_groups sg
|
||||
WHERE sg.company_code = $1
|
||||
AND (sg.is_active = 'Y' OR sg.is_active IS NULL)
|
||||
ORDER BY sg.group_level ASC, sg.display_order ASC, sg.group_name ASC
|
||||
`;
|
||||
|
||||
const groupsResult = await pool.query(groupsQuery, [companyCode]);
|
||||
|
||||
// 2. 트리 구조로 변환
|
||||
const groups = groupsResult.rows;
|
||||
const groupMap = new Map<number, any>();
|
||||
const rootGroups: any[] = [];
|
||||
|
||||
// 먼저 모든 그룹을 Map에 저장
|
||||
for (const group of groups) {
|
||||
const menuItem = {
|
||||
id: group.id,
|
||||
objid: group.menu_objid || group.id, // 권한 체크용 (menu_objid 우선)
|
||||
name: group.group_name,
|
||||
name_kor: group.group_name,
|
||||
icon: group.icon,
|
||||
url: group.default_screen
|
||||
? `/screens/${group.default_screen.screen_id}`
|
||||
: null,
|
||||
screen_id: group.default_screen?.screen_id || null,
|
||||
screen_code: group.default_screen?.screen_code || null,
|
||||
screen_count: parseInt(group.screen_count) || 0,
|
||||
parent_id: group.parent_group_id,
|
||||
level: group.group_level || 0,
|
||||
display_order: group.display_order || 0,
|
||||
is_active: group.is_active === 'Y',
|
||||
menu_objid: group.menu_objid, // 기존 권한 시스템 연결용
|
||||
children: [],
|
||||
// menu_info 호환 필드
|
||||
menu_name_kor: group.group_name,
|
||||
menu_url: group.default_screen
|
||||
? `/screens/${group.default_screen.screen_id}`
|
||||
: null,
|
||||
parent_obj_id: null, // 나중에 설정
|
||||
seq: group.display_order || 0,
|
||||
status: group.is_active === 'Y' ? 'active' : 'inactive',
|
||||
};
|
||||
|
||||
groupMap.set(group.id, menuItem);
|
||||
}
|
||||
|
||||
// 부모-자식 관계 설정
|
||||
for (const group of groups) {
|
||||
const menuItem = groupMap.get(group.id);
|
||||
|
||||
if (group.parent_group_id && groupMap.has(group.parent_group_id)) {
|
||||
const parent = groupMap.get(group.parent_group_id);
|
||||
parent.children.push(menuItem);
|
||||
menuItem.parent_obj_id = parent.objid;
|
||||
} else {
|
||||
// 최상위 그룹
|
||||
rootGroups.push(menuItem);
|
||||
menuItem.parent_obj_id = "0";
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 통계 정보
|
||||
const stats = {
|
||||
totalGroups: groups.length,
|
||||
groupsWithScreens: groups.filter(g => g.default_screen).length,
|
||||
groupsWithMenuObjid: groups.filter(g => g.menu_objid).length,
|
||||
rootGroups: rootGroups.length,
|
||||
};
|
||||
|
||||
logger.info("[PoC] screen_groups 메뉴 트리 생성 완료", stats);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "[PoC] screen_groups 기반 메뉴 트리",
|
||||
data: rootGroups,
|
||||
stats,
|
||||
// 플랫 리스트도 제공 (기존 menu_info 형식 호환)
|
||||
flatList: Array.from(groupMap.values()).map(item => ({
|
||||
objid: String(item.objid),
|
||||
OBJID: String(item.objid),
|
||||
menu_name_kor: item.name,
|
||||
MENU_NAME_KOR: item.name,
|
||||
menu_url: item.url,
|
||||
MENU_URL: item.url,
|
||||
parent_obj_id: String(item.parent_obj_id || "0"),
|
||||
PARENT_OBJ_ID: String(item.parent_obj_id || "0"),
|
||||
seq: item.seq,
|
||||
SEQ: item.seq,
|
||||
status: item.status,
|
||||
STATUS: item.status,
|
||||
menu_type: 1, // 사용자 메뉴
|
||||
MENU_TYPE: 1,
|
||||
screen_group_id: item.id,
|
||||
menu_objid: item.menu_objid,
|
||||
})),
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error("[PoC] screen_groups 메뉴 트리 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "메뉴 트리 조회에 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -834,3 +834,264 @@ export const cleanupDeletedScreenMenuAssignments = async (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 그룹 복제 완료 후 탭 컴포넌트의 screenId 참조 일괄 업데이트
|
||||
export const updateTabScreenReferences = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { targetScreenIds, screenIdMap } = req.body;
|
||||
|
||||
if (!targetScreenIds || !Array.isArray(targetScreenIds)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "targetScreenIds 배열이 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!screenIdMap || typeof screenIdMap !== "object") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "screenIdMap 객체가 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await screenManagementService.updateTabScreenReferences(
|
||||
targetScreenIds,
|
||||
screenIdMap
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `${result.updated}개 레이아웃의 탭 참조가 업데이트되었습니다.`,
|
||||
updated: result.updated,
|
||||
details: result.details,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("탭 screenId 참조 업데이트 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "탭 screenId 참조 업데이트에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 화면-메뉴 할당 복제 (다른 회사로 복제 시)
|
||||
export const copyScreenMenuAssignments = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { sourceCompanyCode, targetCompanyCode, screenIdMap } = req.body;
|
||||
const userCompanyCode = req.user?.companyCode;
|
||||
|
||||
// 권한 체크: 최고 관리자만 가능
|
||||
if (userCompanyCode !== "*") {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: "최고 관리자만 이 기능을 사용할 수 있습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!sourceCompanyCode || !targetCompanyCode) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "sourceCompanyCode와 targetCompanyCode가 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!screenIdMap || typeof screenIdMap !== "object") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "screenIdMap 객체가 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await screenManagementService.copyScreenMenuAssignments(
|
||||
sourceCompanyCode,
|
||||
targetCompanyCode,
|
||||
screenIdMap
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `화면-메뉴 할당 ${result.copiedCount}개 복제 완료`,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("화면-메뉴 할당 복제 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "화면-메뉴 할당 복제에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 코드 카테고리 + 코드 복제
|
||||
export const copyCodeCategoryAndCodes = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { sourceCompanyCode, targetCompanyCode } = req.body;
|
||||
const userCompanyCode = req.user?.companyCode;
|
||||
|
||||
if (userCompanyCode !== "*") {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: "최고 관리자만 이 기능을 사용할 수 있습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!sourceCompanyCode || !targetCompanyCode) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "sourceCompanyCode와 targetCompanyCode가 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await screenManagementService.copyCodeCategoryAndCodes(
|
||||
sourceCompanyCode,
|
||||
targetCompanyCode
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `코드 카테고리 ${result.copiedCategories}개, 코드 ${result.copiedCodes}개 복제 완료`,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("코드 카테고리/코드 복제 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "코드 카테고리/코드 복제에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 카테고리 매핑 + 값 복제
|
||||
export const copyCategoryMapping = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { sourceCompanyCode, targetCompanyCode } = req.body;
|
||||
const userCompanyCode = req.user?.companyCode;
|
||||
|
||||
if (userCompanyCode !== "*") {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: "최고 관리자만 이 기능을 사용할 수 있습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!sourceCompanyCode || !targetCompanyCode) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "sourceCompanyCode와 targetCompanyCode가 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await screenManagementService.copyCategoryMapping(
|
||||
sourceCompanyCode,
|
||||
targetCompanyCode
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `카테고리 매핑 ${result.copiedMappings}개, 값 ${result.copiedValues}개 복제 완료`,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("카테고리 매핑/값 복제 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "카테고리 매핑/값 복제에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 테이블 타입관리 입력타입 설정 복제
|
||||
export const copyTableTypeColumns = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { sourceCompanyCode, targetCompanyCode } = req.body;
|
||||
const userCompanyCode = req.user?.companyCode;
|
||||
|
||||
if (userCompanyCode !== "*") {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: "최고 관리자만 이 기능을 사용할 수 있습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!sourceCompanyCode || !targetCompanyCode) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "sourceCompanyCode와 targetCompanyCode가 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await screenManagementService.copyTableTypeColumns(
|
||||
sourceCompanyCode,
|
||||
targetCompanyCode
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `테이블 타입 컬럼 ${result.copiedCount}개 복제 완료`,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("테이블 타입 컬럼 복제 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "테이블 타입 컬럼 복제에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 연쇄관계 설정 복제
|
||||
export const copyCascadingRelation = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
const { sourceCompanyCode, targetCompanyCode } = req.body;
|
||||
const userCompanyCode = req.user?.companyCode;
|
||||
|
||||
if (userCompanyCode !== "*") {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: "최고 관리자만 이 기능을 사용할 수 있습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!sourceCompanyCode || !targetCompanyCode) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "sourceCompanyCode와 targetCompanyCode가 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await screenManagementService.copyCascadingRelation(
|
||||
sourceCompanyCode,
|
||||
targetCompanyCode
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `연쇄관계 설정 ${result.copiedCount}개 복제 완료`,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("연쇄관계 설정 복제 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "연쇄관계 설정 복제에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user