Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node

This commit is contained in:
kjs
2026-02-09 13:27:59 +09:00
78 changed files with 23661 additions and 36 deletions

View File

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

View File

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

View File

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

View File

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

View File

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