Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node

This commit is contained in:
kjs
2026-02-09 16:03:27 +09:00
parent 0ea5f3d5e4
commit f8c0fe9499
10 changed files with 8264 additions and 276 deletions

View File

@@ -1443,13 +1443,7 @@ async function collectAllChildMenuIds(parentObjid: number): Promise<number[]> {
* 메뉴 및 관련 데이터 정리 헬퍼 함수
*/
async function cleanupMenuRelatedData(menuObjid: number): Promise<void> {
// 1. category_column_mapping에서 menu_objid를 NULL로 설정
await query(
`UPDATE category_column_mapping SET menu_objid = NULL WHERE menu_objid = $1`,
[menuObjid]
);
// 2. code_category에서 menu_objid를 NULL로 설정
// 1. code_category에서 menu_objid를 NULL로 설정
await query(
`UPDATE code_category SET menu_objid = NULL WHERE menu_objid = $1`,
[menuObjid]

View File

@@ -787,6 +787,78 @@ export const updateLayerCondition = async (req: AuthenticatedRequest, res: Respo
}
};
// ========================================
// 조건부 영역(Zone) 관리
// ========================================
// Zone 목록 조회
export const getScreenZones = async (req: AuthenticatedRequest, res: Response) => {
try {
const { screenId } = req.params;
const { companyCode } = req.user as any;
const zones = await screenManagementService.getScreenZones(parseInt(screenId), companyCode);
res.json({ success: true, data: zones });
} catch (error) {
console.error("Zone 목록 조회 실패:", error);
res.status(500).json({ success: false, message: "Zone 목록 조회에 실패했습니다." });
}
};
// Zone 생성
export const createZone = async (req: AuthenticatedRequest, res: Response) => {
try {
const { screenId } = req.params;
const { companyCode } = req.user as any;
const zone = await screenManagementService.createZone(parseInt(screenId), companyCode, req.body);
res.json({ success: true, data: zone });
} catch (error) {
console.error("Zone 생성 실패:", error);
res.status(500).json({ success: false, message: "Zone 생성에 실패했습니다." });
}
};
// Zone 업데이트 (위치/크기/트리거)
export const updateZone = async (req: AuthenticatedRequest, res: Response) => {
try {
const { zoneId } = req.params;
const { companyCode } = req.user as any;
await screenManagementService.updateZone(parseInt(zoneId), companyCode, req.body);
res.json({ success: true, message: "Zone이 업데이트되었습니다." });
} catch (error) {
console.error("Zone 업데이트 실패:", error);
res.status(500).json({ success: false, message: "Zone 업데이트에 실패했습니다." });
}
};
// Zone 삭제
export const deleteZone = async (req: AuthenticatedRequest, res: Response) => {
try {
const { zoneId } = req.params;
const { companyCode } = req.user as any;
await screenManagementService.deleteZone(parseInt(zoneId), companyCode);
res.json({ success: true, message: "Zone이 삭제되었습니다." });
} catch (error) {
console.error("Zone 삭제 실패:", error);
res.status(500).json({ success: false, message: "Zone 삭제에 실패했습니다." });
}
};
// Zone에 레이어 추가
export const addLayerToZone = async (req: AuthenticatedRequest, res: Response) => {
try {
const { screenId, zoneId } = req.params;
const { companyCode } = req.user as any;
const { conditionValue, layerName } = req.body;
const result = await screenManagementService.addLayerToZone(
parseInt(screenId), companyCode, parseInt(zoneId), conditionValue, layerName
);
res.json({ success: true, data: result });
} catch (error) {
console.error("Zone 레이어 추가 실패:", error);
res.status(500).json({ success: false, message: "Zone에 레이어를 추가하지 못했습니다." });
}
};
// ========================================
// POP 레이아웃 관리 (모바일/태블릿)
// ========================================

View File

@@ -46,6 +46,11 @@ import {
getLayerLayout,
deleteLayer,
updateLayerCondition,
getScreenZones,
createZone,
updateZone,
deleteZone,
addLayerToZone,
} from "../controllers/screenManagementController";
const router = express.Router();
@@ -98,6 +103,13 @@ router.get("/screens/:screenId/layers/:layerId/layout", getLayerLayout); // 특
router.delete("/screens/:screenId/layers/:layerId", deleteLayer); // 레이어 삭제
router.put("/screens/:screenId/layers/:layerId/condition", updateLayerCondition); // 레이어 조건 설정
// 조건부 영역(Zone) 관리
router.get("/screens/:screenId/zones", getScreenZones); // Zone 목록
router.post("/screens/:screenId/zones", createZone); // Zone 생성
router.put("/zones/:zoneId", updateZone); // Zone 업데이트
router.delete("/zones/:zoneId", deleteZone); // Zone 삭제
router.post("/screens/:screenId/zones/:zoneId/layers", addLayerToZone); // Zone에 레이어 추가
// POP 레이아웃 관리 (모바일/태블릿)
router.get("/screens/:screenId/layout-pop", getLayoutPop); // POP: 모바일/태블릿용 레이아웃 조회
router.post("/screens/:screenId/layout-pop", saveLayoutPop); // POP: 모바일/태블릿용 레이아웃 저장

View File

@@ -5363,6 +5363,170 @@ export class ScreenManagementService {
);
}
// ========================================
// 조건부 영역(Zone) 관리
// ========================================
/**
* 화면의 조건부 영역(Zone) 목록 조회
*/
async getScreenZones(screenId: number, companyCode: string): Promise<any[]> {
let zones;
if (companyCode === "*") {
// 최고 관리자: 모든 회사 Zone 조회 가능
zones = await query<any>(
`SELECT * FROM screen_conditional_zones WHERE screen_id = $1 ORDER BY zone_id`,
[screenId],
);
} else {
// 일반 회사: 자사 Zone만 조회 (company_code = '*' 제외)
zones = await query<any>(
`SELECT * FROM screen_conditional_zones WHERE screen_id = $1 AND company_code = $2 ORDER BY zone_id`,
[screenId, companyCode],
);
}
return zones;
}
/**
* 조건부 영역(Zone) 생성
*/
async createZone(
screenId: number,
companyCode: string,
zoneData: {
zone_name?: string;
x: number;
y: number;
width: number;
height: number;
trigger_component_id?: string;
trigger_operator?: string;
},
): Promise<any> {
const result = await queryOne<any>(
`INSERT INTO screen_conditional_zones
(screen_id, company_code, zone_name, x, y, width, height, trigger_component_id, trigger_operator)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *`,
[
screenId,
companyCode,
zoneData.zone_name || '조건부 영역',
zoneData.x,
zoneData.y,
zoneData.width,
zoneData.height,
zoneData.trigger_component_id || null,
zoneData.trigger_operator || 'eq',
],
);
return result;
}
/**
* 조건부 영역(Zone) 업데이트 (위치/크기/트리거)
*/
async updateZone(
zoneId: number,
companyCode: string,
updates: {
zone_name?: string;
x?: number;
y?: number;
width?: number;
height?: number;
trigger_component_id?: string;
trigger_operator?: string;
},
): Promise<void> {
const setClauses: string[] = ['updated_at = NOW()'];
const params: any[] = [zoneId, companyCode];
let paramIdx = 3;
for (const [key, value] of Object.entries(updates)) {
if (value !== undefined) {
setClauses.push(`${key} = $${paramIdx}`);
params.push(value);
paramIdx++;
}
}
await query(
`UPDATE screen_conditional_zones SET ${setClauses.join(', ')}
WHERE zone_id = $1 AND company_code = $2`,
params,
);
}
/**
* 조건부 영역(Zone) 삭제 + 소속 레이어들의 condition_config 정리
*/
async deleteZone(zoneId: number, companyCode: string): Promise<void> {
// Zone에 소속된 레이어들의 condition_config에서 zone_id 제거
await query(
`UPDATE screen_layouts_v2 SET condition_config = NULL, updated_at = NOW()
WHERE company_code = $1 AND condition_config->>'zone_id' = $2::text`,
[companyCode, String(zoneId)],
);
await query(
`DELETE FROM screen_conditional_zones WHERE zone_id = $1 AND company_code = $2`,
[zoneId, companyCode],
);
}
/**
* Zone에 레이어 추가 (빈 레이아웃으로 새 레이어 생성 + zone_id 할당)
*/
async addLayerToZone(
screenId: number,
companyCode: string,
zoneId: number,
conditionValue: string,
layerName?: string,
): Promise<{ layerId: number }> {
// 다음 layer_id 계산
const maxResult = await queryOne<{ max_id: number }>(
`SELECT COALESCE(MAX(layer_id), 1) as max_id FROM screen_layouts_v2
WHERE screen_id = $1 AND company_code = $2`,
[screenId, companyCode],
);
const newLayerId = (maxResult?.max_id || 1) + 1;
// Zone 정보로 캔버스 크기 결정 (company_code 필터링 필수)
const zone = await queryOne<any>(
`SELECT * FROM screen_conditional_zones WHERE zone_id = $1 AND company_code = $2`,
[zoneId, companyCode],
);
const layoutData = {
version: "2.1",
components: [],
screenResolution: zone
? { width: zone.width, height: zone.height }
: { width: 800, height: 200 },
};
const conditionConfig = {
zone_id: zoneId,
condition_value: conditionValue,
};
await query(
`INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data, layer_id, layer_name, condition_config)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (screen_id, company_code, layer_id) DO UPDATE
SET layout_data = EXCLUDED.layout_data,
layer_name = EXCLUDED.layer_name,
condition_config = EXCLUDED.condition_config,
updated_at = NOW()`,
[screenId, companyCode, JSON.stringify(layoutData), newLayerId, layerName || `레이어 ${newLayerId}`, JSON.stringify(conditionConfig)],
);
return { layerId: newLayerId };
}
// ========================================
// POP 레이아웃 관리 (모바일/태블릿)
// v2.0: 4모드 레이아웃 지원 (태블릿 가로/세로, 모바일 가로/세로)