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 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 });
|
||||
}
|
||||
});
|
||||
|
|
@ -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 { tableName, columnName } = req.query;
|
||||
const { tableName, columnName } = req.params;
|
||||
|
||||
try {
|
||||
if (!tableName || typeof tableName !== "string") {
|
||||
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: "규칙을 찾을 수 없습니다" });
|
||||
}
|
||||
|
||||
const rule = await numberingRuleService.getNumberingRuleByColumn(companyCode, tableName, columnName);
|
||||
return res.json({ success: true, data: rule });
|
||||
} catch (error: any) {
|
||||
logger.error("테이블+컬럼 기반 채번규칙 조회 실패", {
|
||||
error: error.message,
|
||||
companyCode,
|
||||
tableName,
|
||||
columnName,
|
||||
});
|
||||
logger.error("테이블+컬럼 기반 채번 규칙 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// [테스트] 테스트 테이블에 채번규칙 저장
|
||||
// [테스트] 테스트 테이블에 채번 규칙 저장
|
||||
router.post("/test/save", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||
const companyCode = req.user!.companyCode;
|
||||
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 {
|
||||
if (!config.ruleId || !config.ruleName) {
|
||||
return res.status(400).json({ success: false, error: "ruleId and ruleName are required" });
|
||||
}
|
||||
if (!config.tableName || !config.columnName) {
|
||||
return res.status(400).json({ success: false, error: "tableName and columnName are required" });
|
||||
if (!ruleConfig.tableName || !ruleConfig.columnName) {
|
||||
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 });
|
||||
} catch (error: any) {
|
||||
logger.error("테스트 테이블에 채번규칙 저장 실패", {
|
||||
error: error.message,
|
||||
companyCode,
|
||||
ruleId: config.ruleId,
|
||||
});
|
||||
logger.error("[테스트] 채번 규칙 저장 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// [테스트] 테스트 테이블에서 채번규칙 삭제
|
||||
// [테스트] 테스트 테이블에서 채번 규칙 삭제
|
||||
router.delete("/test/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { ruleId } = req.params;
|
||||
|
||||
try {
|
||||
await numberingRuleService.deleteRuleFromTest(ruleId, companyCode);
|
||||
return res.json({ success: true });
|
||||
return res.json({ success: true, message: "테스트 채번 규칙이 삭제되었습니다" });
|
||||
} catch (error: any) {
|
||||
logger.error("테스트 테이블에서 채번규칙 삭제 실패", {
|
||||
error: error.message,
|
||||
companyCode,
|
||||
ruleId,
|
||||
});
|
||||
logger.error("[테스트] 채번 규칙 삭제 실패", { 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 { tableName, columnName, categoryColumn, categoryValueId } = req.query;
|
||||
const { ruleId } = req.params;
|
||||
const { formData } = req.body;
|
||||
|
||||
try {
|
||||
if (!tableName || typeof tableName !== "string") {
|
||||
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.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 });
|
||||
const previewCode = await numberingRuleService.previewCode(ruleId, companyCode, formData);
|
||||
return res.json({ success: true, data: { generatedCode: previewCode } });
|
||||
} catch (error: any) {
|
||||
logger.error("카테고리 조건 포함 채번규칙 조회 실패", {
|
||||
error: error.message,
|
||||
companyCode,
|
||||
tableName,
|
||||
columnName,
|
||||
});
|
||||
logger.error("[테스트] 코드 미리보기 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// [테스트] 테이블.컬럼별 모든 채번규칙 조회 (카테고리 조건별)
|
||||
router.get("/test/rules-by-table-column", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { tableName, columnName } = req.query;
|
||||
// ==================== 회사별 채번규칙 복제 API ====================
|
||||
|
||||
// 회사별 채번규칙 복제
|
||||
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 {
|
||||
if (!tableName || typeof tableName !== "string") {
|
||||
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 rules = await numberingRuleService.getRulesByTableColumn(
|
||||
companyCode,
|
||||
tableName,
|
||||
columnName
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: rules });
|
||||
const result = await numberingRuleService.copyRulesForCompany(sourceCompanyCode, targetCompanyCode);
|
||||
return res.json({ success: true, data: result });
|
||||
} catch (error: any) {
|
||||
logger.error("테이블.컬럼별 채번규칙 목록 조회 실패", {
|
||||
error: error.message,
|
||||
companyCode,
|
||||
tableName,
|
||||
columnName,
|
||||
});
|
||||
logger.error("회사별 채번규칙 복제 실패", { 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) => {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const userCompanyCode = req.user?.companyCode || "*";
|
||||
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) {
|
||||
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)
|
||||
|
|
@ -388,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) {
|
||||
|
|
@ -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,
|
||||
unassignScreenFromMenu,
|
||||
cleanupDeletedScreenMenuAssignments,
|
||||
updateTabScreenReferences,
|
||||
copyScreenMenuAssignments,
|
||||
copyCodeCategoryAndCodes,
|
||||
copyCategoryMapping,
|
||||
copyTableTypeColumns,
|
||||
copyCascadingRelation,
|
||||
} from "../controllers/screenManagementController";
|
||||
|
||||
const router = express.Router();
|
||||
|
|
@ -83,4 +89,22 @@ router.post(
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ export interface MenuCopyResult {
|
|||
copiedCategoryMappings: number;
|
||||
copiedTableTypeColumns: number; // 테이블 타입관리 입력타입 설정
|
||||
copiedCascadingRelations: number; // 연쇄관계 설정
|
||||
copiedNodeFlows: number; // 노드 플로우 (제어관리)
|
||||
copiedDataflowDiagrams: number; // 데이터플로우 다이어그램 (버튼 제어)
|
||||
menuIdMap: Record<number, number>;
|
||||
screenIdMap: Record<number, number>;
|
||||
flowIdMap: Record<number, number>;
|
||||
|
|
@ -983,6 +985,14 @@ export class MenuCopyService {
|
|||
client
|
||||
);
|
||||
|
||||
// === 2.1단계: 노드 플로우 복사는 화면 복사에서 처리 ===
|
||||
// (screenManagementService.ts의 copyScreen에서 처리)
|
||||
const copiedNodeFlows = 0;
|
||||
|
||||
// === 2.2단계: 데이터플로우 다이어그램 복사는 화면 복사에서 처리 ===
|
||||
// (screenManagementService.ts의 copyScreen에서 처리)
|
||||
const copiedDataflowDiagrams = 0;
|
||||
|
||||
// 변수 초기화
|
||||
let copiedCodeCategories = 0;
|
||||
let copiedCodes = 0;
|
||||
|
|
@ -1132,6 +1142,8 @@ export class MenuCopyService {
|
|||
copiedCategoryMappings,
|
||||
copiedTableTypeColumns,
|
||||
copiedCascadingRelations,
|
||||
copiedNodeFlows,
|
||||
copiedDataflowDiagrams,
|
||||
menuIdMap: Object.fromEntries(menuIdMap),
|
||||
screenIdMap: Object.fromEntries(screenIdMap),
|
||||
flowIdMap: Object.fromEntries(flowIdMap),
|
||||
|
|
@ -1144,6 +1156,8 @@ export class MenuCopyService {
|
|||
- 메뉴: ${result.copiedMenus}개
|
||||
- 화면: ${result.copiedScreens}개
|
||||
- 플로우: ${result.copiedFlows}개
|
||||
- 노드 플로우(제어관리): ${copiedNodeFlows}개
|
||||
- 데이터플로우 다이어그램(버튼 제어): ${copiedDataflowDiagrams}개
|
||||
- 코드 카테고리: ${copiedCodeCategories}개
|
||||
- 코드: ${copiedCodes}개
|
||||
- 채번규칙: ${copiedNumberingRules}개
|
||||
|
|
@ -2556,33 +2570,34 @@ export class MenuCopyService {
|
|||
}
|
||||
|
||||
// 4. 배치 INSERT로 채번 규칙 복사
|
||||
if (rulesToCopy.length > 0) {
|
||||
const ruleValues = rulesToCopy
|
||||
// menu 스코프인데 menu_objid 매핑이 없는 규칙은 제외 (연결 없이 복제하지 않음)
|
||||
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(
|
||||
(_, 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})`
|
||||
)
|
||||
.join(", ");
|
||||
|
||||
const ruleParams = rulesToCopy.flatMap((r) => {
|
||||
const ruleParams = validRulesToCopy.flatMap((r) => {
|
||||
const newMenuObjid = menuIdMap.get(r.menu_objid);
|
||||
// scope_type = 'menu'인 경우 menu_objid가 반드시 필요함 (check 제약조건)
|
||||
// menuIdMap에 없으면 원본 menu_objid가 복사된 메뉴 범위 밖이므로
|
||||
// scope_type을 'table'로 변경하거나, 매핑이 없으면 null 처리
|
||||
// menu 스코프인 경우 반드시 menu_objid가 있음 (위에서 필터링됨)
|
||||
const finalMenuObjid = newMenuObjid !== undefined ? newMenuObjid : null;
|
||||
// scope_type 결정 로직:
|
||||
// 1. menu 스코프인데 menu_objid 매핑이 없는 경우
|
||||
// - 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 스코프
|
||||
}
|
||||
}
|
||||
// scope_type은 원본 유지 (menu 스코프는 반드시 menu_objid가 있으므로)
|
||||
const finalScopeType = r.scope_type;
|
||||
|
||||
return [
|
||||
r.newRuleId,
|
||||
|
|
@ -2610,8 +2625,8 @@ export class MenuCopyService {
|
|||
ruleParams
|
||||
);
|
||||
|
||||
copiedCount = rulesToCopy.length;
|
||||
logger.info(` ✅ 채번 규칙 ${copiedCount}개 복사`);
|
||||
copiedCount = validRulesToCopy.length;
|
||||
logger.info(` ✅ 채번 규칙 ${copiedCount}개 복사 (${rulesToCopy.length - validRulesToCopy.length}개 건너뜀)`);
|
||||
}
|
||||
|
||||
// 4-1. 기존 채번 규칙의 menu_objid 업데이트 (새 메뉴와 연결) - 배치 처리
|
||||
|
|
@ -3324,4 +3339,175 @@ export class MenuCopyService {
|
|||
logger.info(`✅ 연쇄관계 복사 완료: ${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]
|
||||
);
|
||||
|
||||
// 해당 그룹에 연결된 기본 화면으로 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);
|
||||
result.linked++;
|
||||
result.details.push({
|
||||
|
|
@ -286,12 +308,34 @@ export async function syncScreenGroupsToMenu(
|
|||
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에 삽입
|
||||
const insertMenuQuery = `
|
||||
INSERT INTO menu_info (
|
||||
objid, parent_obj_id, menu_name_kor, menu_name_eng,
|
||||
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)
|
||||
seq, menu_type, company_code, writer, regdate, status, screen_group_id, menu_desc,
|
||||
menu_url, screen_code
|
||||
) VALUES ($1, $2, $3, $4, $5, 1, $6, $7, NOW(), 'active', $8, $9, $10, $11)
|
||||
RETURNING objid
|
||||
`;
|
||||
await client.query(insertMenuQuery, [
|
||||
|
|
@ -304,6 +348,8 @@ export async function syncScreenGroupsToMenu(
|
|||
userId,
|
||||
groupId,
|
||||
group.description || null,
|
||||
menuUrl,
|
||||
screenCode,
|
||||
]);
|
||||
|
||||
// screen_groups에 menu_objid 업데이트
|
||||
|
|
@ -336,7 +382,13 @@ export async function syncScreenGroupsToMenu(
|
|||
|
||||
} catch (error: any) {
|
||||
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.errors.push(error.message);
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -858,7 +858,13 @@ class NumberingRuleService {
|
|||
return { ...ruleResult.rows[0], parts };
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("채번 규칙 수정 실패", { error: error.message });
|
||||
logger.error("채번 규칙 수정 실패", {
|
||||
ruleId,
|
||||
companyCode,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
updates
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
|
|
@ -1671,6 +1677,203 @@ class NumberingRuleService {
|
|||
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();
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -118,9 +118,9 @@ export default function ScreenManagementPage() {
|
|||
const filteredScreens = searchKeywords.length > 1
|
||||
? screens // 폴더 계층 검색 시에는 화면 필터링 없음 (폴더에서 이미 필터링됨)
|
||||
: screens.filter((screen) =>
|
||||
screen.screenName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
screen.screenCode.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
screen.screenName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
screen.screenCode.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
// 화면 설계 모드일 때는 레이아웃 없이 전체 화면 사용
|
||||
if (isDesignMode) {
|
||||
|
|
|
|||
|
|
@ -317,13 +317,16 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
|||
});
|
||||
|
||||
// 저장 전에 현재 화면의 테이블명과 menuObjid 자동 설정
|
||||
// 메뉴 기반으로 채번규칙 관리 (menuObjid로 필터링)
|
||||
// menuObjid가 있으면 menu 스코프, 없으면 기존 scopeType 유지
|
||||
const effectiveMenuObjid = menuObjid || currentRule.menuObjid || null;
|
||||
const effectiveScopeType = effectiveMenuObjid ? "menu" : (currentRule.scopeType || "global");
|
||||
|
||||
const ruleToSave = {
|
||||
...currentRule,
|
||||
parts: partsWithDefaults,
|
||||
scopeType: "menu" as const, // 메뉴 기반 채번규칙
|
||||
scopeType: effectiveScopeType as "menu" | "global", // menuObjid 유무에 따라 결정
|
||||
tableName: currentTableName || currentRule.tableName || null, // 현재 테이블명 (참고용)
|
||||
menuObjid: menuObjid || currentRule.menuObjid || null, // 메뉴 OBJID (필터링 기준)
|
||||
menuObjid: effectiveMenuObjid, // 메뉴 OBJID (필터링 기준)
|
||||
};
|
||||
|
||||
console.log("💾 채번 규칙 저장:", {
|
||||
|
|
|
|||
|
|
@ -33,9 +33,10 @@ import {
|
|||
CommandItem,
|
||||
CommandList,
|
||||
} 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 { screenApi } from "@/lib/api/screen";
|
||||
import { screenApi, updateTabScreenReferences } from "@/lib/api/screen";
|
||||
import { ScreenGroup, addScreenToGroup, createScreenGroup } from "@/lib/api/screenGroup";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { toast } from "sonner";
|
||||
|
|
@ -135,6 +136,15 @@ export default function CopyScreenModal({
|
|||
// 그룹 복제 모드: "all" (전체), "folder_only" (폴더만), "screen_only" (화면만)
|
||||
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 [copyProgress, setCopyProgress] = useState({ current: 0, total: 0, message: "" });
|
||||
|
|
@ -584,6 +594,7 @@ export default function CopyScreenModal({
|
|||
screen_id: result.mainScreen.screenId,
|
||||
screen_role: "MAIN",
|
||||
display_order: 1,
|
||||
target_company_code: finalCompanyCode, // 대상 회사 코드 전달
|
||||
});
|
||||
console.log(`✅ 복제된 화면을 그룹(${selectedTargetGroupId})에 추가 완료`);
|
||||
} 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) {
|
||||
return newGroupName.trim();
|
||||
|
|
@ -621,7 +632,12 @@ export default function CopyScreenModal({
|
|||
return originalName.replace(new RegExp(groupFindText, "g"), groupReplaceText);
|
||||
}
|
||||
|
||||
// 기본: "(복제)" 붙이기
|
||||
// 다른 회사로 복제하는 경우: 원본 이름 그대로 사용 (중복될 일 없음)
|
||||
if (sourceCompanyCode && sourceCompanyCode !== targetCompanyCode) {
|
||||
return originalName;
|
||||
}
|
||||
|
||||
// 같은 회사 내 복제: "(복제)" 붙이기 (중복 방지)
|
||||
return `${originalName} (복제)`;
|
||||
};
|
||||
|
||||
|
|
@ -633,17 +649,19 @@ export default function CopyScreenModal({
|
|||
screenCodes: string[], // 미리 생성된 화면 코드 배열
|
||||
codeIndex: { current: number }, // 현재 사용할 코드 인덱스 (참조로 전달)
|
||||
stats: { groups: number; screens: number },
|
||||
totalScreenCount: number // 전체 화면 수 (진행률 표시용)
|
||||
totalScreenCount: number, // 전체 화면 수 (진행률 표시용)
|
||||
screenIdMap: { [key: number]: number } // 원본 화면 ID -> 새 화면 ID 매핑
|
||||
): Promise<void> => {
|
||||
// 1. 현재 그룹 생성 (원본 display_order 유지)
|
||||
const timestamp = Date.now();
|
||||
const randomSuffix = Math.floor(Math.random() * 1000);
|
||||
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({
|
||||
group_name: transformName(sourceGroupData.group_name), // 일괄 이름 변경 적용
|
||||
group_name: transformedGroupName, // 일괄 이름 변경 적용
|
||||
group_code: newGroupCode,
|
||||
parent_group_id: parentGroupId,
|
||||
target_company_code: targetCompany,
|
||||
|
|
@ -663,13 +681,29 @@ export default function CopyScreenModal({
|
|||
const sourceScreensInfo = sourceGroupData.screens || [];
|
||||
|
||||
// 화면 정보와 display_order를 함께 매핑
|
||||
// allScreens에서 못 찾으면 그룹의 screens 정보를 직접 사용 (다른 회사 폴더 복사 시)
|
||||
const screensWithOrder = sourceScreensInfo.map((s: any) => {
|
||||
const screenId = typeof s === 'object' ? s.screen_id : s;
|
||||
const displayOrder = typeof s === 'object' ? s.display_order : 0;
|
||||
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 };
|
||||
}).filter(item => item.screenData); // 화면 데이터가 있는 것만
|
||||
}).filter(item => item.screenData && item.screenId); // screenId가 유효한 것만
|
||||
|
||||
// display_order 순으로 정렬
|
||||
screensWithOrder.sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0));
|
||||
|
|
@ -687,12 +721,13 @@ export default function CopyScreenModal({
|
|||
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, {
|
||||
targetCompanyCode: targetCompany,
|
||||
mainScreen: {
|
||||
screenName: transformName(screen.screenName), // 일괄 이름 변경 적용
|
||||
screenName: transformedScreenName, // 일괄 이름 변경 적용
|
||||
screenCode: newScreenCode,
|
||||
description: screen.description || "",
|
||||
},
|
||||
|
|
@ -700,14 +735,18 @@ export default function CopyScreenModal({
|
|||
});
|
||||
|
||||
if (result.mainScreen?.screenId) {
|
||||
// 원본 화면 ID -> 새 화면 ID 매핑 기록
|
||||
screenIdMap[screen.screenId] = result.mainScreen.screenId;
|
||||
|
||||
await addScreenToGroup({
|
||||
group_id: newGroup.id,
|
||||
screen_id: result.mainScreen.screenId,
|
||||
screen_role: screenRole || "MAIN",
|
||||
display_order: displayOrder, // 원본 정렬순서 유지
|
||||
target_company_code: targetCompany, // 대상 회사 코드 전달
|
||||
});
|
||||
stats.screens++;
|
||||
console.log(` ✅ 화면 복제 완료: ${result.mainScreen.screenName}`);
|
||||
console.log(` ✅ 화면 복제 완료: ${result.mainScreen.screenName} (${screen.screenId} → ${result.mainScreen.screenId})`);
|
||||
}
|
||||
} catch (screenError) {
|
||||
console.error(` ❌ 화면 복제 실패 (${screen.screenCode}):`, screenError);
|
||||
|
|
@ -730,7 +769,8 @@ export default function CopyScreenModal({
|
|||
screenCodes,
|
||||
codeIndex,
|
||||
stats,
|
||||
totalScreenCount
|
||||
totalScreenCount,
|
||||
screenIdMap // screenIdMap 전달
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -769,6 +809,7 @@ export default function CopyScreenModal({
|
|||
|
||||
const finalCompanyCode = targetCompanyCode || sourceGroup.company_code;
|
||||
const stats = { groups: 0, screens: 0 };
|
||||
const screenIdMap: { [key: number]: number } = {}; // 원본 화면 ID -> 새 화면 ID 매핑
|
||||
|
||||
console.log("🔄 그룹 복제 시작 (재귀적):", {
|
||||
sourceGroup: sourceGroup.group_name,
|
||||
|
|
@ -795,7 +836,7 @@ export default function CopyScreenModal({
|
|||
|
||||
// 일괄 이름 변경이 활성화된 경우 원본 이름에 변환 적용
|
||||
const rootGroupName = useGroupBulkRename && groupFindText
|
||||
? transformName(sourceGroup.group_name)
|
||||
? transformName(sourceGroup.group_name, false, sourceGroup.company_code)
|
||||
: newGroupName.trim();
|
||||
|
||||
const newGroupResponse = await createScreenGroup({
|
||||
|
|
@ -818,14 +859,41 @@ export default function CopyScreenModal({
|
|||
if (groupCopyMode !== "folder_only") {
|
||||
const sourceScreensInfo = sourceGroup.screens || [];
|
||||
|
||||
// 화면 정보와 display_order를 함께 매핑
|
||||
const screensWithOrder = sourceScreensInfo.map((s: any) => {
|
||||
const screenId = typeof s === 'object' ? s.screen_id : s;
|
||||
const displayOrder = typeof s === 'object' ? s.display_order : 0;
|
||||
const screenRole = typeof s === 'object' ? s.screen_role : 'MAIN';
|
||||
const screenData = allScreens.find((sc) => sc.screenId === screenId);
|
||||
return { screenId, displayOrder, screenRole, screenData };
|
||||
}).filter(item => item.screenData);
|
||||
// 화면 정보와 display_order를 함께 매핑
|
||||
// allScreens에서 못 찾으면 그룹의 screens 정보를 직접 사용 (다른 회사 폴더 복사 시)
|
||||
console.log(`🔍 루트 그룹 화면 매핑 시작: ${sourceScreensInfo.length}개 화면, allScreens: ${allScreens.length}개`);
|
||||
const screensWithOrder = sourceScreensInfo.map((s: any) => {
|
||||
const screenId = typeof s === 'object' ? s.screen_id : s;
|
||||
const displayOrder = typeof s === 'object' ? s.display_order : 0;
|
||||
const screenRole = typeof s === 'object' ? s.screen_role : 'MAIN';
|
||||
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 순으로 정렬
|
||||
screensWithOrder.sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0));
|
||||
|
|
@ -843,12 +911,13 @@ export default function CopyScreenModal({
|
|||
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, {
|
||||
targetCompanyCode: finalCompanyCode,
|
||||
mainScreen: {
|
||||
screenName: transformName(screen.screenName), // 일괄 이름 변경 적용
|
||||
screenName: transformedScreenName, // 일괄 이름 변경 적용
|
||||
screenCode: newScreenCode,
|
||||
description: screen.description || "",
|
||||
},
|
||||
|
|
@ -856,14 +925,18 @@ export default function CopyScreenModal({
|
|||
});
|
||||
|
||||
if (result.mainScreen?.screenId) {
|
||||
// 원본 화면 ID -> 새 화면 ID 매핑 기록
|
||||
screenIdMap[screen.screenId] = result.mainScreen.screenId;
|
||||
|
||||
await addScreenToGroup({
|
||||
group_id: newRootGroup.id,
|
||||
screen_id: result.mainScreen.screenId,
|
||||
screen_role: screenRole || "MAIN",
|
||||
display_order: displayOrder, // 원본 정렬순서 유지
|
||||
target_company_code: finalCompanyCode, // 대상 회사 코드 전달
|
||||
});
|
||||
stats.screens++;
|
||||
console.log(`✅ 화면 복제 완료: ${result.mainScreen.screenName}`);
|
||||
console.log(`✅ 화면 복제 완료: ${result.mainScreen.screenName} (${screen.screenId} → ${result.mainScreen.screenId})`);
|
||||
}
|
||||
} catch (screenError) {
|
||||
console.error(`화면 복제 실패 (${screen.screenCode}):`, screenError);
|
||||
|
|
@ -886,11 +959,180 @@ export default function CopyScreenModal({
|
|||
screenCodes,
|
||||
codeIndex,
|
||||
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(
|
||||
`그룹 복제가 완료되었습니다! (그룹 ${stats.groups}개 + 화면 ${stats.screens}개)`
|
||||
);
|
||||
|
|
@ -1045,6 +1287,89 @@ export default function CopyScreenModal({
|
|||
</p>
|
||||
</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-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> => {
|
||||
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;
|
||||
display_order?: number;
|
||||
is_default?: string;
|
||||
target_company_code?: string; // 최고 관리자가 다른 회사로 복제할 때 사용
|
||||
}): Promise<ApiResponse<ScreenGroupScreen>> {
|
||||
try {
|
||||
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