feat: 화면 관리 및 메뉴 동기화 기능 개선

- 화면 그룹 컨트롤러 기능 확장
- 메뉴 복사 서비스 개선
- 메뉴-화면 동기화 서비스 추가
- 번호 규칙 서비스 개선
- 화면 관리 서비스 확장
- CopyScreenModal 기능 개선
- DataFlowPanel, FieldJoinPanel 수정
This commit is contained in:
DDD1542
2026-01-21 11:53:51 +09:00
parent 40a226ca30
commit ad8b1791bc
15 changed files with 3895 additions and 136 deletions

View File

@@ -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;

View File

@@ -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,
});
}
};

View File

@@ -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: "연쇄관계 설정 복제에 실패했습니다.",
});
}
};