Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into feature/v2-unified-renewal
This commit is contained in:
commit
8344486e56
|
|
@ -169,14 +169,22 @@ router.put("/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res:
|
||||||
const { ruleId } = req.params;
|
const { ruleId } = req.params;
|
||||||
const updates = req.body;
|
const updates = req.body;
|
||||||
|
|
||||||
|
logger.info("채번 규칙 수정 요청", { ruleId, companyCode, updates });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updatedRule = await numberingRuleService.updateRule(ruleId, updates, companyCode);
|
const updatedRule = await numberingRuleService.updateRule(ruleId, updates, companyCode);
|
||||||
|
logger.info("채번 규칙 수정 성공", { ruleId, companyCode });
|
||||||
return res.json({ success: true, data: updatedRule });
|
return res.json({ success: true, data: updatedRule });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
logger.error("채번 규칙 수정 실패", {
|
||||||
|
ruleId,
|
||||||
|
companyCode,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
if (error.message.includes("찾을 수 없거나")) {
|
if (error.message.includes("찾을 수 없거나")) {
|
||||||
return res.status(404).json({ success: false, error: error.message });
|
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 });
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -259,151 +267,106 @@ router.post("/:ruleId/reset", authenticateToken, async (req: AuthenticatedReques
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ====== 테스트용 API (menu_objid 없는 방식) ======
|
// ==================== 테스트 테이블용 API ====================
|
||||||
|
|
||||||
// [테스트] 테이블+컬럼 기반 채번규칙 조회
|
// [테스트] 테이블+컬럼 기반 채번 규칙 조회
|
||||||
router.get("/test/by-column", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
router.get("/test/by-column/:tableName/:columnName", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
const { tableName, columnName } = req.query;
|
const { tableName, columnName } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!tableName || typeof tableName !== "string") {
|
const rule = await numberingRuleService.getNumberingRuleByColumn(companyCode, tableName, columnName);
|
||||||
return res.status(400).json({ success: false, error: "tableName is required" });
|
|
||||||
}
|
|
||||||
if (!columnName || typeof columnName !== "string") {
|
|
||||||
return res.status(400).json({ success: false, error: "columnName is required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const rule = await numberingRuleService.getNumberingRuleByColumn(
|
|
||||||
companyCode,
|
|
||||||
tableName,
|
|
||||||
columnName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!rule) {
|
|
||||||
return res.status(404).json({ success: false, error: "규칙을 찾을 수 없습니다" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({ success: true, data: rule });
|
return res.json({ success: true, data: rule });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error("테이블+컬럼 기반 채번규칙 조회 실패", {
|
logger.error("테이블+컬럼 기반 채번 규칙 조회 실패", { error: error.message });
|
||||||
error: error.message,
|
|
||||||
companyCode,
|
|
||||||
tableName,
|
|
||||||
columnName,
|
|
||||||
});
|
|
||||||
return res.status(500).json({ success: false, error: error.message });
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// [테스트] 테스트 테이블에 채번규칙 저장
|
// [테스트] 테스트 테이블에 채번 규칙 저장
|
||||||
router.post("/test/save", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
router.post("/test/save", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
const userId = req.user!.userId;
|
const userId = req.user!.userId;
|
||||||
const config = req.body;
|
const ruleConfig = req.body;
|
||||||
|
|
||||||
|
logger.info("[테스트] 채번 규칙 저장 요청", {
|
||||||
|
ruleId: ruleConfig.ruleId,
|
||||||
|
tableName: ruleConfig.tableName,
|
||||||
|
columnName: ruleConfig.columnName,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!config.ruleId || !config.ruleName) {
|
if (!ruleConfig.tableName || !ruleConfig.columnName) {
|
||||||
return res.status(400).json({ success: false, error: "ruleId and ruleName are required" });
|
return res.status(400).json({
|
||||||
}
|
success: false,
|
||||||
if (!config.tableName || !config.columnName) {
|
error: "tableName and columnName are required"
|
||||||
return res.status(400).json({ success: false, error: "tableName and columnName are required" });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedRule = await numberingRuleService.saveRuleToTest(config, companyCode, userId);
|
const savedRule = await numberingRuleService.saveRuleToTest(ruleConfig, companyCode, userId);
|
||||||
return res.json({ success: true, data: savedRule });
|
return res.json({ success: true, data: savedRule });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error("테스트 테이블에 채번규칙 저장 실패", {
|
logger.error("[테스트] 채번 규칙 저장 실패", { error: error.message });
|
||||||
error: error.message,
|
|
||||||
companyCode,
|
|
||||||
ruleId: config.ruleId,
|
|
||||||
});
|
|
||||||
return res.status(500).json({ success: false, error: error.message });
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// [테스트] 테스트 테이블에서 채번규칙 삭제
|
// [테스트] 테스트 테이블에서 채번 규칙 삭제
|
||||||
router.delete("/test/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
router.delete("/test/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
const { ruleId } = req.params;
|
const { ruleId } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await numberingRuleService.deleteRuleFromTest(ruleId, companyCode);
|
await numberingRuleService.deleteRuleFromTest(ruleId, companyCode);
|
||||||
return res.json({ success: true });
|
return res.json({ success: true, message: "테스트 채번 규칙이 삭제되었습니다" });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error("테스트 테이블에서 채번규칙 삭제 실패", {
|
logger.error("[테스트] 채번 규칙 삭제 실패", { error: error.message });
|
||||||
error: error.message,
|
|
||||||
companyCode,
|
|
||||||
ruleId,
|
|
||||||
});
|
|
||||||
return res.status(500).json({ success: false, error: error.message });
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// [테스트] 카테고리 조건 포함 채번규칙 조회
|
// [테스트] 코드 미리보기 (테스트 테이블 사용)
|
||||||
router.get("/test/by-column-with-category", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
router.post("/test/:ruleId/preview", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
const { tableName, columnName, categoryColumn, categoryValueId } = req.query;
|
const { ruleId } = req.params;
|
||||||
|
const { formData } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!tableName || typeof tableName !== "string") {
|
const previewCode = await numberingRuleService.previewCode(ruleId, companyCode, formData);
|
||||||
return res.status(400).json({ success: false, error: "tableName is required" });
|
return res.json({ success: true, data: { generatedCode: previewCode } });
|
||||||
}
|
|
||||||
if (!columnName || typeof columnName !== "string") {
|
|
||||||
return res.status(400).json({ success: false, error: "columnName is required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const rule = await numberingRuleService.getNumberingRuleByColumnWithCategory(
|
|
||||||
companyCode,
|
|
||||||
tableName,
|
|
||||||
columnName,
|
|
||||||
categoryColumn as string | undefined,
|
|
||||||
categoryValueId ? Number(categoryValueId) : undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!rule) {
|
|
||||||
return res.status(404).json({ success: false, error: "규칙을 찾을 수 없습니다" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({ success: true, data: rule });
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error("카테고리 조건 포함 채번규칙 조회 실패", {
|
logger.error("[테스트] 코드 미리보기 실패", { error: error.message });
|
||||||
error: error.message,
|
|
||||||
companyCode,
|
|
||||||
tableName,
|
|
||||||
columnName,
|
|
||||||
});
|
|
||||||
return res.status(500).json({ success: false, error: error.message });
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// [테스트] 테이블.컬럼별 모든 채번규칙 조회 (카테고리 조건별)
|
// ==================== 회사별 채번규칙 복제 API ====================
|
||||||
router.get("/test/rules-by-table-column", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
|
||||||
const companyCode = req.user!.companyCode;
|
// 회사별 채번규칙 복제
|
||||||
const { tableName, columnName } = req.query;
|
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: "sourceCompanyCode와 targetCompanyCode가 필요합니다"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!tableName || typeof tableName !== "string") {
|
const result = await numberingRuleService.copyRulesForCompany(sourceCompanyCode, targetCompanyCode);
|
||||||
return res.status(400).json({ success: false, error: "tableName is required" });
|
return res.json({ success: true, data: result });
|
||||||
}
|
|
||||||
if (!columnName || typeof columnName !== "string") {
|
|
||||||
return res.status(400).json({ success: false, error: "columnName is required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const rules = await numberingRuleService.getRulesByTableColumn(
|
|
||||||
companyCode,
|
|
||||||
tableName,
|
|
||||||
columnName
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.json({ success: true, data: rules });
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error("테이블.컬럼별 채번규칙 목록 조회 실패", {
|
logger.error("회사별 채번규칙 복제 실패", { error: error.message });
|
||||||
error: error.message,
|
|
||||||
companyCode,
|
|
||||||
tableName,
|
|
||||||
columnName,
|
|
||||||
});
|
|
||||||
return res.status(500).json({ success: false, error: error.message });
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -369,14 +369,19 @@ export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response
|
||||||
// 그룹에 화면 추가
|
// 그룹에 화면 추가
|
||||||
export const addScreenToGroup = async (req: AuthenticatedRequest, res: Response) => {
|
export const addScreenToGroup = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const companyCode = req.user?.companyCode || "*";
|
const userCompanyCode = req.user?.companyCode || "*";
|
||||||
const userId = req.user?.userId || "";
|
const userId = req.user?.userId || "";
|
||||||
const { group_id, screen_id, screen_role, display_order, is_default } = req.body;
|
const { group_id, screen_id, screen_role, display_order, is_default, target_company_code } = req.body;
|
||||||
|
|
||||||
if (!group_id || !screen_id) {
|
if (!group_id || !screen_id) {
|
||||||
return res.status(400).json({ success: false, message: "그룹 ID와 화면 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 = `
|
const query = `
|
||||||
INSERT INTO screen_group_screens (group_id, screen_id, screen_role, display_order, is_default, company_code, writer)
|
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)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
|
@ -388,13 +393,13 @@ export const addScreenToGroup = async (req: AuthenticatedRequest, res: Response)
|
||||||
screen_role || 'main',
|
screen_role || 'main',
|
||||||
display_order || 0,
|
display_order || 0,
|
||||||
is_default || 'N',
|
is_default || 'N',
|
||||||
companyCode === "*" ? "*" : companyCode,
|
effectiveCompanyCode,
|
||||||
userId
|
userId
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = await pool.query(query, params);
|
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: "화면이 그룹에 추가되었습니다." });
|
res.json({ success: true, data: result.rows[0], message: "화면이 그룹에 추가되었습니다." });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -2251,3 +2256,168 @@ export const syncAllCompaniesController = async (req: AuthenticatedRequest, res:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [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: "연쇄관계 설정 복제에 실패했습니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,12 @@ import {
|
||||||
getScreensByMenu,
|
getScreensByMenu,
|
||||||
unassignScreenFromMenu,
|
unassignScreenFromMenu,
|
||||||
cleanupDeletedScreenMenuAssignments,
|
cleanupDeletedScreenMenuAssignments,
|
||||||
|
updateTabScreenReferences,
|
||||||
|
copyScreenMenuAssignments,
|
||||||
|
copyCodeCategoryAndCodes,
|
||||||
|
copyCategoryMapping,
|
||||||
|
copyTableTypeColumns,
|
||||||
|
copyCascadingRelation,
|
||||||
} from "../controllers/screenManagementController";
|
} from "../controllers/screenManagementController";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
@ -83,4 +89,22 @@ router.post(
|
||||||
cleanupDeletedScreenMenuAssignments
|
cleanupDeletedScreenMenuAssignments
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 그룹 복제 완료 후 탭 컴포넌트의 screenId 참조 일괄 업데이트
|
||||||
|
router.post("/screens/update-tab-references", updateTabScreenReferences);
|
||||||
|
|
||||||
|
// 화면-메뉴 할당 복제 (다른 회사로 복제 시)
|
||||||
|
router.post("/copy-menu-assignments", copyScreenMenuAssignments);
|
||||||
|
|
||||||
|
// 코드 카테고리 + 코드 복제
|
||||||
|
router.post("/copy-code-category", copyCodeCategoryAndCodes);
|
||||||
|
|
||||||
|
// 카테고리 매핑 + 값 복제
|
||||||
|
router.post("/copy-category-mapping", copyCategoryMapping);
|
||||||
|
|
||||||
|
// 테이블 타입 컬럼 복제
|
||||||
|
router.post("/copy-table-type-columns", copyTableTypeColumns);
|
||||||
|
|
||||||
|
// 연쇄관계 설정 복제
|
||||||
|
router.post("/copy-cascading-relation", copyCascadingRelation);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ export interface MenuCopyResult {
|
||||||
copiedCategoryMappings: number;
|
copiedCategoryMappings: number;
|
||||||
copiedTableTypeColumns: number; // 테이블 타입관리 입력타입 설정
|
copiedTableTypeColumns: number; // 테이블 타입관리 입력타입 설정
|
||||||
copiedCascadingRelations: number; // 연쇄관계 설정
|
copiedCascadingRelations: number; // 연쇄관계 설정
|
||||||
|
copiedNodeFlows: number; // 노드 플로우 (제어관리)
|
||||||
|
copiedDataflowDiagrams: number; // 데이터플로우 다이어그램 (버튼 제어)
|
||||||
menuIdMap: Record<number, number>;
|
menuIdMap: Record<number, number>;
|
||||||
screenIdMap: Record<number, number>;
|
screenIdMap: Record<number, number>;
|
||||||
flowIdMap: Record<number, number>;
|
flowIdMap: Record<number, number>;
|
||||||
|
|
@ -983,6 +985,14 @@ export class MenuCopyService {
|
||||||
client
|
client
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// === 2.1단계: 노드 플로우 복사는 화면 복사에서 처리 ===
|
||||||
|
// (screenManagementService.ts의 copyScreen에서 처리)
|
||||||
|
const copiedNodeFlows = 0;
|
||||||
|
|
||||||
|
// === 2.2단계: 데이터플로우 다이어그램 복사는 화면 복사에서 처리 ===
|
||||||
|
// (screenManagementService.ts의 copyScreen에서 처리)
|
||||||
|
const copiedDataflowDiagrams = 0;
|
||||||
|
|
||||||
// 변수 초기화
|
// 변수 초기화
|
||||||
let copiedCodeCategories = 0;
|
let copiedCodeCategories = 0;
|
||||||
let copiedCodes = 0;
|
let copiedCodes = 0;
|
||||||
|
|
@ -1132,6 +1142,8 @@ export class MenuCopyService {
|
||||||
copiedCategoryMappings,
|
copiedCategoryMappings,
|
||||||
copiedTableTypeColumns,
|
copiedTableTypeColumns,
|
||||||
copiedCascadingRelations,
|
copiedCascadingRelations,
|
||||||
|
copiedNodeFlows,
|
||||||
|
copiedDataflowDiagrams,
|
||||||
menuIdMap: Object.fromEntries(menuIdMap),
|
menuIdMap: Object.fromEntries(menuIdMap),
|
||||||
screenIdMap: Object.fromEntries(screenIdMap),
|
screenIdMap: Object.fromEntries(screenIdMap),
|
||||||
flowIdMap: Object.fromEntries(flowIdMap),
|
flowIdMap: Object.fromEntries(flowIdMap),
|
||||||
|
|
@ -1144,6 +1156,8 @@ export class MenuCopyService {
|
||||||
- 메뉴: ${result.copiedMenus}개
|
- 메뉴: ${result.copiedMenus}개
|
||||||
- 화면: ${result.copiedScreens}개
|
- 화면: ${result.copiedScreens}개
|
||||||
- 플로우: ${result.copiedFlows}개
|
- 플로우: ${result.copiedFlows}개
|
||||||
|
- 노드 플로우(제어관리): ${copiedNodeFlows}개
|
||||||
|
- 데이터플로우 다이어그램(버튼 제어): ${copiedDataflowDiagrams}개
|
||||||
- 코드 카테고리: ${copiedCodeCategories}개
|
- 코드 카테고리: ${copiedCodeCategories}개
|
||||||
- 코드: ${copiedCodes}개
|
- 코드: ${copiedCodes}개
|
||||||
- 채번규칙: ${copiedNumberingRules}개
|
- 채번규칙: ${copiedNumberingRules}개
|
||||||
|
|
@ -2556,33 +2570,34 @@ export class MenuCopyService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 배치 INSERT로 채번 규칙 복사
|
// 4. 배치 INSERT로 채번 규칙 복사
|
||||||
if (rulesToCopy.length > 0) {
|
// menu 스코프인데 menu_objid 매핑이 없는 규칙은 제외 (연결 없이 복제하지 않음)
|
||||||
const ruleValues = rulesToCopy
|
const validRulesToCopy = rulesToCopy.filter((r) => {
|
||||||
|
if (r.scope_type === "menu") {
|
||||||
|
const newMenuObjid = menuIdMap.get(r.menu_objid);
|
||||||
|
if (newMenuObjid === undefined) {
|
||||||
|
logger.info(` ⏭️ 채번규칙 "${r.rule_name}" 건너뜀: 메뉴 연결 없음 (원본 menu_objid: ${r.menu_objid})`);
|
||||||
|
// ruleIdMap에서도 제거
|
||||||
|
ruleIdMap.delete(r.rule_id);
|
||||||
|
return false; // 복제 대상에서 제외
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validRulesToCopy.length > 0) {
|
||||||
|
const ruleValues = validRulesToCopy
|
||||||
.map(
|
.map(
|
||||||
(_, i) =>
|
(_, i) =>
|
||||||
`($${i * 13 + 1}, $${i * 13 + 2}, $${i * 13 + 3}, $${i * 13 + 4}, $${i * 13 + 5}, $${i * 13 + 6}, $${i * 13 + 7}, $${i * 13 + 8}, $${i * 13 + 9}, NOW(), $${i * 13 + 10}, $${i * 13 + 11}, $${i * 13 + 12}, $${i * 13 + 13})`
|
`($${i * 13 + 1}, $${i * 13 + 2}, $${i * 13 + 3}, $${i * 13 + 4}, $${i * 13 + 5}, $${i * 13 + 6}, $${i * 13 + 7}, $${i * 13 + 8}, $${i * 13 + 9}, NOW(), $${i * 13 + 10}, $${i * 13 + 11}, $${i * 13 + 12}, $${i * 13 + 13})`
|
||||||
)
|
)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
const ruleParams = rulesToCopy.flatMap((r) => {
|
const ruleParams = validRulesToCopy.flatMap((r) => {
|
||||||
const newMenuObjid = menuIdMap.get(r.menu_objid);
|
const newMenuObjid = menuIdMap.get(r.menu_objid);
|
||||||
// scope_type = 'menu'인 경우 menu_objid가 반드시 필요함 (check 제약조건)
|
// menu 스코프인 경우 반드시 menu_objid가 있음 (위에서 필터링됨)
|
||||||
// menuIdMap에 없으면 원본 menu_objid가 복사된 메뉴 범위 밖이므로
|
|
||||||
// scope_type을 'table'로 변경하거나, 매핑이 없으면 null 처리
|
|
||||||
const finalMenuObjid = newMenuObjid !== undefined ? newMenuObjid : null;
|
const finalMenuObjid = newMenuObjid !== undefined ? newMenuObjid : null;
|
||||||
// scope_type 결정 로직:
|
// scope_type은 원본 유지 (menu 스코프는 반드시 menu_objid가 있으므로)
|
||||||
// 1. menu 스코프인데 menu_objid 매핑이 없는 경우
|
const finalScopeType = r.scope_type;
|
||||||
// - table_name이 있으면 'table' 스코프로 변경
|
|
||||||
// - table_name이 없으면 'global' 스코프로 변경
|
|
||||||
// 2. 그 외에는 원본 scope_type 유지
|
|
||||||
let finalScopeType = r.scope_type;
|
|
||||||
if (r.scope_type === "menu" && finalMenuObjid === null) {
|
|
||||||
if (r.table_name) {
|
|
||||||
finalScopeType = "table"; // table_name이 있으면 table 스코프
|
|
||||||
} else {
|
|
||||||
finalScopeType = "global"; // table_name도 없으면 global 스코프
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
r.newRuleId,
|
r.newRuleId,
|
||||||
|
|
@ -2610,8 +2625,8 @@ export class MenuCopyService {
|
||||||
ruleParams
|
ruleParams
|
||||||
);
|
);
|
||||||
|
|
||||||
copiedCount = rulesToCopy.length;
|
copiedCount = validRulesToCopy.length;
|
||||||
logger.info(` ✅ 채번 규칙 ${copiedCount}개 복사`);
|
logger.info(` ✅ 채번 규칙 ${copiedCount}개 복사 (${rulesToCopy.length - validRulesToCopy.length}개 건너뜀)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4-1. 기존 채번 규칙의 menu_objid 업데이트 (새 메뉴와 연결) - 배치 처리
|
// 4-1. 기존 채번 규칙의 menu_objid 업데이트 (새 메뉴와 연결) - 배치 처리
|
||||||
|
|
@ -3324,4 +3339,175 @@ export class MenuCopyService {
|
||||||
logger.info(`✅ 연쇄관계 복사 완료: ${copiedCount}개`);
|
logger.info(`✅ 연쇄관계 복사 완료: ${copiedCount}개`);
|
||||||
return copiedCount;
|
return copiedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 노드 플로우 복사 (node_flows 테이블 - 제어관리에서 사용)
|
||||||
|
* - 원본 회사의 모든 node_flows를 대상 회사로 복사
|
||||||
|
* - 대상 회사에 같은 이름의 노드 플로우가 있으면 재사용
|
||||||
|
* - 없으면 새로 복사 (flow_data 포함)
|
||||||
|
* - 원본 ID → 새 ID 매핑 반환 (버튼의 flowId, selectedDiagramId 매핑용)
|
||||||
|
*/
|
||||||
|
private async copyNodeFlows(
|
||||||
|
sourceCompanyCode: string,
|
||||||
|
targetCompanyCode: string,
|
||||||
|
client: PoolClient
|
||||||
|
): Promise<{ copiedCount: number; nodeFlowIdMap: Map<number, number> }> {
|
||||||
|
logger.info(`📋 노드 플로우(제어관리) 복사 시작`);
|
||||||
|
const nodeFlowIdMap = new Map<number, number>();
|
||||||
|
let copiedCount = 0;
|
||||||
|
|
||||||
|
// 1. 원본 회사의 모든 node_flows 조회
|
||||||
|
const sourceFlowsResult = await client.query(
|
||||||
|
`SELECT * FROM node_flows WHERE company_code = $1`,
|
||||||
|
[sourceCompanyCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sourceFlowsResult.rows.length === 0) {
|
||||||
|
logger.info(` 📭 원본 회사에 노드 플로우 없음`);
|
||||||
|
return { copiedCount: 0, nodeFlowIdMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(` 📋 원본 노드 플로우: ${sourceFlowsResult.rows.length}개`);
|
||||||
|
|
||||||
|
// 2. 대상 회사의 기존 노드 플로우 조회 (이름 기준)
|
||||||
|
const existingFlowsResult = await client.query(
|
||||||
|
`SELECT flow_id, flow_name FROM node_flows WHERE company_code = $1`,
|
||||||
|
[targetCompanyCode]
|
||||||
|
);
|
||||||
|
const existingFlowsByName = new Map<string, number>(
|
||||||
|
existingFlowsResult.rows.map((f) => [f.flow_name, f.flow_id])
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. 복사할 플로우 필터링 + 기존 플로우 매핑
|
||||||
|
const flowsToCopy: any[] = [];
|
||||||
|
for (const flow of sourceFlowsResult.rows) {
|
||||||
|
const existingId = existingFlowsByName.get(flow.flow_name);
|
||||||
|
if (existingId) {
|
||||||
|
// 기존 플로우 재사용 - ID 매핑 추가
|
||||||
|
nodeFlowIdMap.set(flow.flow_id, existingId);
|
||||||
|
logger.info(` ♻️ 기존 노드 플로우 재사용: ${flow.flow_name} (${flow.flow_id} → ${existingId})`);
|
||||||
|
} else {
|
||||||
|
flowsToCopy.push(flow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flowsToCopy.length === 0) {
|
||||||
|
logger.info(` 📭 모든 노드 플로우가 이미 존재함 (매핑 ${nodeFlowIdMap.size}개)`);
|
||||||
|
return { copiedCount: 0, nodeFlowIdMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(` 🔄 복사할 노드 플로우: ${flowsToCopy.length}개`);
|
||||||
|
|
||||||
|
// 4. 개별 INSERT (RETURNING으로 새 ID 획득)
|
||||||
|
for (const flow of flowsToCopy) {
|
||||||
|
const insertResult = await client.query(
|
||||||
|
`INSERT INTO node_flows (flow_name, flow_description, flow_data, company_code)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING flow_id`,
|
||||||
|
[
|
||||||
|
flow.flow_name,
|
||||||
|
flow.flow_description,
|
||||||
|
JSON.stringify(flow.flow_data),
|
||||||
|
targetCompanyCode,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const newFlowId = insertResult.rows[0].flow_id;
|
||||||
|
nodeFlowIdMap.set(flow.flow_id, newFlowId);
|
||||||
|
logger.info(` ➕ 노드 플로우 복사: ${flow.flow_name} (${flow.flow_id} → ${newFlowId})`);
|
||||||
|
copiedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(` ✅ 노드 플로우 복사 완료: ${copiedCount}개, 매핑 ${nodeFlowIdMap.size}개`);
|
||||||
|
|
||||||
|
return { copiedCount, nodeFlowIdMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데이터플로우 다이어그램 복사 (dataflow_diagrams 테이블 - 버튼 제어 설정에서 사용)
|
||||||
|
* - 원본 회사의 모든 dataflow_diagrams를 대상 회사로 복사
|
||||||
|
* - 대상 회사에 같은 이름의 다이어그램이 있으면 재사용
|
||||||
|
* - 없으면 새로 복사 (relationships, node_positions, control, plan, category 포함)
|
||||||
|
* - 원본 ID → 새 ID 매핑 반환
|
||||||
|
*/
|
||||||
|
private async copyDataflowDiagrams(
|
||||||
|
sourceCompanyCode: string,
|
||||||
|
targetCompanyCode: string,
|
||||||
|
userId: string,
|
||||||
|
client: PoolClient
|
||||||
|
): Promise<{ copiedCount: number; diagramIdMap: Map<number, number> }> {
|
||||||
|
logger.info(`📋 데이터플로우 다이어그램(버튼 제어) 복사 시작`);
|
||||||
|
const diagramIdMap = new Map<number, number>();
|
||||||
|
let copiedCount = 0;
|
||||||
|
|
||||||
|
// 1. 원본 회사의 모든 dataflow_diagrams 조회
|
||||||
|
const sourceDiagramsResult = await client.query(
|
||||||
|
`SELECT * FROM dataflow_diagrams WHERE company_code = $1`,
|
||||||
|
[sourceCompanyCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sourceDiagramsResult.rows.length === 0) {
|
||||||
|
logger.info(` 📭 원본 회사에 데이터플로우 다이어그램 없음`);
|
||||||
|
return { copiedCount: 0, diagramIdMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(` 📋 원본 데이터플로우 다이어그램: ${sourceDiagramsResult.rows.length}개`);
|
||||||
|
|
||||||
|
// 2. 대상 회사의 기존 다이어그램 조회 (이름 기준)
|
||||||
|
const existingDiagramsResult = await client.query(
|
||||||
|
`SELECT diagram_id, diagram_name FROM dataflow_diagrams WHERE company_code = $1`,
|
||||||
|
[targetCompanyCode]
|
||||||
|
);
|
||||||
|
const existingDiagramsByName = new Map<string, number>(
|
||||||
|
existingDiagramsResult.rows.map((d) => [d.diagram_name, d.diagram_id])
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. 복사할 다이어그램 필터링 + 기존 다이어그램 매핑
|
||||||
|
const diagramsToCopy: any[] = [];
|
||||||
|
for (const diagram of sourceDiagramsResult.rows) {
|
||||||
|
const existingId = existingDiagramsByName.get(diagram.diagram_name);
|
||||||
|
if (existingId) {
|
||||||
|
// 기존 다이어그램 재사용 - ID 매핑 추가
|
||||||
|
diagramIdMap.set(diagram.diagram_id, existingId);
|
||||||
|
logger.info(` ♻️ 기존 다이어그램 재사용: ${diagram.diagram_name} (${diagram.diagram_id} → ${existingId})`);
|
||||||
|
} else {
|
||||||
|
diagramsToCopy.push(diagram);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diagramsToCopy.length === 0) {
|
||||||
|
logger.info(` 📭 모든 다이어그램이 이미 존재함 (매핑 ${diagramIdMap.size}개)`);
|
||||||
|
return { copiedCount: 0, diagramIdMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(` 🔄 복사할 다이어그램: ${diagramsToCopy.length}개`);
|
||||||
|
|
||||||
|
// 4. 개별 INSERT (RETURNING으로 새 ID 획득)
|
||||||
|
for (const diagram of diagramsToCopy) {
|
||||||
|
const insertResult = await client.query(
|
||||||
|
`INSERT INTO dataflow_diagrams (diagram_name, relationships, company_code, created_by, node_positions, control, plan, category)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
|
RETURNING diagram_id`,
|
||||||
|
[
|
||||||
|
diagram.diagram_name,
|
||||||
|
JSON.stringify(diagram.relationships),
|
||||||
|
targetCompanyCode,
|
||||||
|
userId,
|
||||||
|
diagram.node_positions ? JSON.stringify(diagram.node_positions) : null,
|
||||||
|
diagram.control ? JSON.stringify(diagram.control) : null,
|
||||||
|
diagram.plan ? JSON.stringify(diagram.plan) : null,
|
||||||
|
diagram.category ? JSON.stringify(diagram.category) : null,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const newDiagramId = insertResult.rows[0].diagram_id;
|
||||||
|
diagramIdMap.set(diagram.diagram_id, newDiagramId);
|
||||||
|
logger.info(` ➕ 다이어그램 복사: ${diagram.diagram_name} (${diagram.diagram_id} → ${newDiagramId})`);
|
||||||
|
copiedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(` ✅ 데이터플로우 다이어그램 복사 완료: ${copiedCount}개, 매핑 ${diagramIdMap.size}개`);
|
||||||
|
|
||||||
|
return { copiedCount, diagramIdMap };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,28 @@ export async function syncScreenGroupsToMenu(
|
||||||
[groupId, menuObjid]
|
[groupId, menuObjid]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 해당 그룹에 연결된 기본 화면으로 URL 항상 업데이트 (화면 재생성 시에도 반영)
|
||||||
|
const defaultScreenQuery = `
|
||||||
|
SELECT sd.screen_id, sd.screen_code, sd.screen_name
|
||||||
|
FROM screen_group_screens sgs
|
||||||
|
JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id
|
||||||
|
WHERE sgs.group_id = $1 AND sgs.company_code = $2
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN sgs.is_default = 'Y' THEN 0 ELSE 1 END,
|
||||||
|
sgs.display_order ASC
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
const defaultScreenResult = await client.query(defaultScreenQuery, [groupId, companyCode]);
|
||||||
|
if (defaultScreenResult.rows.length > 0) {
|
||||||
|
const defaultScreen = defaultScreenResult.rows[0];
|
||||||
|
const newMenuUrl = `/screens/${defaultScreen.screen_id}`;
|
||||||
|
await client.query(
|
||||||
|
`UPDATE menu_info SET menu_url = $1, screen_code = $2 WHERE objid = $3`,
|
||||||
|
[newMenuUrl, defaultScreen.screen_code, menuObjid]
|
||||||
|
);
|
||||||
|
logger.info("메뉴 URL 업데이트", { groupName, screenId: defaultScreen.screen_id, menuUrl: newMenuUrl });
|
||||||
|
}
|
||||||
|
|
||||||
groupToMenuMap.set(groupId, menuObjid);
|
groupToMenuMap.set(groupId, menuObjid);
|
||||||
result.linked++;
|
result.linked++;
|
||||||
result.details.push({
|
result.details.push({
|
||||||
|
|
@ -286,12 +308,34 @@ export async function syncScreenGroupsToMenu(
|
||||||
nextSeq = parseInt(maxSeqResult.rows[0].next_seq) || 1;
|
nextSeq = parseInt(maxSeqResult.rows[0].next_seq) || 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 해당 그룹에 연결된 기본 화면 조회 (is_default = 'Y' 우선, 없으면 첫 번째 화면)
|
||||||
|
let menuUrl: string | null = null;
|
||||||
|
let screenCode: string | null = null;
|
||||||
|
const defaultScreenQuery2 = `
|
||||||
|
SELECT sd.screen_id, sd.screen_code, sd.screen_name
|
||||||
|
FROM screen_group_screens sgs
|
||||||
|
JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id
|
||||||
|
WHERE sgs.group_id = $1 AND sgs.company_code = $2
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN sgs.is_default = 'Y' THEN 0 ELSE 1 END,
|
||||||
|
sgs.display_order ASC
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
const defaultScreenResult2 = await client.query(defaultScreenQuery2, [groupId, companyCode]);
|
||||||
|
if (defaultScreenResult2.rows.length > 0) {
|
||||||
|
const defaultScreen = defaultScreenResult2.rows[0];
|
||||||
|
screenCode = defaultScreen.screen_code;
|
||||||
|
menuUrl = `/screens/${defaultScreen.screen_id}`;
|
||||||
|
logger.info("기본 화면 URL 설정", { groupName, screenId: defaultScreen.screen_id, menuUrl });
|
||||||
|
}
|
||||||
|
|
||||||
// menu_info에 삽입
|
// menu_info에 삽입
|
||||||
const insertMenuQuery = `
|
const insertMenuQuery = `
|
||||||
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(), 'active', $8, $9)
|
menu_url, screen_code
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, 1, $6, $7, NOW(), 'active', $8, $9, $10, $11)
|
||||||
RETURNING objid
|
RETURNING objid
|
||||||
`;
|
`;
|
||||||
await client.query(insertMenuQuery, [
|
await client.query(insertMenuQuery, [
|
||||||
|
|
@ -304,6 +348,8 @@ export async function syncScreenGroupsToMenu(
|
||||||
userId,
|
userId,
|
||||||
groupId,
|
groupId,
|
||||||
group.description || null,
|
group.description || null,
|
||||||
|
menuUrl,
|
||||||
|
screenCode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// screen_groups에 menu_objid 업데이트
|
// screen_groups에 menu_objid 업데이트
|
||||||
|
|
@ -336,7 +382,13 @@ export async function syncScreenGroupsToMenu(
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
await client.query('ROLLBACK');
|
await client.query('ROLLBACK');
|
||||||
logger.error("화면관리 → 메뉴 동기화 실패", { companyCode, error: error.message });
|
logger.error("화면관리 → 메뉴 동기화 실패", {
|
||||||
|
companyCode,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
code: error.code,
|
||||||
|
detail: error.detail,
|
||||||
|
});
|
||||||
result.success = false;
|
result.success = false;
|
||||||
result.errors.push(error.message);
|
result.errors.push(error.message);
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -858,7 +858,13 @@ class NumberingRuleService {
|
||||||
return { ...ruleResult.rows[0], parts };
|
return { ...ruleResult.rows[0], parts };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
await client.query("ROLLBACK");
|
await client.query("ROLLBACK");
|
||||||
logger.error("채번 규칙 수정 실패", { error: error.message });
|
logger.error("채번 규칙 수정 실패", {
|
||||||
|
ruleId,
|
||||||
|
companyCode,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
updates
|
||||||
|
});
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
client.release();
|
client.release();
|
||||||
|
|
@ -1671,6 +1677,203 @@ class NumberingRuleService {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회사별 채번규칙 복제 (메뉴 동기화 완료 후 호출)
|
||||||
|
* 메뉴 이름을 기준으로 채번규칙을 대상 회사의 메뉴에 연결
|
||||||
|
* 복제 후 화면 레이아웃의 numberingRuleId 참조도 업데이트
|
||||||
|
*/
|
||||||
|
async copyRulesForCompany(
|
||||||
|
sourceCompanyCode: string,
|
||||||
|
targetCompanyCode: string
|
||||||
|
): Promise<{ copiedCount: number; skippedCount: number; details: string[]; ruleIdMap: Record<string, string> }> {
|
||||||
|
const pool = getPool();
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
|
const result = { copiedCount: 0, skippedCount: 0, details: [] as string[], ruleIdMap: {} as Record<string, string> };
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
|
// 1. 원본 회사의 채번규칙 조회 (menu + table 스코프 모두)
|
||||||
|
const sourceRulesResult = await client.query(
|
||||||
|
`SELECT nr.*, mi.menu_name_kor as source_menu_name
|
||||||
|
FROM numbering_rules nr
|
||||||
|
LEFT JOIN menu_info mi ON nr.menu_objid = mi.objid
|
||||||
|
WHERE nr.company_code = $1 AND nr.scope_type IN ('menu', 'table')`,
|
||||||
|
[sourceCompanyCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info("원본 채번규칙 조회", {
|
||||||
|
sourceCompanyCode,
|
||||||
|
count: sourceRulesResult.rowCount
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 각 채번규칙 복제
|
||||||
|
for (const rule of sourceRulesResult.rows) {
|
||||||
|
// 새 rule_id 생성
|
||||||
|
const newRuleId = `rule-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
// 이미 존재하는지 확인 (이름 기반)
|
||||||
|
const existsCheck = await client.query(
|
||||||
|
`SELECT rule_id FROM numbering_rules
|
||||||
|
WHERE company_code = $1 AND rule_name = $2`,
|
||||||
|
[targetCompanyCode, rule.rule_name]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existsCheck.rows.length > 0) {
|
||||||
|
// 이미 존재하면 매핑만 추가
|
||||||
|
result.ruleIdMap[rule.rule_id] = existsCheck.rows[0].rule_id;
|
||||||
|
result.skippedCount++;
|
||||||
|
result.details.push(`건너뜀 (이미 존재): ${rule.rule_name}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetMenuObjid = null;
|
||||||
|
|
||||||
|
// menu 스코프인 경우 대상 메뉴 찾기
|
||||||
|
if (rule.scope_type === 'menu' && rule.source_menu_name) {
|
||||||
|
const targetMenuResult = await client.query(
|
||||||
|
`SELECT objid FROM menu_info
|
||||||
|
WHERE company_code = $1 AND menu_name_kor = $2
|
||||||
|
LIMIT 1`,
|
||||||
|
[targetCompanyCode, rule.source_menu_name]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetMenuResult.rows.length === 0) {
|
||||||
|
result.skippedCount++;
|
||||||
|
result.details.push(`건너뜀 (메뉴 없음): ${rule.rule_name} - 메뉴: ${rule.source_menu_name}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetMenuObjid = targetMenuResult.rows[0].objid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 채번규칙 복제
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO numbering_rules (
|
||||||
|
rule_id, rule_name, description, separator, reset_period,
|
||||||
|
current_sequence, table_name, column_name, company_code,
|
||||||
|
created_at, updated_at, created_by, scope_type, menu_objid
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW(), $10, $11, $12)`,
|
||||||
|
[
|
||||||
|
newRuleId,
|
||||||
|
rule.rule_name,
|
||||||
|
rule.description,
|
||||||
|
rule.separator,
|
||||||
|
rule.reset_period,
|
||||||
|
0, // 시퀀스 초기화
|
||||||
|
rule.table_name,
|
||||||
|
rule.column_name,
|
||||||
|
targetCompanyCode,
|
||||||
|
rule.created_by,
|
||||||
|
rule.scope_type,
|
||||||
|
targetMenuObjid,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 채번규칙 파트 복제
|
||||||
|
const partsResult = await client.query(
|
||||||
|
`SELECT * FROM numbering_rule_parts WHERE rule_id = $1 ORDER BY part_order`,
|
||||||
|
[rule.rule_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const part of partsResult.rows) {
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO numbering_rule_parts (
|
||||||
|
rule_id, part_order, part_type, generation_method,
|
||||||
|
auto_config, manual_config, company_code, created_at
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())`,
|
||||||
|
[
|
||||||
|
newRuleId,
|
||||||
|
part.part_order,
|
||||||
|
part.part_type,
|
||||||
|
part.generation_method,
|
||||||
|
part.auto_config ? JSON.stringify(part.auto_config) : null,
|
||||||
|
part.manual_config ? JSON.stringify(part.manual_config) : null,
|
||||||
|
targetCompanyCode,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 매핑 추가
|
||||||
|
result.ruleIdMap[rule.rule_id] = newRuleId;
|
||||||
|
result.copiedCount++;
|
||||||
|
result.details.push(`복제 완료: ${rule.rule_name} (${rule.scope_type})`);
|
||||||
|
logger.info("채번규칙 복제 완료", {
|
||||||
|
ruleName: rule.rule_name,
|
||||||
|
oldRuleId: rule.rule_id,
|
||||||
|
newRuleId,
|
||||||
|
targetMenuObjid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 화면 레이아웃의 numberingRuleId 참조 업데이트
|
||||||
|
if (Object.keys(result.ruleIdMap).length > 0) {
|
||||||
|
logger.info("화면 레이아웃 numberingRuleId 참조 업데이트 시작", {
|
||||||
|
targetCompanyCode,
|
||||||
|
mappingCount: Object.keys(result.ruleIdMap).length
|
||||||
|
});
|
||||||
|
|
||||||
|
// 대상 회사의 모든 화면 레이아웃 조회
|
||||||
|
const layoutsResult = await client.query(
|
||||||
|
`SELECT sl.layout_id, sl.properties
|
||||||
|
FROM screen_layouts sl
|
||||||
|
JOIN screen_definitions sd ON sl.screen_id = sd.screen_id
|
||||||
|
WHERE sd.company_code = $1
|
||||||
|
AND sl.properties::text LIKE '%numberingRuleId%'`,
|
||||||
|
[targetCompanyCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
let updatedLayouts = 0;
|
||||||
|
|
||||||
|
for (const layout of layoutsResult.rows) {
|
||||||
|
let propsStr = JSON.stringify(layout.properties);
|
||||||
|
let updated = false;
|
||||||
|
|
||||||
|
// 각 매핑에 대해 치환
|
||||||
|
for (const [oldRuleId, newRuleId] of Object.entries(result.ruleIdMap)) {
|
||||||
|
if (propsStr.includes(`"${oldRuleId}"`)) {
|
||||||
|
propsStr = propsStr.split(`"${oldRuleId}"`).join(`"${newRuleId}"`);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
await client.query(
|
||||||
|
`UPDATE screen_layouts SET properties = $1::jsonb WHERE layout_id = $2`,
|
||||||
|
[propsStr, layout.layout_id]
|
||||||
|
);
|
||||||
|
updatedLayouts++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("화면 레이아웃 numberingRuleId 참조 업데이트 완료", {
|
||||||
|
targetCompanyCode,
|
||||||
|
updatedLayouts
|
||||||
|
});
|
||||||
|
result.details.push(`화면 레이아웃 ${updatedLayouts}개의 채번규칙 참조 업데이트`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
|
||||||
|
logger.info("회사별 채번규칙 복제 완료", {
|
||||||
|
sourceCompanyCode,
|
||||||
|
targetCompanyCode,
|
||||||
|
copiedCount: result.copiedCount,
|
||||||
|
skippedCount: result.skippedCount,
|
||||||
|
ruleIdMapCount: Object.keys(result.ruleIdMap).length
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
logger.error("회사별 채번규칙 복제 실패", { error, sourceCompanyCode, targetCompanyCode });
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const numberingRuleService = new NumberingRuleService();
|
export const numberingRuleService = new NumberingRuleService();
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -118,9 +118,9 @@ export default function ScreenManagementPage() {
|
||||||
const filteredScreens = searchKeywords.length > 1
|
const filteredScreens = searchKeywords.length > 1
|
||||||
? screens // 폴더 계층 검색 시에는 화면 필터링 없음 (폴더에서 이미 필터링됨)
|
? screens // 폴더 계층 검색 시에는 화면 필터링 없음 (폴더에서 이미 필터링됨)
|
||||||
: screens.filter((screen) =>
|
: screens.filter((screen) =>
|
||||||
screen.screenName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
screen.screenName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
screen.screenCode.toLowerCase().includes(searchTerm.toLowerCase())
|
screen.screenCode.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
// 화면 설계 모드일 때는 레이아웃 없이 전체 화면 사용
|
// 화면 설계 모드일 때는 레이아웃 없이 전체 화면 사용
|
||||||
if (isDesignMode) {
|
if (isDesignMode) {
|
||||||
|
|
|
||||||
|
|
@ -317,13 +317,16 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
// 저장 전에 현재 화면의 테이블명과 menuObjid 자동 설정
|
// 저장 전에 현재 화면의 테이블명과 menuObjid 자동 설정
|
||||||
// 메뉴 기반으로 채번규칙 관리 (menuObjid로 필터링)
|
// menuObjid가 있으면 menu 스코프, 없으면 기존 scopeType 유지
|
||||||
|
const effectiveMenuObjid = menuObjid || currentRule.menuObjid || null;
|
||||||
|
const effectiveScopeType = effectiveMenuObjid ? "menu" : (currentRule.scopeType || "global");
|
||||||
|
|
||||||
const ruleToSave = {
|
const ruleToSave = {
|
||||||
...currentRule,
|
...currentRule,
|
||||||
parts: partsWithDefaults,
|
parts: partsWithDefaults,
|
||||||
scopeType: "menu" as const, // 메뉴 기반 채번규칙
|
scopeType: effectiveScopeType as "menu" | "global", // menuObjid 유무에 따라 결정
|
||||||
tableName: currentTableName || currentRule.tableName || null, // 현재 테이블명 (참고용)
|
tableName: currentTableName || currentRule.tableName || null, // 현재 테이블명 (참고용)
|
||||||
menuObjid: menuObjid || currentRule.menuObjid || null, // 메뉴 OBJID (필터링 기준)
|
menuObjid: effectiveMenuObjid, // 메뉴 OBJID (필터링 기준)
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("💾 채번 규칙 저장:", {
|
console.log("💾 채번 규칙 저장:", {
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,10 @@ import {
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
import { Loader2, Copy, Link as LinkIcon, Trash2, AlertCircle, AlertTriangle, Check, ChevronsUpDown, FolderTree } from "lucide-react";
|
import { Loader2, Copy, Link as LinkIcon, Trash2, AlertCircle, AlertTriangle, Check, ChevronsUpDown, FolderTree, Hash, Code, Table, Settings, Database } from "lucide-react";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { ScreenDefinition } from "@/types/screen";
|
import { ScreenDefinition } from "@/types/screen";
|
||||||
import { screenApi } from "@/lib/api/screen";
|
import { screenApi, updateTabScreenReferences } from "@/lib/api/screen";
|
||||||
import { ScreenGroup, addScreenToGroup, createScreenGroup } from "@/lib/api/screenGroup";
|
import { ScreenGroup, addScreenToGroup, createScreenGroup } from "@/lib/api/screenGroup";
|
||||||
import { apiClient } from "@/lib/api/client";
|
import { apiClient } from "@/lib/api/client";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
@ -135,6 +136,15 @@ export default function CopyScreenModal({
|
||||||
// 그룹 복제 모드: "all" (전체), "folder_only" (폴더만), "screen_only" (화면만)
|
// 그룹 복제 모드: "all" (전체), "folder_only" (폴더만), "screen_only" (화면만)
|
||||||
const [groupCopyMode, setGroupCopyMode] = useState<"all" | "folder_only" | "screen_only">("all");
|
const [groupCopyMode, setGroupCopyMode] = useState<"all" | "folder_only" | "screen_only">("all");
|
||||||
|
|
||||||
|
// 채번규칙 복제 옵션 (체크 시: 복제 → 메뉴 동기화 → 채번규칙 복제 순서로 실행)
|
||||||
|
const [copyNumberingRules, setCopyNumberingRules] = useState(false);
|
||||||
|
|
||||||
|
// 추가 복사 옵션들
|
||||||
|
const [copyCodeCategory, setCopyCodeCategory] = useState(false); // 코드 카테고리 + 코드 복사
|
||||||
|
const [copyCategoryMapping, setCopyCategoryMapping] = useState(false); // 카테고리 매핑 + 값 복사
|
||||||
|
const [copyTableTypeColumns, setCopyTableTypeColumns] = useState(false); // 테이블 타입관리 입력타입 설정 복사
|
||||||
|
const [copyCascadingRelation, setCopyCascadingRelation] = useState(false); // 연쇄관계 설정 복사
|
||||||
|
|
||||||
// 복사 중 상태
|
// 복사 중 상태
|
||||||
const [isCopying, setIsCopying] = useState(false);
|
const [isCopying, setIsCopying] = useState(false);
|
||||||
const [copyProgress, setCopyProgress] = useState({ current: 0, total: 0, message: "" });
|
const [copyProgress, setCopyProgress] = useState({ current: 0, total: 0, message: "" });
|
||||||
|
|
@ -584,6 +594,7 @@ export default function CopyScreenModal({
|
||||||
screen_id: result.mainScreen.screenId,
|
screen_id: result.mainScreen.screenId,
|
||||||
screen_role: "MAIN",
|
screen_role: "MAIN",
|
||||||
display_order: 1,
|
display_order: 1,
|
||||||
|
target_company_code: finalCompanyCode, // 대상 회사 코드 전달
|
||||||
});
|
});
|
||||||
console.log(`✅ 복제된 화면을 그룹(${selectedTargetGroupId})에 추가 완료`);
|
console.log(`✅ 복제된 화면을 그룹(${selectedTargetGroupId})에 추가 완료`);
|
||||||
} catch (groupError) {
|
} catch (groupError) {
|
||||||
|
|
@ -609,7 +620,7 @@ export default function CopyScreenModal({
|
||||||
};
|
};
|
||||||
|
|
||||||
// 이름 변환 헬퍼 함수 (일괄 이름 변경 적용)
|
// 이름 변환 헬퍼 함수 (일괄 이름 변경 적용)
|
||||||
const transformName = (originalName: string, isRootGroup: boolean = false): string => {
|
const transformName = (originalName: string, isRootGroup: boolean = false, sourceCompanyCode?: string): string => {
|
||||||
// 루트 그룹은 사용자가 직접 입력한 이름 사용
|
// 루트 그룹은 사용자가 직접 입력한 이름 사용
|
||||||
if (isRootGroup) {
|
if (isRootGroup) {
|
||||||
return newGroupName.trim();
|
return newGroupName.trim();
|
||||||
|
|
@ -621,7 +632,12 @@ export default function CopyScreenModal({
|
||||||
return originalName.replace(new RegExp(groupFindText, "g"), groupReplaceText);
|
return originalName.replace(new RegExp(groupFindText, "g"), groupReplaceText);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 기본: "(복제)" 붙이기
|
// 다른 회사로 복제하는 경우: 원본 이름 그대로 사용 (중복될 일 없음)
|
||||||
|
if (sourceCompanyCode && sourceCompanyCode !== targetCompanyCode) {
|
||||||
|
return originalName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 같은 회사 내 복제: "(복제)" 붙이기 (중복 방지)
|
||||||
return `${originalName} (복제)`;
|
return `${originalName} (복제)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -633,17 +649,19 @@ export default function CopyScreenModal({
|
||||||
screenCodes: string[], // 미리 생성된 화면 코드 배열
|
screenCodes: string[], // 미리 생성된 화면 코드 배열
|
||||||
codeIndex: { current: number }, // 현재 사용할 코드 인덱스 (참조로 전달)
|
codeIndex: { current: number }, // 현재 사용할 코드 인덱스 (참조로 전달)
|
||||||
stats: { groups: number; screens: number },
|
stats: { groups: number; screens: number },
|
||||||
totalScreenCount: number // 전체 화면 수 (진행률 표시용)
|
totalScreenCount: number, // 전체 화면 수 (진행률 표시용)
|
||||||
|
screenIdMap: { [key: number]: number } // 원본 화면 ID -> 새 화면 ID 매핑
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
// 1. 현재 그룹 생성 (원본 display_order 유지)
|
// 1. 현재 그룹 생성 (원본 display_order 유지)
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const randomSuffix = Math.floor(Math.random() * 1000);
|
const randomSuffix = Math.floor(Math.random() * 1000);
|
||||||
const newGroupCode = `${targetCompany}_GROUP_${timestamp}_${randomSuffix}`;
|
const newGroupCode = `${targetCompany}_GROUP_${timestamp}_${randomSuffix}`;
|
||||||
|
|
||||||
console.log(`📁 그룹 생성: ${sourceGroupData.group_name} (복제)`);
|
const transformedGroupName = transformName(sourceGroupData.group_name, false, sourceGroupData.company_code);
|
||||||
|
console.log(`📁 그룹 생성: ${transformedGroupName}`);
|
||||||
|
|
||||||
const newGroupResponse = await createScreenGroup({
|
const newGroupResponse = await createScreenGroup({
|
||||||
group_name: transformName(sourceGroupData.group_name), // 일괄 이름 변경 적용
|
group_name: transformedGroupName, // 일괄 이름 변경 적용
|
||||||
group_code: newGroupCode,
|
group_code: newGroupCode,
|
||||||
parent_group_id: parentGroupId,
|
parent_group_id: parentGroupId,
|
||||||
target_company_code: targetCompany,
|
target_company_code: targetCompany,
|
||||||
|
|
@ -663,13 +681,29 @@ export default function CopyScreenModal({
|
||||||
const sourceScreensInfo = sourceGroupData.screens || [];
|
const sourceScreensInfo = sourceGroupData.screens || [];
|
||||||
|
|
||||||
// 화면 정보와 display_order를 함께 매핑
|
// 화면 정보와 display_order를 함께 매핑
|
||||||
|
// allScreens에서 못 찾으면 그룹의 screens 정보를 직접 사용 (다른 회사 폴더 복사 시)
|
||||||
const screensWithOrder = sourceScreensInfo.map((s: any) => {
|
const screensWithOrder = sourceScreensInfo.map((s: any) => {
|
||||||
const screenId = typeof s === 'object' ? s.screen_id : s;
|
const screenId = typeof s === 'object' ? s.screen_id : s;
|
||||||
const displayOrder = typeof s === 'object' ? s.display_order : 0;
|
const displayOrder = typeof s === 'object' ? s.display_order : 0;
|
||||||
const screenRole = typeof s === 'object' ? s.screen_role : 'MAIN';
|
const screenRole = typeof s === 'object' ? s.screen_role : 'MAIN';
|
||||||
const screenData = allScreens.find((sc) => sc.screenId === screenId);
|
const screenName = typeof s === 'object' ? s.screen_name : '';
|
||||||
|
const tableName = typeof s === 'object' ? s.table_name : '';
|
||||||
|
|
||||||
|
// allScreens에서 먼저 찾고, 없으면 그룹의 screens 정보로 대체
|
||||||
|
let screenData = allScreens.find((sc) => sc.screenId === screenId);
|
||||||
|
if (!screenData && screenId && screenName) {
|
||||||
|
// allScreens에 없는 경우 (다른 회사 화면) - 그룹의 screens 정보로 최소한의 데이터 구성
|
||||||
|
screenData = {
|
||||||
|
screenId: screenId,
|
||||||
|
screenName: screenName,
|
||||||
|
screenCode: `SCREEN_${screenId}`, // 임시 코드 (실제 복사 시 새 코드 생성)
|
||||||
|
tableName: tableName || '',
|
||||||
|
description: '',
|
||||||
|
companyCode: sourceGroupData.company_code || '',
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
return { screenId, displayOrder, screenRole, screenData };
|
return { screenId, displayOrder, screenRole, screenData };
|
||||||
}).filter(item => item.screenData); // 화면 데이터가 있는 것만
|
}).filter(item => item.screenData && item.screenId); // screenId가 유효한 것만
|
||||||
|
|
||||||
// display_order 순으로 정렬
|
// display_order 순으로 정렬
|
||||||
screensWithOrder.sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0));
|
screensWithOrder.sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0));
|
||||||
|
|
@ -687,12 +721,13 @@ export default function CopyScreenModal({
|
||||||
message: `화면 복제 중: ${screen.screenName}`
|
message: `화면 복제 중: ${screen.screenName}`
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(` 📄 화면 복제: ${screen.screenName} → ${newScreenCode}`);
|
const transformedScreenName = transformName(screen.screenName, false, sourceGroupData.company_code);
|
||||||
|
console.log(` 📄 화면 복제: ${screen.screenName} → ${transformedScreenName}`);
|
||||||
|
|
||||||
const result = await screenApi.copyScreenWithModals(screen.screenId, {
|
const result = await screenApi.copyScreenWithModals(screen.screenId, {
|
||||||
targetCompanyCode: targetCompany,
|
targetCompanyCode: targetCompany,
|
||||||
mainScreen: {
|
mainScreen: {
|
||||||
screenName: transformName(screen.screenName), // 일괄 이름 변경 적용
|
screenName: transformedScreenName, // 일괄 이름 변경 적용
|
||||||
screenCode: newScreenCode,
|
screenCode: newScreenCode,
|
||||||
description: screen.description || "",
|
description: screen.description || "",
|
||||||
},
|
},
|
||||||
|
|
@ -700,14 +735,18 @@ export default function CopyScreenModal({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.mainScreen?.screenId) {
|
if (result.mainScreen?.screenId) {
|
||||||
|
// 원본 화면 ID -> 새 화면 ID 매핑 기록
|
||||||
|
screenIdMap[screen.screenId] = result.mainScreen.screenId;
|
||||||
|
|
||||||
await addScreenToGroup({
|
await addScreenToGroup({
|
||||||
group_id: newGroup.id,
|
group_id: newGroup.id,
|
||||||
screen_id: result.mainScreen.screenId,
|
screen_id: result.mainScreen.screenId,
|
||||||
screen_role: screenRole || "MAIN",
|
screen_role: screenRole || "MAIN",
|
||||||
display_order: displayOrder, // 원본 정렬순서 유지
|
display_order: displayOrder, // 원본 정렬순서 유지
|
||||||
|
target_company_code: targetCompany, // 대상 회사 코드 전달
|
||||||
});
|
});
|
||||||
stats.screens++;
|
stats.screens++;
|
||||||
console.log(` ✅ 화면 복제 완료: ${result.mainScreen.screenName}`);
|
console.log(` ✅ 화면 복제 완료: ${result.mainScreen.screenName} (${screen.screenId} → ${result.mainScreen.screenId})`);
|
||||||
}
|
}
|
||||||
} catch (screenError) {
|
} catch (screenError) {
|
||||||
console.error(` ❌ 화면 복제 실패 (${screen.screenCode}):`, screenError);
|
console.error(` ❌ 화면 복제 실패 (${screen.screenCode}):`, screenError);
|
||||||
|
|
@ -730,7 +769,8 @@ export default function CopyScreenModal({
|
||||||
screenCodes,
|
screenCodes,
|
||||||
codeIndex,
|
codeIndex,
|
||||||
stats,
|
stats,
|
||||||
totalScreenCount
|
totalScreenCount,
|
||||||
|
screenIdMap // screenIdMap 전달
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -769,6 +809,7 @@ export default function CopyScreenModal({
|
||||||
|
|
||||||
const finalCompanyCode = targetCompanyCode || sourceGroup.company_code;
|
const finalCompanyCode = targetCompanyCode || sourceGroup.company_code;
|
||||||
const stats = { groups: 0, screens: 0 };
|
const stats = { groups: 0, screens: 0 };
|
||||||
|
const screenIdMap: { [key: number]: number } = {}; // 원본 화면 ID -> 새 화면 ID 매핑
|
||||||
|
|
||||||
console.log("🔄 그룹 복제 시작 (재귀적):", {
|
console.log("🔄 그룹 복제 시작 (재귀적):", {
|
||||||
sourceGroup: sourceGroup.group_name,
|
sourceGroup: sourceGroup.group_name,
|
||||||
|
|
@ -795,7 +836,7 @@ export default function CopyScreenModal({
|
||||||
|
|
||||||
// 일괄 이름 변경이 활성화된 경우 원본 이름에 변환 적용
|
// 일괄 이름 변경이 활성화된 경우 원본 이름에 변환 적용
|
||||||
const rootGroupName = useGroupBulkRename && groupFindText
|
const rootGroupName = useGroupBulkRename && groupFindText
|
||||||
? transformName(sourceGroup.group_name)
|
? transformName(sourceGroup.group_name, false, sourceGroup.company_code)
|
||||||
: newGroupName.trim();
|
: newGroupName.trim();
|
||||||
|
|
||||||
const newGroupResponse = await createScreenGroup({
|
const newGroupResponse = await createScreenGroup({
|
||||||
|
|
@ -818,14 +859,41 @@ export default function CopyScreenModal({
|
||||||
if (groupCopyMode !== "folder_only") {
|
if (groupCopyMode !== "folder_only") {
|
||||||
const sourceScreensInfo = sourceGroup.screens || [];
|
const sourceScreensInfo = sourceGroup.screens || [];
|
||||||
|
|
||||||
// 화면 정보와 display_order를 함께 매핑
|
// 화면 정보와 display_order를 함께 매핑
|
||||||
const screensWithOrder = sourceScreensInfo.map((s: any) => {
|
// allScreens에서 못 찾으면 그룹의 screens 정보를 직접 사용 (다른 회사 폴더 복사 시)
|
||||||
const screenId = typeof s === 'object' ? s.screen_id : s;
|
console.log(`🔍 루트 그룹 화면 매핑 시작: ${sourceScreensInfo.length}개 화면, allScreens: ${allScreens.length}개`);
|
||||||
const displayOrder = typeof s === 'object' ? s.display_order : 0;
|
const screensWithOrder = sourceScreensInfo.map((s: any) => {
|
||||||
const screenRole = typeof s === 'object' ? s.screen_role : 'MAIN';
|
const screenId = typeof s === 'object' ? s.screen_id : s;
|
||||||
const screenData = allScreens.find((sc) => sc.screenId === screenId);
|
const displayOrder = typeof s === 'object' ? s.display_order : 0;
|
||||||
return { screenId, displayOrder, screenRole, screenData };
|
const screenRole = typeof s === 'object' ? s.screen_role : 'MAIN';
|
||||||
}).filter(item => item.screenData);
|
const screenName = typeof s === 'object' ? s.screen_name : '';
|
||||||
|
const tableName = typeof s === 'object' ? s.table_name : '';
|
||||||
|
|
||||||
|
// allScreens에서 먼저 찾고, 없으면 그룹의 screens 정보로 대체
|
||||||
|
let screenData = allScreens.find((sc) => sc.screenId === screenId);
|
||||||
|
const foundInAllScreens = !!screenData;
|
||||||
|
|
||||||
|
if (!screenData && screenId && screenName) {
|
||||||
|
// allScreens에 없는 경우 (다른 회사 화면) - 그룹의 screens 정보로 최소한의 데이터 구성
|
||||||
|
console.log(` ⚠️ allScreens에서 못 찾음, 그룹 정보 사용: ${screenId} - ${screenName}`);
|
||||||
|
screenData = {
|
||||||
|
screenId: screenId,
|
||||||
|
screenName: screenName,
|
||||||
|
screenCode: `SCREEN_${screenId}`, // 임시 코드 (실제 복사 시 새 코드 생성)
|
||||||
|
tableName: tableName || '',
|
||||||
|
description: '',
|
||||||
|
companyCode: sourceGroup.company_code || '',
|
||||||
|
} as any;
|
||||||
|
} else if (screenData) {
|
||||||
|
console.log(` ✅ allScreens에서 찾음: ${screenId} - ${screenData.screenName}`);
|
||||||
|
} else {
|
||||||
|
console.log(` ❌ 화면 정보 없음: screenId=${screenId}, screenName=${screenName}`);
|
||||||
|
}
|
||||||
|
return { screenId, displayOrder, screenRole, screenData };
|
||||||
|
}).filter(item => item.screenData && item.screenId); // screenId가 유효한 것만
|
||||||
|
|
||||||
|
console.log(`🔍 매핑 완료: ${screensWithOrder.length}개 화면 복사 예정`);
|
||||||
|
screensWithOrder.forEach(item => console.log(` - ${item.screenId}: ${item.screenData?.screenName}`));
|
||||||
|
|
||||||
// display_order 순으로 정렬
|
// display_order 순으로 정렬
|
||||||
screensWithOrder.sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0));
|
screensWithOrder.sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0));
|
||||||
|
|
@ -843,12 +911,13 @@ export default function CopyScreenModal({
|
||||||
message: `화면 복제 중: ${screen.screenName}`
|
message: `화면 복제 중: ${screen.screenName}`
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`📄 화면 복제: ${screen.screenName} → ${newScreenCode}`);
|
const transformedScreenName = transformName(screen.screenName, false, sourceGroup.company_code);
|
||||||
|
console.log(`📄 화면 복제: ${screen.screenName} → ${transformedScreenName}`);
|
||||||
|
|
||||||
const result = await screenApi.copyScreenWithModals(screen.screenId, {
|
const result = await screenApi.copyScreenWithModals(screen.screenId, {
|
||||||
targetCompanyCode: finalCompanyCode,
|
targetCompanyCode: finalCompanyCode,
|
||||||
mainScreen: {
|
mainScreen: {
|
||||||
screenName: transformName(screen.screenName), // 일괄 이름 변경 적용
|
screenName: transformedScreenName, // 일괄 이름 변경 적용
|
||||||
screenCode: newScreenCode,
|
screenCode: newScreenCode,
|
||||||
description: screen.description || "",
|
description: screen.description || "",
|
||||||
},
|
},
|
||||||
|
|
@ -856,14 +925,18 @@ export default function CopyScreenModal({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.mainScreen?.screenId) {
|
if (result.mainScreen?.screenId) {
|
||||||
|
// 원본 화면 ID -> 새 화면 ID 매핑 기록
|
||||||
|
screenIdMap[screen.screenId] = result.mainScreen.screenId;
|
||||||
|
|
||||||
await addScreenToGroup({
|
await addScreenToGroup({
|
||||||
group_id: newRootGroup.id,
|
group_id: newRootGroup.id,
|
||||||
screen_id: result.mainScreen.screenId,
|
screen_id: result.mainScreen.screenId,
|
||||||
screen_role: screenRole || "MAIN",
|
screen_role: screenRole || "MAIN",
|
||||||
display_order: displayOrder, // 원본 정렬순서 유지
|
display_order: displayOrder, // 원본 정렬순서 유지
|
||||||
|
target_company_code: finalCompanyCode, // 대상 회사 코드 전달
|
||||||
});
|
});
|
||||||
stats.screens++;
|
stats.screens++;
|
||||||
console.log(`✅ 화면 복제 완료: ${result.mainScreen.screenName}`);
|
console.log(`✅ 화면 복제 완료: ${result.mainScreen.screenName} (${screen.screenId} → ${result.mainScreen.screenId})`);
|
||||||
}
|
}
|
||||||
} catch (screenError) {
|
} catch (screenError) {
|
||||||
console.error(`화면 복제 실패 (${screen.screenCode}):`, screenError);
|
console.error(`화면 복제 실패 (${screen.screenCode}):`, screenError);
|
||||||
|
|
@ -886,11 +959,180 @@ export default function CopyScreenModal({
|
||||||
screenCodes,
|
screenCodes,
|
||||||
codeIndex,
|
codeIndex,
|
||||||
stats,
|
stats,
|
||||||
totalScreenCount
|
totalScreenCount,
|
||||||
|
screenIdMap // screenIdMap 전달
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6. 탭 컴포넌트의 screenId 참조 일괄 업데이트
|
||||||
|
console.log("🔍 screenIdMap 상태:", screenIdMap, "키 개수:", Object.keys(screenIdMap).length);
|
||||||
|
if (Object.keys(screenIdMap).length > 0) {
|
||||||
|
console.log("🔗 탭 screenId 참조 업데이트 중...", screenIdMap);
|
||||||
|
setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "탭 참조 업데이트 중..." });
|
||||||
|
|
||||||
|
const targetScreenIds = Object.values(screenIdMap);
|
||||||
|
try {
|
||||||
|
const updateResult = await updateTabScreenReferences(targetScreenIds, screenIdMap);
|
||||||
|
console.log(`✅ 탭 screenId 참조 업데이트 완료: ${updateResult.updated}개 레이아웃`);
|
||||||
|
} catch (tabUpdateError) {
|
||||||
|
console.warn("탭 screenId 참조 업데이트 실패 (무시):", tabUpdateError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 채번규칙 복제 옵션이 선택된 경우 (복제 → 메뉴 동기화 → 채번규칙 복제)
|
||||||
|
if (copyNumberingRules) {
|
||||||
|
try {
|
||||||
|
setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "메뉴 동기화 중..." });
|
||||||
|
console.log("📋 메뉴 동기화 시작 (채번규칙 복제 준비)...");
|
||||||
|
|
||||||
|
// 7-1. 메뉴 동기화 (화면 그룹 → 메뉴)
|
||||||
|
const syncResponse = await apiClient.post("/screen-groups/sync/screen-to-menu", {
|
||||||
|
targetCompanyCode: finalCompanyCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (syncResponse.data?.success) {
|
||||||
|
console.log("✅ 메뉴 동기화 완료:", syncResponse.data.data);
|
||||||
|
|
||||||
|
// 7-2. 채번규칙 복제
|
||||||
|
setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "채번규칙 복제 중..." });
|
||||||
|
console.log("📋 채번규칙 복제 시작...");
|
||||||
|
|
||||||
|
const numberingResponse = await apiClient.post("/numbering-rules/copy-for-company", {
|
||||||
|
sourceCompanyCode: sourceGroup.company_code,
|
||||||
|
targetCompanyCode: finalCompanyCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (numberingResponse.data?.success) {
|
||||||
|
console.log("✅ 채번규칙 복제 완료:", numberingResponse.data.data);
|
||||||
|
toast.success(`채번규칙 ${numberingResponse.data.data?.copiedCount || 0}개가 복제되었습니다.`);
|
||||||
|
} else {
|
||||||
|
console.warn("채번규칙 복제 실패:", numberingResponse.data?.error);
|
||||||
|
toast.warning("채번규칙 복제에 실패했습니다. 수동으로 복제해주세요.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7-3. 화면-메뉴 할당 복제 (screen_menu_assignments)
|
||||||
|
setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "화면-메뉴 할당 복제 중..." });
|
||||||
|
console.log("📋 화면-메뉴 할당 복제 시작...");
|
||||||
|
|
||||||
|
const menuAssignResponse = await apiClient.post("/screen-management/copy-menu-assignments", {
|
||||||
|
sourceCompanyCode: sourceGroup.company_code,
|
||||||
|
targetCompanyCode: finalCompanyCode,
|
||||||
|
screenIdMap,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (menuAssignResponse.data?.success) {
|
||||||
|
console.log("✅ 화면-메뉴 할당 복제 완료:", menuAssignResponse.data.data);
|
||||||
|
toast.success(`화면-메뉴 할당 ${menuAssignResponse.data.data?.copiedCount || 0}개가 복제되었습니다.`);
|
||||||
|
} else {
|
||||||
|
console.warn("화면-메뉴 할당 복제 실패:", menuAssignResponse.data?.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("메뉴 동기화 실패:", syncResponse.data?.error);
|
||||||
|
toast.warning("메뉴 동기화에 실패했습니다. 채번규칙이 복제되지 않았습니다.");
|
||||||
|
}
|
||||||
|
} catch (numberingError) {
|
||||||
|
console.error("채번규칙 복제 중 오류:", numberingError);
|
||||||
|
toast.warning("채번규칙 복제 중 오류가 발생했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 코드 카테고리 + 코드 복제
|
||||||
|
if (copyCodeCategory) {
|
||||||
|
try {
|
||||||
|
setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "코드 카테고리/코드 복제 중..." });
|
||||||
|
console.log("📋 코드 카테고리/코드 복제 시작...");
|
||||||
|
|
||||||
|
const response = await apiClient.post("/screen-management/copy-code-category", {
|
||||||
|
sourceCompanyCode: sourceGroup.company_code,
|
||||||
|
targetCompanyCode: finalCompanyCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data?.success) {
|
||||||
|
console.log("✅ 코드 카테고리/코드 복제 완료:", response.data.data);
|
||||||
|
toast.success(`코드 카테고리 ${response.data.data?.copiedCategories || 0}개, 코드 ${response.data.data?.copiedCodes || 0}개가 복제되었습니다.`);
|
||||||
|
} else {
|
||||||
|
console.warn("코드 카테고리/코드 복제 실패:", response.data?.error);
|
||||||
|
toast.warning("코드 카테고리/코드 복제에 실패했습니다.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("코드 카테고리/코드 복제 중 오류:", error);
|
||||||
|
toast.warning("코드 카테고리/코드 복제 중 오류가 발생했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 카테고리 매핑 + 값 복제
|
||||||
|
if (copyCategoryMapping) {
|
||||||
|
try {
|
||||||
|
setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "카테고리 매핑/값 복제 중..." });
|
||||||
|
console.log("📋 카테고리 매핑/값 복제 시작...");
|
||||||
|
|
||||||
|
const response = await apiClient.post("/screen-management/copy-category-mapping", {
|
||||||
|
sourceCompanyCode: sourceGroup.company_code,
|
||||||
|
targetCompanyCode: finalCompanyCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data?.success) {
|
||||||
|
console.log("✅ 카테고리 매핑/값 복제 완료:", response.data.data);
|
||||||
|
toast.success(`카테고리 매핑 ${response.data.data?.copiedMappings || 0}개, 값 ${response.data.data?.copiedValues || 0}개가 복제되었습니다.`);
|
||||||
|
} else {
|
||||||
|
console.warn("카테고리 매핑/값 복제 실패:", response.data?.error);
|
||||||
|
toast.warning("카테고리 매핑/값 복제에 실패했습니다.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("카테고리 매핑/값 복제 중 오류:", error);
|
||||||
|
toast.warning("카테고리 매핑/값 복제 중 오류가 발생했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. 테이블 타입관리 입력타입 설정 복제
|
||||||
|
if (copyTableTypeColumns) {
|
||||||
|
try {
|
||||||
|
setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "테이블 타입 컬럼 복제 중..." });
|
||||||
|
console.log("📋 테이블 타입 컬럼 복제 시작...");
|
||||||
|
|
||||||
|
const response = await apiClient.post("/screen-management/copy-table-type-columns", {
|
||||||
|
sourceCompanyCode: sourceGroup.company_code,
|
||||||
|
targetCompanyCode: finalCompanyCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data?.success) {
|
||||||
|
console.log("✅ 테이블 타입 컬럼 복제 완료:", response.data.data);
|
||||||
|
toast.success(`테이블 타입 컬럼 ${response.data.data?.copiedCount || 0}개가 복제되었습니다.`);
|
||||||
|
} else {
|
||||||
|
console.warn("테이블 타입 컬럼 복제 실패:", response.data?.error);
|
||||||
|
toast.warning("테이블 타입 컬럼 복제에 실패했습니다.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("테이블 타입 컬럼 복제 중 오류:", error);
|
||||||
|
toast.warning("테이블 타입 컬럼 복제 중 오류가 발생했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. 연쇄관계 설정 복제
|
||||||
|
if (copyCascadingRelation) {
|
||||||
|
try {
|
||||||
|
setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "연쇄관계 설정 복제 중..." });
|
||||||
|
console.log("📋 연쇄관계 설정 복제 시작...");
|
||||||
|
|
||||||
|
const response = await apiClient.post("/screen-management/copy-cascading-relation", {
|
||||||
|
sourceCompanyCode: sourceGroup.company_code,
|
||||||
|
targetCompanyCode: finalCompanyCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data?.success) {
|
||||||
|
console.log("✅ 연쇄관계 설정 복제 완료:", response.data.data);
|
||||||
|
toast.success(`연쇄관계 설정 ${response.data.data?.copiedCount || 0}개가 복제되었습니다.`);
|
||||||
|
} else {
|
||||||
|
console.warn("연쇄관계 설정 복제 실패:", response.data?.error);
|
||||||
|
toast.warning("연쇄관계 설정 복제에 실패했습니다.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("연쇄관계 설정 복제 중 오류:", error);
|
||||||
|
toast.warning("연쇄관계 설정 복제 중 오류가 발생했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
`그룹 복제가 완료되었습니다! (그룹 ${stats.groups}개 + 화면 ${stats.screens}개)`
|
`그룹 복제가 완료되었습니다! (그룹 ${stats.groups}개 + 화면 ${stats.screens}개)`
|
||||||
);
|
);
|
||||||
|
|
@ -1045,6 +1287,89 @@ export default function CopyScreenModal({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 추가 복사 옵션 (선택사항) */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs sm:text-sm font-medium">추가 복사 옵션 (선택사항):</Label>
|
||||||
|
|
||||||
|
{/* 코드 카테고리 + 코드 복사 */}
|
||||||
|
<div className="flex items-center space-x-2 p-2 bg-muted/30 rounded-md ml-2">
|
||||||
|
<Checkbox
|
||||||
|
id="copyCodeCategory"
|
||||||
|
checked={copyCodeCategory}
|
||||||
|
onCheckedChange={(checked) => setCopyCodeCategory(checked === true)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="copyCodeCategory" className="text-xs sm:text-sm cursor-pointer flex items-center gap-2">
|
||||||
|
<Code className="h-4 w-4 text-muted-foreground" />
|
||||||
|
코드 카테고리 + 코드 복사
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 채번규칙 복제 */}
|
||||||
|
<div className="flex items-center space-x-2 p-2 bg-muted/30 rounded-md ml-2">
|
||||||
|
<Checkbox
|
||||||
|
id="copyNumberingRules"
|
||||||
|
checked={copyNumberingRules}
|
||||||
|
onCheckedChange={(checked) => setCopyNumberingRules(checked === true)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="copyNumberingRules" className="text-xs sm:text-sm cursor-pointer flex items-center gap-2">
|
||||||
|
<Hash className="h-4 w-4 text-muted-foreground" />
|
||||||
|
채번 규칙 복사
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 카테고리 매핑 + 값 복사 */}
|
||||||
|
<div className="flex items-center space-x-2 p-2 bg-muted/30 rounded-md ml-2">
|
||||||
|
<Checkbox
|
||||||
|
id="copyCategoryMapping"
|
||||||
|
checked={copyCategoryMapping}
|
||||||
|
onCheckedChange={(checked) => setCopyCategoryMapping(checked === true)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="copyCategoryMapping" className="text-xs sm:text-sm cursor-pointer flex items-center gap-2">
|
||||||
|
<Table className="h-4 w-4 text-muted-foreground" />
|
||||||
|
카테고리 매핑 + 값 복사
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테이블 타입관리 입력타입 설정 복사 */}
|
||||||
|
<div className="flex items-center space-x-2 p-2 bg-muted/30 rounded-md ml-2">
|
||||||
|
<Checkbox
|
||||||
|
id="copyTableTypeColumns"
|
||||||
|
checked={copyTableTypeColumns}
|
||||||
|
onCheckedChange={(checked) => setCopyTableTypeColumns(checked === true)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="copyTableTypeColumns" className="text-xs sm:text-sm cursor-pointer flex items-center gap-2">
|
||||||
|
<Settings className="h-4 w-4 text-muted-foreground" />
|
||||||
|
테이블 타입관리 입력타입 설정 복사
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 연쇄관계 설정 복사 */}
|
||||||
|
<div className="flex items-center space-x-2 p-2 bg-muted/30 rounded-md ml-2">
|
||||||
|
<Checkbox
|
||||||
|
id="copyCascadingRelation"
|
||||||
|
checked={copyCascadingRelation}
|
||||||
|
onCheckedChange={(checked) => setCopyCascadingRelation(checked === true)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="copyCascadingRelation" className="text-xs sm:text-sm cursor-pointer flex items-center gap-2">
|
||||||
|
<Database className="h-4 w-4 text-muted-foreground" />
|
||||||
|
연쇄관계 설정 복사
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 기본 복사 항목 안내 */}
|
||||||
|
<div className="p-3 bg-muted/50 rounded-lg">
|
||||||
|
<p className="text-xs font-medium mb-1">기본 복사 항목:</p>
|
||||||
|
<ul className="text-[10px] sm:text-xs text-muted-foreground space-y-0.5 list-disc list-inside">
|
||||||
|
<li>메뉴 구조 (하위 메뉴 포함)</li>
|
||||||
|
<li>화면 + 레이아웃 (모달, 조건부 컨테이너)</li>
|
||||||
|
<li>플로우 제어 (스텝, 연결)</li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-[10px] text-muted-foreground mt-2 italic">
|
||||||
|
* 코드, 채번규칙, 카테고리는 위 옵션 선택 시 복사됩니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 새 그룹명 + 정렬 순서 */}
|
{/* 새 그룹명 + 정렬 순서 */}
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
|
||||||
|
|
@ -461,5 +461,3 @@ export default function DataFlowPanel({ groupId, screenId, screens = [] }: DataF
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -413,5 +413,3 @@ export default function FieldJoinPanel({ screenId, componentId, layoutId }: Fiel
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -450,4 +450,17 @@ export const menuScreenApi = {
|
||||||
unassignScreenFromMenu: async (screenId: number, menuObjid: number): Promise<void> => {
|
unassignScreenFromMenu: async (screenId: number, menuObjid: number): Promise<void> => {
|
||||||
await apiClient.delete(`/screen-management/screens/${screenId}/menus/${menuObjid}`);
|
await apiClient.delete(`/screen-management/screens/${screenId}/menus/${menuObjid}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 그룹 복제 완료 후 탭 컴포넌트의 screenId 참조 일괄 업데이트 (별도 export)
|
||||||
|
export async function updateTabScreenReferences(
|
||||||
|
targetScreenIds: number[],
|
||||||
|
screenIdMap: { [key: number]: number }
|
||||||
|
): Promise<{ success: boolean; updated: number; details: string[] }> {
|
||||||
|
const response = await apiClient.post("/screen-management/screens/update-tab-references", {
|
||||||
|
targetScreenIds,
|
||||||
|
screenIdMap,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,7 @@ export async function addScreenToGroup(data: {
|
||||||
screen_role?: string;
|
screen_role?: string;
|
||||||
display_order?: number;
|
display_order?: number;
|
||||||
is_default?: string;
|
is_default?: string;
|
||||||
|
target_company_code?: string; // 최고 관리자가 다른 회사로 복제할 때 사용
|
||||||
}): Promise<ApiResponse<ScreenGroupScreen>> {
|
}): Promise<ApiResponse<ScreenGroupScreen>> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post("/screen-groups/group-screens", data);
|
const response = await apiClient.post("/screen-groups/group-screens", data);
|
||||||
|
|
@ -592,3 +593,71 @@ export async function syncAllCompanies(): Promise<ApiResponse<AllCompaniesSyncRe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// [PoC] screen_groups 기반 메뉴 트리 조회
|
||||||
|
// 화면관리 → 메뉴관리 통합 테스트용
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
export interface MenuTreeItem {
|
||||||
|
id: number;
|
||||||
|
objid: number | string;
|
||||||
|
name: string;
|
||||||
|
name_kor: string;
|
||||||
|
icon?: string;
|
||||||
|
url: string | null;
|
||||||
|
screen_id: number | null;
|
||||||
|
screen_code?: string;
|
||||||
|
screen_count: number;
|
||||||
|
parent_id: number | null;
|
||||||
|
level: number;
|
||||||
|
display_order: number;
|
||||||
|
is_active: boolean;
|
||||||
|
menu_objid: number | null;
|
||||||
|
children: MenuTreeItem[];
|
||||||
|
// menu_info 호환 필드
|
||||||
|
menu_name_kor: string;
|
||||||
|
menu_url: string | null;
|
||||||
|
parent_obj_id: string | null;
|
||||||
|
seq: number;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuTreeResult {
|
||||||
|
data: MenuTreeItem[];
|
||||||
|
stats: {
|
||||||
|
totalGroups: number;
|
||||||
|
groupsWithScreens: number;
|
||||||
|
groupsWithMenuObjid: number;
|
||||||
|
rootGroups: number;
|
||||||
|
};
|
||||||
|
flatList: Array<{
|
||||||
|
objid: string;
|
||||||
|
OBJID: string;
|
||||||
|
menu_name_kor: string;
|
||||||
|
MENU_NAME_KOR: string;
|
||||||
|
menu_url: string | null;
|
||||||
|
MENU_URL: string | null;
|
||||||
|
parent_obj_id: string;
|
||||||
|
PARENT_OBJ_ID: string;
|
||||||
|
seq: number;
|
||||||
|
SEQ: number;
|
||||||
|
status: string;
|
||||||
|
STATUS: string;
|
||||||
|
menu_type: number;
|
||||||
|
MENU_TYPE: number;
|
||||||
|
screen_group_id: number;
|
||||||
|
menu_objid: number | null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [PoC] screen_groups 기반 메뉴 트리 조회
|
||||||
|
export async function getMenuTreeFromScreenGroups(targetCompanyCode?: string): Promise<ApiResponse<MenuTreeResult>> {
|
||||||
|
try {
|
||||||
|
const params = targetCompanyCode ? { targetCompanyCode } : {};
|
||||||
|
const response = await apiClient.get("/screen-groups/menu-tree", { params });
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("[PoC] 메뉴 트리 조회 실패:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue