Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node
This commit is contained in:
@@ -2563,3 +2563,280 @@ export const getMenuTreeFromScreenGroups = async (req: AuthenticatedRequest, res
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// POP 전용 화면 그룹 API
|
||||
// hierarchy_path LIKE 'POP/%' 필터로 POP 카테고리만 조회
|
||||
// ============================================================
|
||||
|
||||
// POP 화면 그룹 목록 조회 (카테고리 트리용)
|
||||
export const getPopScreenGroups = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const { searchTerm } = req.query;
|
||||
|
||||
let whereClause = "WHERE hierarchy_path LIKE 'POP/%' OR hierarchy_path = 'POP'";
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 회사 코드 필터링 (멀티테넌시)
|
||||
if (companyCode !== "*") {
|
||||
whereClause += ` AND company_code = $${paramIndex}`;
|
||||
params.push(companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 검색어 필터링
|
||||
if (searchTerm) {
|
||||
whereClause += ` AND (group_name ILIKE $${paramIndex} OR group_code ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`;
|
||||
params.push(`%${searchTerm}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// POP 그룹 조회 (계층 구조를 위해 전체 조회)
|
||||
const dataQuery = `
|
||||
SELECT
|
||||
sg.*,
|
||||
(SELECT COUNT(*) FROM screen_group_screens sgs WHERE sgs.group_id = sg.id) as screen_count,
|
||||
(SELECT json_agg(
|
||||
json_build_object(
|
||||
'id', sgs.id,
|
||||
'screen_id', sgs.screen_id,
|
||||
'screen_name', sd.screen_name,
|
||||
'screen_role', sgs.screen_role,
|
||||
'display_order', sgs.display_order,
|
||||
'is_default', sgs.is_default,
|
||||
'table_name', sd.table_name
|
||||
) ORDER BY sgs.display_order
|
||||
) FROM screen_group_screens sgs
|
||||
LEFT JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id
|
||||
WHERE sgs.group_id = sg.id
|
||||
) as screens
|
||||
FROM screen_groups sg
|
||||
${whereClause}
|
||||
ORDER BY sg.display_order ASC, sg.hierarchy_path ASC
|
||||
`;
|
||||
|
||||
const result = await pool.query(dataQuery, params);
|
||||
|
||||
logger.info("POP 화면 그룹 목록 조회", { companyCode, count: result.rows.length });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.rows,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("POP 화면 그룹 목록 조회 실패:", error);
|
||||
res.status(500).json({ success: false, message: "POP 화면 그룹 목록 조회에 실패했습니다.", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// POP 화면 그룹 생성 (hierarchy_path 자동 설정)
|
||||
export const createPopScreenGroup = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const userCompanyCode = req.user?.companyCode || "*";
|
||||
const userId = req.user?.userId || "";
|
||||
const { group_name, group_code, description, icon, display_order, parent_group_id, target_company_code } = req.body;
|
||||
|
||||
if (!group_name || !group_code) {
|
||||
return res.status(400).json({ success: false, message: "그룹명과 그룹코드는 필수입니다." });
|
||||
}
|
||||
|
||||
// 회사 코드 결정
|
||||
const effectiveCompanyCode = target_company_code || userCompanyCode;
|
||||
if (userCompanyCode !== "*" && effectiveCompanyCode !== userCompanyCode) {
|
||||
return res.status(403).json({ success: false, message: "다른 회사의 그룹을 생성할 권한이 없습니다." });
|
||||
}
|
||||
|
||||
// hierarchy_path 계산 - POP 하위로 설정
|
||||
let hierarchyPath = "POP";
|
||||
if (parent_group_id) {
|
||||
// 부모 그룹의 hierarchy_path 조회
|
||||
const parentResult = await pool.query(
|
||||
`SELECT hierarchy_path FROM screen_groups WHERE id = $1`,
|
||||
[parent_group_id]
|
||||
);
|
||||
if (parentResult.rows.length > 0) {
|
||||
hierarchyPath = `${parentResult.rows[0].hierarchy_path}/${group_code}`;
|
||||
}
|
||||
} else {
|
||||
// 최상위 POP 카테고리
|
||||
hierarchyPath = `POP/${group_code}`;
|
||||
}
|
||||
|
||||
// 중복 체크
|
||||
const duplicateCheck = await pool.query(
|
||||
`SELECT id FROM screen_groups WHERE group_code = $1 AND company_code = $2`,
|
||||
[group_code, effectiveCompanyCode]
|
||||
);
|
||||
if (duplicateCheck.rows.length > 0) {
|
||||
return res.status(400).json({ success: false, message: "동일한 그룹코드가 이미 존재합니다." });
|
||||
}
|
||||
|
||||
// 그룹 생성 (writer 컬럼 사용, is_active는 'Y' - 기존 스키마에 맞춤)
|
||||
const insertQuery = `
|
||||
INSERT INTO screen_groups (
|
||||
group_name, group_code, description, icon, display_order,
|
||||
parent_group_id, hierarchy_path, company_code, writer, is_active
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'Y')
|
||||
RETURNING *
|
||||
`;
|
||||
const insertParams = [
|
||||
group_name,
|
||||
group_code,
|
||||
description || null,
|
||||
icon || null,
|
||||
display_order || 0,
|
||||
parent_group_id || null,
|
||||
hierarchyPath,
|
||||
effectiveCompanyCode,
|
||||
userId,
|
||||
];
|
||||
|
||||
const result = await pool.query(insertQuery, insertParams);
|
||||
|
||||
logger.info("POP 화면 그룹 생성", { groupId: result.rows[0].id, groupCode: group_code, companyCode: effectiveCompanyCode });
|
||||
|
||||
res.json({ success: true, data: result.rows[0], message: "POP 화면 그룹이 생성되었습니다." });
|
||||
} catch (error: any) {
|
||||
logger.error("POP 화면 그룹 생성 실패:", error);
|
||||
res.status(500).json({ success: false, message: "POP 화면 그룹 생성에 실패했습니다.", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// POP 화면 그룹 수정
|
||||
export const updatePopScreenGroup = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const { group_name, description, icon, display_order, is_active } = req.body;
|
||||
|
||||
// 기존 그룹 확인
|
||||
let checkQuery = `SELECT * FROM screen_groups WHERE id = $1`;
|
||||
const checkParams: any[] = [id];
|
||||
if (companyCode !== "*") {
|
||||
checkQuery += ` AND company_code = $2`;
|
||||
checkParams.push(companyCode);
|
||||
}
|
||||
|
||||
const existing = await pool.query(checkQuery, checkParams);
|
||||
if (existing.rows.length === 0) {
|
||||
return res.status(404).json({ success: false, message: "그룹을 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
// POP 그룹인지 확인
|
||||
if (!existing.rows[0].hierarchy_path?.startsWith("POP")) {
|
||||
return res.status(400).json({ success: false, message: "POP 그룹만 수정할 수 있습니다." });
|
||||
}
|
||||
|
||||
// 업데이트
|
||||
const updateQuery = `
|
||||
UPDATE screen_groups
|
||||
SET group_name = COALESCE($1, group_name),
|
||||
description = COALESCE($2, description),
|
||||
icon = COALESCE($3, icon),
|
||||
display_order = COALESCE($4, display_order),
|
||||
is_active = COALESCE($5, is_active),
|
||||
updated_date = NOW()
|
||||
WHERE id = $6
|
||||
RETURNING *
|
||||
`;
|
||||
const updateParams = [group_name, description, icon, display_order, is_active, id];
|
||||
const result = await pool.query(updateQuery, updateParams);
|
||||
|
||||
logger.info("POP 화면 그룹 수정", { groupId: id, companyCode });
|
||||
|
||||
res.json({ success: true, data: result.rows[0], message: "POP 화면 그룹이 수정되었습니다." });
|
||||
} catch (error: any) {
|
||||
logger.error("POP 화면 그룹 수정 실패:", error);
|
||||
res.status(500).json({ success: false, message: "POP 화면 그룹 수정에 실패했습니다.", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// POP 화면 그룹 삭제
|
||||
export const deletePopScreenGroup = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
// 기존 그룹 확인
|
||||
let checkQuery = `SELECT * FROM screen_groups WHERE id = $1`;
|
||||
const checkParams: any[] = [id];
|
||||
if (companyCode !== "*") {
|
||||
checkQuery += ` AND company_code = $2`;
|
||||
checkParams.push(companyCode);
|
||||
}
|
||||
|
||||
const existing = await pool.query(checkQuery, checkParams);
|
||||
if (existing.rows.length === 0) {
|
||||
return res.status(404).json({ success: false, message: "그룹을 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
// POP 그룹인지 확인
|
||||
if (!existing.rows[0].hierarchy_path?.startsWith("POP")) {
|
||||
return res.status(400).json({ success: false, message: "POP 그룹만 삭제할 수 있습니다." });
|
||||
}
|
||||
|
||||
// 하위 그룹 확인
|
||||
const childCheck = await pool.query(
|
||||
`SELECT COUNT(*) as count FROM screen_groups WHERE parent_group_id = $1`,
|
||||
[id]
|
||||
);
|
||||
if (parseInt(childCheck.rows[0].count) > 0) {
|
||||
return res.status(400).json({ success: false, message: "하위 그룹이 있어 삭제할 수 없습니다." });
|
||||
}
|
||||
|
||||
// 연결된 화면 확인
|
||||
const screenCheck = await pool.query(
|
||||
`SELECT COUNT(*) as count FROM screen_group_screens WHERE group_id = $1`,
|
||||
[id]
|
||||
);
|
||||
if (parseInt(screenCheck.rows[0].count) > 0) {
|
||||
return res.status(400).json({ success: false, message: "그룹에 연결된 화면이 있어 삭제할 수 없습니다." });
|
||||
}
|
||||
|
||||
// 삭제
|
||||
await pool.query(`DELETE FROM screen_groups WHERE id = $1`, [id]);
|
||||
|
||||
logger.info("POP 화면 그룹 삭제", { groupId: id, companyCode });
|
||||
|
||||
res.json({ success: true, message: "POP 화면 그룹이 삭제되었습니다." });
|
||||
} catch (error: any) {
|
||||
logger.error("POP 화면 그룹 삭제 실패:", error);
|
||||
res.status(500).json({ success: false, message: "POP 화면 그룹 삭제에 실패했습니다.", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// POP 루트 그룹 확보 (없으면 자동 생성)
|
||||
export const ensurePopRootGroup = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
// POP 루트 그룹 확인
|
||||
const checkQuery = `
|
||||
SELECT * FROM screen_groups
|
||||
WHERE hierarchy_path = 'POP' AND company_code = $1
|
||||
`;
|
||||
const existing = await pool.query(checkQuery, [companyCode]);
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
return res.json({ success: true, data: existing.rows[0], message: "POP 루트 그룹이 이미 존재합니다." });
|
||||
}
|
||||
|
||||
// 없으면 생성 (writer 컬럼 사용, is_active는 'Y' - 기존 스키마에 맞춤)
|
||||
const insertQuery = `
|
||||
INSERT INTO screen_groups (
|
||||
group_name, group_code, hierarchy_path, company_code,
|
||||
description, display_order, is_active, writer
|
||||
) VALUES ('POP 화면', 'POP', 'POP', $1, 'POP 화면 관리 루트', 0, 'Y', $2)
|
||||
RETURNING *
|
||||
`;
|
||||
const result = await pool.query(insertQuery, [companyCode, req.user?.userId || ""]);
|
||||
|
||||
logger.info("POP 루트 그룹 생성", { groupId: result.rows[0].id, companyCode });
|
||||
|
||||
res.json({ success: true, data: result.rows[0], message: "POP 루트 그룹이 생성되었습니다." });
|
||||
} catch (error: any) {
|
||||
logger.error("POP 루트 그룹 확보 실패:", error);
|
||||
res.status(500).json({ success: false, message: "POP 루트 그룹 확보에 실패했습니다.", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -732,7 +732,7 @@ export const saveLayoutV2 = async (req: AuthenticatedRequest, res: Response) =>
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 레이어 목록 조회
|
||||
// 레이어 목록 조회
|
||||
export const getScreenLayers = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { screenId } = req.params;
|
||||
@@ -745,7 +745,7 @@ export const getScreenLayers = async (req: AuthenticatedRequest, res: Response)
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 특정 레이어 레이아웃 조회
|
||||
// 특정 레이어 레이아웃 조회
|
||||
export const getLayerLayout = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { screenId, layerId } = req.params;
|
||||
@@ -758,7 +758,7 @@ export const getLayerLayout = async (req: AuthenticatedRequest, res: Response) =
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 레이어 삭제
|
||||
// 레이어 삭제
|
||||
export const deleteLayer = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { screenId, layerId } = req.params;
|
||||
@@ -771,7 +771,7 @@ export const deleteLayer = async (req: AuthenticatedRequest, res: Response) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 레이어 조건 설정 업데이트
|
||||
// 레이어 조건 설정 업데이트
|
||||
export const updateLayerCondition = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { screenId, layerId } = req.params;
|
||||
@@ -787,6 +787,90 @@ export const updateLayerCondition = async (req: AuthenticatedRequest, res: Respo
|
||||
}
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// POP 레이아웃 관리 (모바일/태블릿)
|
||||
// ========================================
|
||||
|
||||
// POP 레이아웃 조회
|
||||
export const getLayoutPop = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { screenId } = req.params;
|
||||
const { companyCode, userType } = req.user as any;
|
||||
const layout = await screenManagementService.getLayoutPop(
|
||||
parseInt(screenId),
|
||||
companyCode,
|
||||
userType
|
||||
);
|
||||
res.json({ success: true, data: layout });
|
||||
} catch (error) {
|
||||
console.error("POP 레이아웃 조회 실패:", error);
|
||||
res
|
||||
.status(500)
|
||||
.json({ success: false, message: "POP 레이아웃 조회에 실패했습니다." });
|
||||
}
|
||||
};
|
||||
|
||||
// POP 레이아웃 저장
|
||||
export const saveLayoutPop = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { screenId } = req.params;
|
||||
const { companyCode, userId } = req.user as any;
|
||||
const layoutData = req.body;
|
||||
|
||||
await screenManagementService.saveLayoutPop(
|
||||
parseInt(screenId),
|
||||
layoutData,
|
||||
companyCode,
|
||||
userId
|
||||
);
|
||||
res.json({ success: true, message: "POP 레이아웃이 저장되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("POP 레이아웃 저장 실패:", error);
|
||||
res
|
||||
.status(500)
|
||||
.json({ success: false, message: "POP 레이아웃 저장에 실패했습니다." });
|
||||
}
|
||||
};
|
||||
|
||||
// POP 레이아웃 삭제
|
||||
export const deleteLayoutPop = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { screenId } = req.params;
|
||||
const { companyCode } = req.user as any;
|
||||
|
||||
await screenManagementService.deleteLayoutPop(
|
||||
parseInt(screenId),
|
||||
companyCode
|
||||
);
|
||||
res.json({ success: true, message: "POP 레이아웃이 삭제되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("POP 레이아웃 삭제 실패:", error);
|
||||
res
|
||||
.status(500)
|
||||
.json({ success: false, message: "POP 레이아웃 삭제에 실패했습니다." });
|
||||
}
|
||||
};
|
||||
|
||||
// POP 레이아웃 존재하는 화면 ID 목록 조회
|
||||
export const getScreenIdsWithPopLayout = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { companyCode } = req.user as any;
|
||||
|
||||
const screenIds = await screenManagementService.getScreenIdsWithPopLayout(companyCode);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: screenIds,
|
||||
count: screenIds.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("POP 레이아웃 화면 ID 목록 조회 실패:", error);
|
||||
res
|
||||
.status(500)
|
||||
.json({ success: false, message: "POP 레이아웃 화면 ID 목록 조회에 실패했습니다." });
|
||||
}
|
||||
};
|
||||
|
||||
// 화면 코드 자동 생성
|
||||
export const generateScreenCode = async (
|
||||
req: AuthenticatedRequest,
|
||||
|
||||
@@ -36,6 +36,12 @@ import {
|
||||
syncMenuToScreenGroupsController,
|
||||
getSyncStatusController,
|
||||
syncAllCompaniesController,
|
||||
// POP 전용 화면 그룹
|
||||
getPopScreenGroups,
|
||||
createPopScreenGroup,
|
||||
updatePopScreenGroup,
|
||||
deletePopScreenGroup,
|
||||
ensurePopRootGroup,
|
||||
} from "../controllers/screenGroupController";
|
||||
|
||||
const router = Router();
|
||||
@@ -106,6 +112,15 @@ router.post("/sync/menu-to-screen", syncMenuToScreenGroupsController);
|
||||
// 전체 회사 동기화 (최고 관리자만)
|
||||
router.post("/sync/all", syncAllCompaniesController);
|
||||
|
||||
// ============================================================
|
||||
// POP 전용 화면 그룹 (hierarchy_path LIKE 'POP/%')
|
||||
// ============================================================
|
||||
router.get("/pop/groups", getPopScreenGroups);
|
||||
router.post("/pop/groups", createPopScreenGroup);
|
||||
router.put("/pop/groups/:id", updatePopScreenGroup);
|
||||
router.delete("/pop/groups/:id", deletePopScreenGroup);
|
||||
router.post("/pop/ensure-root", ensurePopRootGroup);
|
||||
|
||||
export default router;
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ import {
|
||||
getLayoutV1,
|
||||
getLayoutV2,
|
||||
saveLayoutV2,
|
||||
getLayoutPop,
|
||||
saveLayoutPop,
|
||||
deleteLayoutPop,
|
||||
getScreenIdsWithPopLayout,
|
||||
generateScreenCode,
|
||||
generateMultipleScreenCodes,
|
||||
assignScreenToMenu,
|
||||
@@ -88,12 +92,18 @@ router.get("/screens/:screenId/layout-v1", getLayoutV1); // V1: component_url +
|
||||
router.get("/screens/:screenId/layout-v2", getLayoutV2); // V2: 1 레코드 방식 (url + overrides)
|
||||
router.post("/screens/:screenId/layout-v2", saveLayoutV2); // V2: 1 레코드 방식 저장
|
||||
|
||||
// 🆕 레이어 관리
|
||||
// 레이어 관리
|
||||
router.get("/screens/:screenId/layers", getScreenLayers); // 레이어 목록
|
||||
router.get("/screens/:screenId/layers/:layerId/layout", getLayerLayout); // 특정 레이어 레이아웃
|
||||
router.delete("/screens/:screenId/layers/:layerId", deleteLayer); // 레이어 삭제
|
||||
router.put("/screens/:screenId/layers/:layerId/condition", updateLayerCondition); // 레이어 조건 설정
|
||||
|
||||
// POP 레이아웃 관리 (모바일/태블릿)
|
||||
router.get("/screens/:screenId/layout-pop", getLayoutPop); // POP: 모바일/태블릿용 레이아웃 조회
|
||||
router.post("/screens/:screenId/layout-pop", saveLayoutPop); // POP: 모바일/태블릿용 레이아웃 저장
|
||||
router.delete("/screens/:screenId/layout-pop", deleteLayoutPop); // POP: 레이아웃 삭제
|
||||
router.get("/pop-layout-screen-ids", getScreenIdsWithPopLayout); // POP: 레이아웃 존재하는 화면 ID 목록
|
||||
|
||||
// 메뉴-화면 할당 관리
|
||||
router.post("/screens/:screenId/assign-menu", assignScreenToMenu);
|
||||
router.get("/menus/:menuObjid/screens", getScreensByMenu);
|
||||
|
||||
@@ -5348,6 +5348,322 @@ export class ScreenManagementService {
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// POP 레이아웃 관리 (모바일/태블릿)
|
||||
// v2.0: 4모드 레이아웃 지원 (태블릿 가로/세로, 모바일 가로/세로)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* POP v1 → v2 마이그레이션 (백엔드)
|
||||
* - 단일 sections 배열 → 4모드별 layouts + 공유 sections/components
|
||||
*/
|
||||
private migratePopV1ToV2(v1Data: any): any {
|
||||
console.log("POP v1 → v2 마이그레이션 시작");
|
||||
|
||||
// 기본 v2 구조
|
||||
const v2Data: any = {
|
||||
version: "pop-2.0",
|
||||
layouts: {
|
||||
tablet_landscape: { sectionPositions: {}, componentPositions: {} },
|
||||
tablet_portrait: { sectionPositions: {}, componentPositions: {} },
|
||||
mobile_landscape: { sectionPositions: {}, componentPositions: {} },
|
||||
mobile_portrait: { sectionPositions: {}, componentPositions: {} },
|
||||
},
|
||||
sections: {},
|
||||
components: {},
|
||||
dataFlow: {
|
||||
sectionConnections: [],
|
||||
},
|
||||
settings: {
|
||||
touchTargetMin: 48,
|
||||
mode: "normal",
|
||||
canvasGrid: v1Data.canvasGrid || { columns: 24, rowHeight: 20, gap: 4 },
|
||||
},
|
||||
metadata: v1Data.metadata,
|
||||
};
|
||||
|
||||
// v1 섹션 배열 처리
|
||||
const sections = v1Data.sections || [];
|
||||
const modeKeys = ["tablet_landscape", "tablet_portrait", "mobile_landscape", "mobile_portrait"];
|
||||
|
||||
for (const section of sections) {
|
||||
// 섹션 정의 생성
|
||||
v2Data.sections[section.id] = {
|
||||
id: section.id,
|
||||
label: section.label,
|
||||
componentIds: (section.components || []).map((c: any) => c.id),
|
||||
innerGrid: section.innerGrid || { columns: 3, rows: 3, gap: 4 },
|
||||
style: section.style,
|
||||
};
|
||||
|
||||
// 섹션 위치 복사 (4모드 모두 동일)
|
||||
const sectionPos = section.grid || { col: 1, row: 1, colSpan: 3, rowSpan: 4 };
|
||||
for (const mode of modeKeys) {
|
||||
v2Data.layouts[mode].sectionPositions[section.id] = { ...sectionPos };
|
||||
}
|
||||
|
||||
// 컴포넌트별 처리
|
||||
for (const comp of section.components || []) {
|
||||
// 컴포넌트 정의 생성
|
||||
v2Data.components[comp.id] = {
|
||||
id: comp.id,
|
||||
type: comp.type,
|
||||
label: comp.label,
|
||||
dataBinding: comp.dataBinding,
|
||||
style: comp.style,
|
||||
config: comp.config,
|
||||
};
|
||||
|
||||
// 컴포넌트 위치 복사 (4모드 모두 동일)
|
||||
const compPos = comp.grid || { col: 1, row: 1, colSpan: 1, rowSpan: 1 };
|
||||
for (const mode of modeKeys) {
|
||||
v2Data.layouts[mode].componentPositions[comp.id] = { ...compPos };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sectionCount = Object.keys(v2Data.sections).length;
|
||||
const componentCount = Object.keys(v2Data.components).length;
|
||||
console.log(`POP v1 → v2 마이그레이션 완료: ${sectionCount}개 섹션, ${componentCount}개 컴포넌트`);
|
||||
|
||||
return v2Data;
|
||||
}
|
||||
|
||||
/**
|
||||
* POP 레이아웃 조회
|
||||
* - screen_layouts_pop 테이블에서 화면당 1개 레코드 조회
|
||||
* - v1 데이터는 자동으로 v2로 마이그레이션하여 반환
|
||||
*/
|
||||
async getLayoutPop(
|
||||
screenId: number,
|
||||
companyCode: string,
|
||||
userType?: string,
|
||||
): Promise<any | null> {
|
||||
console.log(`=== POP 레이아웃 로드 시작 ===`);
|
||||
console.log(`화면 ID: ${screenId}, 회사: ${companyCode}, 사용자 유형: ${userType}`);
|
||||
|
||||
// SUPER_ADMIN 여부 확인
|
||||
const isSuperAdmin = userType === "SUPER_ADMIN";
|
||||
|
||||
// 권한 확인
|
||||
const screens = await query<{
|
||||
company_code: string | null;
|
||||
table_name: string | null;
|
||||
}>(
|
||||
`SELECT company_code, table_name FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
|
||||
[screenId],
|
||||
);
|
||||
|
||||
if (screens.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existingScreen = screens[0];
|
||||
|
||||
// SUPER_ADMIN이 아니고 회사 코드가 다르면 권한 없음
|
||||
if (!isSuperAdmin && companyCode !== "*" && existingScreen.company_code !== companyCode) {
|
||||
throw new Error("이 화면의 POP 레이아웃을 조회할 권한이 없습니다.");
|
||||
}
|
||||
|
||||
let layout: { layout_data: any } | null = null;
|
||||
|
||||
// SUPER_ADMIN인 경우: 화면의 회사 코드로 레이아웃 조회
|
||||
if (isSuperAdmin) {
|
||||
// 1. 화면 정의의 회사 코드로 레이아웃 조회
|
||||
layout = await queryOne<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_pop
|
||||
WHERE screen_id = $1 AND company_code = $2`,
|
||||
[screenId, existingScreen.company_code],
|
||||
);
|
||||
|
||||
// 2. 화면 정의의 회사 코드로 없으면, 해당 화면의 모든 레이아웃 중 첫 번째 조회
|
||||
if (!layout) {
|
||||
layout = await queryOne<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_pop
|
||||
WHERE screen_id = $1
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1`,
|
||||
[screenId],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 일반 사용자: 회사별 우선, 없으면 공통(*) 조회
|
||||
layout = await queryOne<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_pop
|
||||
WHERE screen_id = $1 AND company_code = $2`,
|
||||
[screenId, companyCode],
|
||||
);
|
||||
|
||||
// 회사별 레이아웃이 없으면 공통(*) 레이아웃 조회
|
||||
if (!layout && companyCode !== "*") {
|
||||
layout = await queryOne<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_pop
|
||||
WHERE screen_id = $1 AND company_code = '*'`,
|
||||
[screenId],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!layout) {
|
||||
console.log(`POP 레이아웃 없음: screen_id=${screenId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const layoutData = layout.layout_data;
|
||||
|
||||
// v1 → v2 자동 마이그레이션
|
||||
if (layoutData && layoutData.version === "pop-1.0") {
|
||||
console.log("POP v1 레이아웃 감지, v2로 마이그레이션");
|
||||
return this.migratePopV1ToV2(layoutData);
|
||||
}
|
||||
|
||||
// v2 또는 버전 태그 없는 경우 (버전 태그 없으면 sections 구조 확인)
|
||||
if (layoutData && !layoutData.version && layoutData.sections && Array.isArray(layoutData.sections)) {
|
||||
console.log("버전 태그 없는 v1 레이아웃 감지, v2로 마이그레이션");
|
||||
return this.migratePopV1ToV2({ ...layoutData, version: "pop-1.0" });
|
||||
}
|
||||
|
||||
// v2 레이아웃 그대로 반환
|
||||
const sectionCount = layoutData?.sections ? Object.keys(layoutData.sections).length : 0;
|
||||
const componentCount = layoutData?.components ? Object.keys(layoutData.components).length : 0;
|
||||
console.log(`POP v2 레이아웃 로드 완료: ${sectionCount}개 섹션, ${componentCount}개 컴포넌트`);
|
||||
|
||||
return layoutData;
|
||||
}
|
||||
|
||||
/**
|
||||
* POP 레이아웃 저장
|
||||
* - screen_layouts_pop 테이블에 화면당 1개 레코드 저장
|
||||
* - v3 형식 지원 (version: "pop-3.0", 섹션 제거)
|
||||
* - v2/v1 하위 호환
|
||||
*/
|
||||
async saveLayoutPop(
|
||||
screenId: number,
|
||||
layoutData: any,
|
||||
companyCode: string,
|
||||
userId?: string,
|
||||
): Promise<void> {
|
||||
console.log(`=== POP 레이아웃 저장 (v5 그리드 시스템) ===`);
|
||||
console.log(`화면 ID: ${screenId}, 회사: ${companyCode}`);
|
||||
|
||||
// v5 그리드 레이아웃만 지원
|
||||
const componentCount = Object.keys(layoutData.components || {}).length;
|
||||
console.log(`컴포넌트: ${componentCount}개`);
|
||||
|
||||
// v5 형식 검증
|
||||
if (layoutData.version && layoutData.version !== "pop-5.0") {
|
||||
console.warn(`레거시 버전 감지 (${layoutData.version}), v5로 변환 필요`);
|
||||
}
|
||||
|
||||
// 권한 확인
|
||||
const screens = await query<{ company_code: string | null }>(
|
||||
`SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
|
||||
[screenId],
|
||||
);
|
||||
|
||||
if (screens.length === 0) {
|
||||
throw new Error("화면을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
const existingScreen = screens[0];
|
||||
|
||||
if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
|
||||
throw new Error("이 화면의 POP 레이아웃을 저장할 권한이 없습니다.");
|
||||
}
|
||||
|
||||
// SUPER_ADMIN인 경우: 화면 정의의 company_code로 저장 (로드와 동일하게)
|
||||
const targetCompanyCode = companyCode === "*"
|
||||
? (existingScreen.company_code || "*")
|
||||
: companyCode;
|
||||
|
||||
console.log(`저장 대상 company_code: ${targetCompanyCode} (사용자: ${companyCode}, 화면: ${existingScreen.company_code})`);
|
||||
|
||||
// v5 그리드 레이아웃으로 저장 (단일 버전)
|
||||
const dataToSave = {
|
||||
...layoutData,
|
||||
version: "pop-5.0",
|
||||
};
|
||||
console.log(`저장: gridConfig=${JSON.stringify(dataToSave.gridConfig || 'default')}`)
|
||||
|
||||
// UPSERT (있으면 업데이트, 없으면 삽입)
|
||||
await query(
|
||||
`INSERT INTO screen_layouts_pop (screen_id, company_code, layout_data, created_at, updated_at, created_by, updated_by)
|
||||
VALUES ($1, $2, $3, NOW(), NOW(), $4, $4)
|
||||
ON CONFLICT (screen_id, company_code)
|
||||
DO UPDATE SET layout_data = $3, updated_at = NOW(), updated_by = $4`,
|
||||
[screenId, targetCompanyCode, JSON.stringify(dataToSave), userId || null],
|
||||
);
|
||||
|
||||
console.log(`POP 레이아웃 저장 완료 (version: ${dataToSave.version}, company: ${targetCompanyCode})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* POP 레이아웃이 존재하는 화면 ID 목록 조회
|
||||
* - 옵션 B: POP 레이아웃 존재 여부로 화면 구분
|
||||
*/
|
||||
async getScreenIdsWithPopLayout(
|
||||
companyCode: string,
|
||||
): Promise<number[]> {
|
||||
console.log(`=== POP 레이아웃 존재 화면 ID 조회 ===`);
|
||||
console.log(`회사 코드: ${companyCode}`);
|
||||
|
||||
let result: { screen_id: number }[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 모든 POP 레이아웃 조회
|
||||
result = await query<{ screen_id: number }>(
|
||||
`SELECT DISTINCT screen_id FROM screen_layouts_pop`,
|
||||
[],
|
||||
);
|
||||
} else {
|
||||
// 일반 회사: 해당 회사 또는 공통(*) 레이아웃 조회
|
||||
result = await query<{ screen_id: number }>(
|
||||
`SELECT DISTINCT screen_id FROM screen_layouts_pop
|
||||
WHERE company_code = $1 OR company_code = '*'`,
|
||||
[companyCode],
|
||||
);
|
||||
}
|
||||
|
||||
const screenIds = result.map((r) => r.screen_id);
|
||||
console.log(`POP 레이아웃 존재 화면 수: ${screenIds.length}개`);
|
||||
return screenIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* POP 레이아웃 삭제
|
||||
*/
|
||||
async deleteLayoutPop(
|
||||
screenId: number,
|
||||
companyCode: string,
|
||||
): Promise<boolean> {
|
||||
console.log(`=== POP 레이아웃 삭제 시작 ===`);
|
||||
console.log(`화면 ID: ${screenId}, 회사: ${companyCode}`);
|
||||
|
||||
// 권한 확인
|
||||
const screens = await query<{ company_code: string | null }>(
|
||||
`SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
|
||||
[screenId],
|
||||
);
|
||||
|
||||
if (screens.length === 0) {
|
||||
throw new Error("화면을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
const existingScreen = screens[0];
|
||||
|
||||
if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
|
||||
throw new Error("이 화면의 POP 레이아웃을 삭제할 권한이 없습니다.");
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
`DELETE FROM screen_layouts_pop WHERE screen_id = $1 AND company_code = $2`,
|
||||
[screenId, companyCode],
|
||||
);
|
||||
|
||||
console.log(`POP 레이아웃 삭제 완료`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 서비스 인스턴스 export
|
||||
|
||||
Reference in New Issue
Block a user