984 lines
35 KiB
TypeScript
984 lines
35 KiB
TypeScript
|
|
import { Request, Response } from "express";
|
||
|
|
import { getPool } from "../database/db";
|
||
|
|
import { logger } from "../utils/logger";
|
||
|
|
|
||
|
|
// pool 인스턴스 가져오기
|
||
|
|
const pool = getPool();
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// 화면 그룹 (screen_groups) CRUD
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
// 화면 그룹 목록 조회
|
||
|
|
export const getScreenGroups = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
const { page = 1, size = 20, searchTerm } = req.query;
|
||
|
|
const offset = (parseInt(page as string) - 1) * parseInt(size as string);
|
||
|
|
|
||
|
|
let whereClause = "WHERE 1=1";
|
||
|
|
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++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 전체 개수 조회
|
||
|
|
const countQuery = `SELECT COUNT(*) as total FROM screen_groups ${whereClause}`;
|
||
|
|
const countResult = await pool.query(countQuery, params);
|
||
|
|
const total = parseInt(countResult.rows[0].total);
|
||
|
|
|
||
|
|
// 데이터 조회
|
||
|
|
const dataQuery = `
|
||
|
|
SELECT
|
||
|
|
sg.*,
|
||
|
|
(SELECT COUNT(*) FROM screen_group_screens sgs WHERE sgs.group_id = sg.id) as screen_count
|
||
|
|
FROM screen_groups sg
|
||
|
|
${whereClause}
|
||
|
|
ORDER BY sg.display_order ASC, sg.created_date DESC
|
||
|
|
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||
|
|
`;
|
||
|
|
params.push(parseInt(size as string), offset);
|
||
|
|
|
||
|
|
const result = await pool.query(dataQuery, params);
|
||
|
|
|
||
|
|
logger.info("화면 그룹 목록 조회", { companyCode, total, count: result.rows.length });
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: result.rows,
|
||
|
|
total,
|
||
|
|
page: parseInt(page as string),
|
||
|
|
size: parseInt(size as string),
|
||
|
|
totalPages: Math.ceil(total / parseInt(size as string)),
|
||
|
|
});
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("화면 그룹 목록 조회 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "화면 그룹 목록 조회에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 화면 그룹 상세 조회
|
||
|
|
export const getScreenGroup = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
|
||
|
|
let query = `
|
||
|
|
SELECT sg.*,
|
||
|
|
(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
|
||
|
|
) 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
|
||
|
|
WHERE sg.id = $1
|
||
|
|
`;
|
||
|
|
const params: any[] = [id];
|
||
|
|
|
||
|
|
// 멀티테넌시 필터링
|
||
|
|
if (companyCode !== "*") {
|
||
|
|
query += ` AND sg.company_code = $2`;
|
||
|
|
params.push(companyCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
if (result.rows.length === 0) {
|
||
|
|
return res.status(404).json({ success: false, message: "화면 그룹을 찾을 수 없습니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({ success: true, data: result.rows[0] });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("화면 그룹 상세 조회 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "화면 그룹 조회에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 화면 그룹 생성
|
||
|
|
export const createScreenGroup = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
const userId = (req.user as any).userId;
|
||
|
|
const { group_name, group_code, main_table_name, description, icon, display_order, is_active } = req.body;
|
||
|
|
|
||
|
|
if (!group_name || !group_code) {
|
||
|
|
return res.status(400).json({ success: false, message: "그룹명과 그룹코드는 필수입니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
const query = `
|
||
|
|
INSERT INTO screen_groups (group_name, group_code, main_table_name, description, icon, display_order, is_active, company_code, writer)
|
||
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||
|
|
RETURNING *
|
||
|
|
`;
|
||
|
|
const params = [
|
||
|
|
group_name,
|
||
|
|
group_code,
|
||
|
|
main_table_name || null,
|
||
|
|
description || null,
|
||
|
|
icon || null,
|
||
|
|
display_order || 0,
|
||
|
|
is_active || 'Y',
|
||
|
|
companyCode === "*" ? "*" : companyCode,
|
||
|
|
userId
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
logger.info("화면 그룹 생성", { companyCode, groupId: result.rows[0].id, groupName: group_name });
|
||
|
|
|
||
|
|
res.json({ success: true, data: result.rows[0], message: "화면 그룹이 생성되었습니다." });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("화면 그룹 생성 실패:", error);
|
||
|
|
if (error.code === '23505') {
|
||
|
|
return res.status(400).json({ success: false, message: "이미 존재하는 그룹 코드입니다." });
|
||
|
|
}
|
||
|
|
res.status(500).json({ success: false, message: "화면 그룹 생성에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 화면 그룹 수정
|
||
|
|
export const updateScreenGroup = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
const { group_name, group_code, main_table_name, description, icon, display_order, is_active } = req.body;
|
||
|
|
|
||
|
|
let query = `
|
||
|
|
UPDATE screen_groups
|
||
|
|
SET group_name = $1, group_code = $2, main_table_name = $3, description = $4,
|
||
|
|
icon = $5, display_order = $6, is_active = $7, updated_date = NOW()
|
||
|
|
WHERE id = $8
|
||
|
|
`;
|
||
|
|
const params: any[] = [group_name, group_code, main_table_name, description, icon, display_order, is_active, id];
|
||
|
|
|
||
|
|
// 멀티테넌시 필터링
|
||
|
|
if (companyCode !== "*") {
|
||
|
|
query += ` AND company_code = $9`;
|
||
|
|
params.push(companyCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
query += " RETURNING *";
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
if (result.rows.length === 0) {
|
||
|
|
return res.status(404).json({ success: false, message: "화면 그룹을 찾을 수 없거나 권한이 없습니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info("화면 그룹 수정", { companyCode, groupId: id });
|
||
|
|
|
||
|
|
res.json({ success: true, data: result.rows[0], message: "화면 그룹이 수정되었습니다." });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("화면 그룹 수정 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "화면 그룹 수정에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 화면 그룹 삭제
|
||
|
|
export const deleteScreenGroup = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
|
||
|
|
let query = `DELETE FROM screen_groups WHERE id = $1`;
|
||
|
|
const params: any[] = [id];
|
||
|
|
|
||
|
|
if (companyCode !== "*") {
|
||
|
|
query += ` AND company_code = $2`;
|
||
|
|
params.push(companyCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
query += " RETURNING id";
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
if (result.rows.length === 0) {
|
||
|
|
return res.status(404).json({ success: false, message: "화면 그룹을 찾을 수 없거나 권한이 없습니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info("화면 그룹 삭제", { companyCode, groupId: id });
|
||
|
|
|
||
|
|
res.json({ success: true, message: "화면 그룹이 삭제되었습니다." });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("화면 그룹 삭제 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "화면 그룹 삭제에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// 화면-그룹 연결 (screen_group_screens) CRUD
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
// 그룹에 화면 추가
|
||
|
|
export const addScreenToGroup = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
const userId = (req.user as any).userId;
|
||
|
|
const { group_id, screen_id, screen_role, display_order, is_default } = req.body;
|
||
|
|
|
||
|
|
if (!group_id || !screen_id) {
|
||
|
|
return res.status(400).json({ success: false, message: "그룹 ID와 화면 ID는 필수입니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
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)
|
||
|
|
RETURNING *
|
||
|
|
`;
|
||
|
|
const params = [
|
||
|
|
group_id,
|
||
|
|
screen_id,
|
||
|
|
screen_role || 'main',
|
||
|
|
display_order || 0,
|
||
|
|
is_default || 'N',
|
||
|
|
companyCode === "*" ? "*" : companyCode,
|
||
|
|
userId
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
logger.info("화면-그룹 연결 추가", { companyCode, groupId: group_id, screenId: screen_id });
|
||
|
|
|
||
|
|
res.json({ success: true, data: result.rows[0], message: "화면이 그룹에 추가되었습니다." });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("화면-그룹 연결 추가 실패:", error);
|
||
|
|
if (error.code === '23505') {
|
||
|
|
return res.status(400).json({ success: false, message: "이미 그룹에 추가된 화면입니다." });
|
||
|
|
}
|
||
|
|
res.status(500).json({ success: false, message: "화면 추가에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 그룹에서 화면 제거
|
||
|
|
export const removeScreenFromGroup = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
|
||
|
|
let query = `DELETE FROM screen_group_screens WHERE id = $1`;
|
||
|
|
const params: any[] = [id];
|
||
|
|
|
||
|
|
if (companyCode !== "*") {
|
||
|
|
query += ` AND company_code = $2`;
|
||
|
|
params.push(companyCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
query += " RETURNING id";
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
if (result.rows.length === 0) {
|
||
|
|
return res.status(404).json({ success: false, message: "연결을 찾을 수 없거나 권한이 없습니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info("화면-그룹 연결 제거", { companyCode, id });
|
||
|
|
|
||
|
|
res.json({ success: true, message: "화면이 그룹에서 제거되었습니다." });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("화면-그룹 연결 제거 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "화면 제거에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 그룹 내 화면 순서/역할 수정
|
||
|
|
export const updateScreenInGroup = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
const { screen_role, display_order, is_default } = req.body;
|
||
|
|
|
||
|
|
let query = `
|
||
|
|
UPDATE screen_group_screens
|
||
|
|
SET screen_role = $1, display_order = $2, is_default = $3, updated_date = NOW()
|
||
|
|
WHERE id = $4
|
||
|
|
`;
|
||
|
|
const params: any[] = [screen_role, display_order, is_default, id];
|
||
|
|
|
||
|
|
if (companyCode !== "*") {
|
||
|
|
query += ` AND company_code = $5`;
|
||
|
|
params.push(companyCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
query += " RETURNING *";
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
if (result.rows.length === 0) {
|
||
|
|
return res.status(404).json({ success: false, message: "연결을 찾을 수 없거나 권한이 없습니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({ success: true, data: result.rows[0], message: "화면 정보가 수정되었습니다." });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("화면-그룹 연결 수정 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "화면 정보 수정에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// 화면 필드 조인 설정 (screen_field_joins) CRUD
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
// 화면 필드 조인 목록 조회
|
||
|
|
export const getFieldJoins = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
const { screen_id } = req.query;
|
||
|
|
|
||
|
|
let query = `
|
||
|
|
SELECT sfj.*,
|
||
|
|
tl1.table_label as save_table_label,
|
||
|
|
tl2.table_label as join_table_label
|
||
|
|
FROM screen_field_joins sfj
|
||
|
|
LEFT JOIN table_labels tl1 ON sfj.save_table = tl1.table_name
|
||
|
|
LEFT JOIN table_labels tl2 ON sfj.join_table = tl2.table_name
|
||
|
|
WHERE 1=1
|
||
|
|
`;
|
||
|
|
const params: any[] = [];
|
||
|
|
let paramIndex = 1;
|
||
|
|
|
||
|
|
if (companyCode !== "*") {
|
||
|
|
query += ` AND sfj.company_code = $${paramIndex}`;
|
||
|
|
params.push(companyCode);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (screen_id) {
|
||
|
|
query += ` AND sfj.screen_id = $${paramIndex}`;
|
||
|
|
params.push(screen_id);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
|
||
|
|
query += " ORDER BY sfj.id ASC";
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
res.json({ success: true, data: result.rows });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("필드 조인 목록 조회 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "필드 조인 목록 조회에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 화면 필드 조인 생성
|
||
|
|
export const createFieldJoin = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
const userId = (req.user as any).userId;
|
||
|
|
const {
|
||
|
|
screen_id, layout_id, component_id, field_name,
|
||
|
|
save_table, save_column, join_table, join_column, display_column,
|
||
|
|
join_type, filter_condition, sort_column, sort_direction, is_active
|
||
|
|
} = req.body;
|
||
|
|
|
||
|
|
if (!screen_id || !save_table || !save_column || !join_table || !join_column || !display_column) {
|
||
|
|
return res.status(400).json({ success: false, message: "필수 필드가 누락되었습니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
const query = `
|
||
|
|
INSERT INTO screen_field_joins (
|
||
|
|
screen_id, layout_id, component_id, field_name,
|
||
|
|
save_table, save_column, join_table, join_column, display_column,
|
||
|
|
join_type, filter_condition, sort_column, sort_direction, is_active, company_code, writer
|
||
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||
|
|
RETURNING *
|
||
|
|
`;
|
||
|
|
const params = [
|
||
|
|
screen_id, layout_id || null, component_id || null, field_name || null,
|
||
|
|
save_table, save_column, join_table, join_column, display_column,
|
||
|
|
join_type || 'LEFT', filter_condition || null, sort_column || null, sort_direction || 'ASC',
|
||
|
|
is_active || 'Y', companyCode === "*" ? "*" : companyCode, userId
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
logger.info("필드 조인 생성", { companyCode, screenId: screen_id, id: result.rows[0].id });
|
||
|
|
|
||
|
|
res.json({ success: true, data: result.rows[0], message: "필드 조인이 생성되었습니다." });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("필드 조인 생성 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "필드 조인 생성에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 화면 필드 조인 수정
|
||
|
|
export const updateFieldJoin = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
const {
|
||
|
|
layout_id, component_id, field_name,
|
||
|
|
save_table, save_column, join_table, join_column, display_column,
|
||
|
|
join_type, filter_condition, sort_column, sort_direction, is_active
|
||
|
|
} = req.body;
|
||
|
|
|
||
|
|
let query = `
|
||
|
|
UPDATE screen_field_joins SET
|
||
|
|
layout_id = $1, component_id = $2, field_name = $3,
|
||
|
|
save_table = $4, save_column = $5, join_table = $6, join_column = $7, display_column = $8,
|
||
|
|
join_type = $9, filter_condition = $10, sort_column = $11, sort_direction = $12,
|
||
|
|
is_active = $13, updated_date = NOW()
|
||
|
|
WHERE id = $14
|
||
|
|
`;
|
||
|
|
const params: any[] = [
|
||
|
|
layout_id, component_id, field_name,
|
||
|
|
save_table, save_column, join_table, join_column, display_column,
|
||
|
|
join_type, filter_condition, sort_column, sort_direction, is_active, id
|
||
|
|
];
|
||
|
|
|
||
|
|
if (companyCode !== "*") {
|
||
|
|
query += ` AND company_code = $15`;
|
||
|
|
params.push(companyCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
query += " RETURNING *";
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
if (result.rows.length === 0) {
|
||
|
|
return res.status(404).json({ success: false, message: "필드 조인을 찾을 수 없거나 권한이 없습니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({ success: true, data: result.rows[0], message: "필드 조인이 수정되었습니다." });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("필드 조인 수정 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "필드 조인 수정에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 화면 필드 조인 삭제
|
||
|
|
export const deleteFieldJoin = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
|
||
|
|
let query = `DELETE FROM screen_field_joins WHERE id = $1`;
|
||
|
|
const params: any[] = [id];
|
||
|
|
|
||
|
|
if (companyCode !== "*") {
|
||
|
|
query += ` AND company_code = $2`;
|
||
|
|
params.push(companyCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
query += " RETURNING id";
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
if (result.rows.length === 0) {
|
||
|
|
return res.status(404).json({ success: false, message: "필드 조인을 찾을 수 없거나 권한이 없습니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({ success: true, message: "필드 조인이 삭제되었습니다." });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("필드 조인 삭제 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "필드 조인 삭제에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// 데이터 흐름 (screen_data_flows) CRUD
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
// 데이터 흐름 목록 조회
|
||
|
|
export const getDataFlows = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
const { group_id } = req.query;
|
||
|
|
|
||
|
|
let query = `
|
||
|
|
SELECT sdf.*,
|
||
|
|
sd1.screen_name as source_screen_name,
|
||
|
|
sd2.screen_name as target_screen_name,
|
||
|
|
sg.group_name
|
||
|
|
FROM screen_data_flows sdf
|
||
|
|
LEFT JOIN screen_definitions sd1 ON sdf.source_screen_id = sd1.screen_id
|
||
|
|
LEFT JOIN screen_definitions sd2 ON sdf.target_screen_id = sd2.screen_id
|
||
|
|
LEFT JOIN screen_groups sg ON sdf.group_id = sg.id
|
||
|
|
WHERE 1=1
|
||
|
|
`;
|
||
|
|
const params: any[] = [];
|
||
|
|
let paramIndex = 1;
|
||
|
|
|
||
|
|
if (companyCode !== "*") {
|
||
|
|
query += ` AND sdf.company_code = $${paramIndex}`;
|
||
|
|
params.push(companyCode);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (group_id) {
|
||
|
|
query += ` AND sdf.group_id = $${paramIndex}`;
|
||
|
|
params.push(group_id);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
|
||
|
|
query += " ORDER BY sdf.id ASC";
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
res.json({ success: true, data: result.rows });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("데이터 흐름 목록 조회 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "데이터 흐름 목록 조회에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 데이터 흐름 생성
|
||
|
|
export const createDataFlow = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
const userId = (req.user as any).userId;
|
||
|
|
const {
|
||
|
|
group_id, source_screen_id, source_action, target_screen_id, target_action,
|
||
|
|
data_mapping, flow_type, flow_label, condition_expression, is_active
|
||
|
|
} = req.body;
|
||
|
|
|
||
|
|
if (!source_screen_id || !target_screen_id) {
|
||
|
|
return res.status(400).json({ success: false, message: "소스 화면과 타겟 화면은 필수입니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
const query = `
|
||
|
|
INSERT INTO screen_data_flows (
|
||
|
|
group_id, source_screen_id, source_action, target_screen_id, target_action,
|
||
|
|
data_mapping, flow_type, flow_label, condition_expression, is_active, company_code, writer
|
||
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||
|
|
RETURNING *
|
||
|
|
`;
|
||
|
|
const params = [
|
||
|
|
group_id || null, source_screen_id, source_action || null, target_screen_id, target_action || null,
|
||
|
|
data_mapping ? JSON.stringify(data_mapping) : null, flow_type || 'unidirectional',
|
||
|
|
flow_label || null, condition_expression || null, is_active || 'Y',
|
||
|
|
companyCode === "*" ? "*" : companyCode, userId
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
logger.info("데이터 흐름 생성", { companyCode, id: result.rows[0].id });
|
||
|
|
|
||
|
|
res.json({ success: true, data: result.rows[0], message: "데이터 흐름이 생성되었습니다." });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("데이터 흐름 생성 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "데이터 흐름 생성에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 데이터 흐름 수정
|
||
|
|
export const updateDataFlow = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
const {
|
||
|
|
group_id, source_screen_id, source_action, target_screen_id, target_action,
|
||
|
|
data_mapping, flow_type, flow_label, condition_expression, is_active
|
||
|
|
} = req.body;
|
||
|
|
|
||
|
|
let query = `
|
||
|
|
UPDATE screen_data_flows SET
|
||
|
|
group_id = $1, source_screen_id = $2, source_action = $3,
|
||
|
|
target_screen_id = $4, target_action = $5, data_mapping = $6,
|
||
|
|
flow_type = $7, flow_label = $8, condition_expression = $9,
|
||
|
|
is_active = $10, updated_date = NOW()
|
||
|
|
WHERE id = $11
|
||
|
|
`;
|
||
|
|
const params: any[] = [
|
||
|
|
group_id, source_screen_id, source_action, target_screen_id, target_action,
|
||
|
|
data_mapping ? JSON.stringify(data_mapping) : null, flow_type, flow_label, condition_expression, is_active, id
|
||
|
|
];
|
||
|
|
|
||
|
|
if (companyCode !== "*") {
|
||
|
|
query += ` AND company_code = $12`;
|
||
|
|
params.push(companyCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
query += " RETURNING *";
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
if (result.rows.length === 0) {
|
||
|
|
return res.status(404).json({ success: false, message: "데이터 흐름을 찾을 수 없거나 권한이 없습니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({ success: true, data: result.rows[0], message: "데이터 흐름이 수정되었습니다." });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("데이터 흐름 수정 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "데이터 흐름 수정에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 데이터 흐름 삭제
|
||
|
|
export const deleteDataFlow = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
|
||
|
|
let query = `DELETE FROM screen_data_flows WHERE id = $1`;
|
||
|
|
const params: any[] = [id];
|
||
|
|
|
||
|
|
if (companyCode !== "*") {
|
||
|
|
query += ` AND company_code = $2`;
|
||
|
|
params.push(companyCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
query += " RETURNING id";
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
if (result.rows.length === 0) {
|
||
|
|
return res.status(404).json({ success: false, message: "데이터 흐름을 찾을 수 없거나 권한이 없습니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({ success: true, message: "데이터 흐름이 삭제되었습니다." });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("데이터 흐름 삭제 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "데이터 흐름 삭제에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// 화면-테이블 관계 (screen_table_relations) CRUD
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
// 화면-테이블 관계 목록 조회
|
||
|
|
export const getTableRelations = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
const { screen_id, group_id } = req.query;
|
||
|
|
|
||
|
|
let query = `
|
||
|
|
SELECT str.*,
|
||
|
|
sd.screen_name,
|
||
|
|
sg.group_name,
|
||
|
|
tl.table_label
|
||
|
|
FROM screen_table_relations str
|
||
|
|
LEFT JOIN screen_definitions sd ON str.screen_id = sd.screen_id
|
||
|
|
LEFT JOIN screen_groups sg ON str.group_id = sg.id
|
||
|
|
LEFT JOIN table_labels tl ON str.table_name = tl.table_name
|
||
|
|
WHERE 1=1
|
||
|
|
`;
|
||
|
|
const params: any[] = [];
|
||
|
|
let paramIndex = 1;
|
||
|
|
|
||
|
|
if (companyCode !== "*") {
|
||
|
|
query += ` AND str.company_code = $${paramIndex}`;
|
||
|
|
params.push(companyCode);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (screen_id) {
|
||
|
|
query += ` AND str.screen_id = $${paramIndex}`;
|
||
|
|
params.push(screen_id);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (group_id) {
|
||
|
|
query += ` AND str.group_id = $${paramIndex}`;
|
||
|
|
params.push(group_id);
|
||
|
|
paramIndex++;
|
||
|
|
}
|
||
|
|
|
||
|
|
query += " ORDER BY str.id ASC";
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
res.json({ success: true, data: result.rows });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("화면-테이블 관계 목록 조회 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "화면-테이블 관계 목록 조회에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 화면-테이블 관계 생성
|
||
|
|
export const createTableRelation = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
const userId = (req.user as any).userId;
|
||
|
|
const { group_id, screen_id, table_name, relation_type, crud_operations, description, is_active } = req.body;
|
||
|
|
|
||
|
|
if (!screen_id || !table_name) {
|
||
|
|
return res.status(400).json({ success: false, message: "화면 ID와 테이블명은 필수입니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
const query = `
|
||
|
|
INSERT INTO screen_table_relations (group_id, screen_id, table_name, relation_type, crud_operations, description, is_active, company_code, writer)
|
||
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||
|
|
RETURNING *
|
||
|
|
`;
|
||
|
|
const params = [
|
||
|
|
group_id || null, screen_id, table_name, relation_type || 'main',
|
||
|
|
crud_operations || 'CRUD', description || null, is_active || 'Y',
|
||
|
|
companyCode === "*" ? "*" : companyCode, userId
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
logger.info("화면-테이블 관계 생성", { companyCode, screenId: screen_id, tableName: table_name });
|
||
|
|
|
||
|
|
res.json({ success: true, data: result.rows[0], message: "화면-테이블 관계가 생성되었습니다." });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("화면-테이블 관계 생성 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "화면-테이블 관계 생성에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 화면-테이블 관계 수정
|
||
|
|
export const updateTableRelation = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
const { group_id, table_name, relation_type, crud_operations, description, is_active } = req.body;
|
||
|
|
|
||
|
|
let query = `
|
||
|
|
UPDATE screen_table_relations SET
|
||
|
|
group_id = $1, table_name = $2, relation_type = $3, crud_operations = $4,
|
||
|
|
description = $5, is_active = $6, updated_date = NOW()
|
||
|
|
WHERE id = $7
|
||
|
|
`;
|
||
|
|
const params: any[] = [group_id, table_name, relation_type, crud_operations, description, is_active, id];
|
||
|
|
|
||
|
|
if (companyCode !== "*") {
|
||
|
|
query += ` AND company_code = $8`;
|
||
|
|
params.push(companyCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
query += " RETURNING *";
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
if (result.rows.length === 0) {
|
||
|
|
return res.status(404).json({ success: false, message: "화면-테이블 관계를 찾을 수 없거나 권한이 없습니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({ success: true, data: result.rows[0], message: "화면-테이블 관계가 수정되었습니다." });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("화면-테이블 관계 수정 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "화면-테이블 관계 수정에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 화면-테이블 관계 삭제
|
||
|
|
export const deleteTableRelation = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const { id } = req.params;
|
||
|
|
const companyCode = (req.user as any).companyCode;
|
||
|
|
|
||
|
|
let query = `DELETE FROM screen_table_relations WHERE id = $1`;
|
||
|
|
const params: any[] = [id];
|
||
|
|
|
||
|
|
if (companyCode !== "*") {
|
||
|
|
query += ` AND company_code = $2`;
|
||
|
|
params.push(companyCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
query += " RETURNING id";
|
||
|
|
|
||
|
|
const result = await pool.query(query, params);
|
||
|
|
|
||
|
|
if (result.rows.length === 0) {
|
||
|
|
return res.status(404).json({ success: false, message: "화면-테이블 관계를 찾을 수 없거나 권한이 없습니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({ success: true, message: "화면-테이블 관계가 삭제되었습니다." });
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("화면-테이블 관계 삭제 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "화면-테이블 관계 삭제에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// 화면 레이아웃 요약 정보 (미리보기용)
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
// 화면 레이아웃 요약 조회 (위젯 타입별 개수, 라벨 목록)
|
||
|
|
export const getScreenLayoutSummary = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const { screenId } = req.params;
|
||
|
|
|
||
|
|
// 화면의 컴포넌트 정보 조회
|
||
|
|
const query = `
|
||
|
|
SELECT
|
||
|
|
properties->>'widgetType' as widget_type,
|
||
|
|
properties->>'label' as label,
|
||
|
|
properties->>'fieldName' as field_name,
|
||
|
|
properties->>'tableName' as table_name
|
||
|
|
FROM screen_layouts
|
||
|
|
WHERE screen_id = $1
|
||
|
|
AND component_type = 'component'
|
||
|
|
ORDER BY display_order ASC
|
||
|
|
`;
|
||
|
|
|
||
|
|
const result = await pool.query(query, [screenId]);
|
||
|
|
|
||
|
|
// 위젯 타입별 집계
|
||
|
|
const widgetCounts: Record<string, number> = {};
|
||
|
|
const labels: string[] = [];
|
||
|
|
const fields: Array<{ label: string; widgetType: string; fieldName?: string }> = [];
|
||
|
|
|
||
|
|
result.rows.forEach((row: any) => {
|
||
|
|
const widgetType = row.widget_type || 'text';
|
||
|
|
widgetCounts[widgetType] = (widgetCounts[widgetType] || 0) + 1;
|
||
|
|
|
||
|
|
if (row.label && row.label !== '기본 버튼') {
|
||
|
|
labels.push(row.label);
|
||
|
|
fields.push({
|
||
|
|
label: row.label,
|
||
|
|
widgetType: widgetType,
|
||
|
|
fieldName: row.field_name,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 화면 타입 추론 (가장 많은 컴포넌트 기준)
|
||
|
|
let screenType = 'form'; // 기본값
|
||
|
|
if (widgetCounts['table'] > 0) {
|
||
|
|
screenType = 'grid';
|
||
|
|
} else if (widgetCounts['custom'] > 2) {
|
||
|
|
screenType = 'dashboard';
|
||
|
|
} else if (Object.keys(widgetCounts).length <= 2 && widgetCounts['button'] > 0) {
|
||
|
|
screenType = 'action';
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info("화면 레이아웃 요약 조회", { screenId, widgetCounts, fieldCount: fields.length });
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: {
|
||
|
|
screenId: parseInt(screenId),
|
||
|
|
screenType,
|
||
|
|
widgetCounts,
|
||
|
|
totalComponents: result.rows.length,
|
||
|
|
fields: fields.slice(0, 10), // 최대 10개
|
||
|
|
labels: labels.slice(0, 8), // 최대 8개
|
||
|
|
},
|
||
|
|
});
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("화면 레이아웃 요약 조회 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "화면 레이아웃 요약 조회에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 여러 화면의 레이아웃 요약 일괄 조회 (미니어처 렌더링용 좌표 포함)
|
||
|
|
export const getMultipleScreenLayoutSummary = async (req: Request, res: Response) => {
|
||
|
|
try {
|
||
|
|
const { screenIds } = req.body;
|
||
|
|
|
||
|
|
if (!screenIds || !Array.isArray(screenIds) || screenIds.length === 0) {
|
||
|
|
return res.status(400).json({ success: false, message: "screenIds 배열이 필요합니다." });
|
||
|
|
}
|
||
|
|
|
||
|
|
// 여러 화면의 컴포넌트 정보 (좌표 포함) 한번에 조회
|
||
|
|
// componentType이 더 정확한 위젯 종류 (table-list, button-primary 등)
|
||
|
|
const query = `
|
||
|
|
SELECT
|
||
|
|
screen_id,
|
||
|
|
component_type,
|
||
|
|
position_x,
|
||
|
|
position_y,
|
||
|
|
width,
|
||
|
|
height,
|
||
|
|
properties->>'componentType' as component_kind,
|
||
|
|
properties->>'widgetType' as widget_type,
|
||
|
|
properties->>'label' as label
|
||
|
|
FROM screen_layouts
|
||
|
|
WHERE screen_id = ANY($1)
|
||
|
|
AND component_type = 'component'
|
||
|
|
ORDER BY screen_id, display_order ASC
|
||
|
|
`;
|
||
|
|
|
||
|
|
const result = await pool.query(query, [screenIds]);
|
||
|
|
|
||
|
|
// 화면별로 그룹핑
|
||
|
|
const summaryMap: Record<number, any> = {};
|
||
|
|
|
||
|
|
screenIds.forEach((id: number) => {
|
||
|
|
summaryMap[id] = {
|
||
|
|
screenId: id,
|
||
|
|
screenType: 'form',
|
||
|
|
widgetCounts: {},
|
||
|
|
totalComponents: 0,
|
||
|
|
// 미니어처 렌더링용 레이아웃 데이터
|
||
|
|
layoutItems: [],
|
||
|
|
canvasWidth: 0,
|
||
|
|
canvasHeight: 0,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
result.rows.forEach((row: any) => {
|
||
|
|
const screenId = row.screen_id;
|
||
|
|
// componentKind가 더 정확한 타입 (table-list, button-primary, table-search-widget 등)
|
||
|
|
const componentKind = row.component_kind || row.widget_type || 'text';
|
||
|
|
const widgetType = row.widget_type || 'text';
|
||
|
|
|
||
|
|
if (summaryMap[screenId]) {
|
||
|
|
summaryMap[screenId].widgetCounts[componentKind] =
|
||
|
|
(summaryMap[screenId].widgetCounts[componentKind] || 0) + 1;
|
||
|
|
summaryMap[screenId].totalComponents++;
|
||
|
|
|
||
|
|
// 레이아웃 아이템 추가 (미니어처 렌더링용)
|
||
|
|
summaryMap[screenId].layoutItems.push({
|
||
|
|
x: row.position_x || 0,
|
||
|
|
y: row.position_y || 0,
|
||
|
|
width: row.width || 100,
|
||
|
|
height: row.height || 30,
|
||
|
|
componentKind: componentKind, // 정확한 컴포넌트 종류
|
||
|
|
widgetType: widgetType,
|
||
|
|
label: row.label,
|
||
|
|
});
|
||
|
|
|
||
|
|
// 캔버스 크기 계산 (최대 좌표 기준)
|
||
|
|
const rightEdge = (row.position_x || 0) + (row.width || 100);
|
||
|
|
const bottomEdge = (row.position_y || 0) + (row.height || 30);
|
||
|
|
if (rightEdge > summaryMap[screenId].canvasWidth) {
|
||
|
|
summaryMap[screenId].canvasWidth = rightEdge;
|
||
|
|
}
|
||
|
|
if (bottomEdge > summaryMap[screenId].canvasHeight) {
|
||
|
|
summaryMap[screenId].canvasHeight = bottomEdge;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 화면 타입 추론 (componentKind 기준)
|
||
|
|
Object.values(summaryMap).forEach((summary: any) => {
|
||
|
|
if (summary.widgetCounts['table-list'] > 0) {
|
||
|
|
summary.screenType = 'grid';
|
||
|
|
} else if (summary.widgetCounts['table-search-widget'] > 1) {
|
||
|
|
summary.screenType = 'dashboard';
|
||
|
|
} else if (summary.totalComponents <= 5 && summary.widgetCounts['button-primary'] > 0) {
|
||
|
|
summary.screenType = 'action';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
logger.info("여러 화면 레이아웃 요약 조회", { screenIds, count: Object.keys(summaryMap).length });
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: summaryMap,
|
||
|
|
});
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("여러 화면 레이아웃 요약 조회 실패:", error);
|
||
|
|
res.status(500).json({ success: false, message: "여러 화면 레이아웃 요약 조회에 실패했습니다.", error: error.message });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|