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); // 데이터 조회 (screens 배열 포함) 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.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, '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 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 = {}; 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 = {}; 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 }); } }; // ============================================================ // 화면 서브 테이블 관계 조회 (조인/참조 테이블) // ============================================================ // 여러 화면의 서브 테이블 정보 조회 (메인 테이블 → 서브 테이블 관계) export const getScreenSubTables = 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 배열이 필요합니다." }); } // 화면별 메인 테이블과 서브 테이블 관계 조회 // componentConfig에서 tableName, sourceTable 추출 const query = ` SELECT DISTINCT sd.screen_id, sd.screen_name, sd.table_name as main_table, COALESCE( sl.properties->'componentConfig'->>'tableName', sl.properties->'componentConfig'->>'sourceTable' ) as sub_table, sl.properties->>'componentType' as component_type, sl.properties->'componentConfig'->>'targetTable' as target_table FROM screen_definitions sd JOIN screen_layouts sl ON sd.screen_id = sl.screen_id WHERE sd.screen_id = ANY($1) AND ( sl.properties->'componentConfig'->>'tableName' IS NOT NULL OR sl.properties->'componentConfig'->>'sourceTable' IS NOT NULL ) ORDER BY sd.screen_id `; const result = await pool.query(query, [screenIds]); // 화면별 서브 테이블 그룹화 const screenSubTables: Record; }> = {}; result.rows.forEach((row: any) => { const screenId = row.screen_id; const mainTable = row.main_table; const subTable = row.sub_table; // 메인 테이블과 동일한 경우 제외 if (!subTable || subTable === mainTable) { return; } if (!screenSubTables[screenId]) { screenSubTables[screenId] = { screenId, screenName: row.screen_name, mainTable: mainTable || '', subTables: [], }; } // 중복 체크 const exists = screenSubTables[screenId].subTables.some( (st) => st.tableName === subTable ); if (!exists) { // 관계 타입 추론 let relationType = 'lookup'; const componentType = row.component_type || ''; if (componentType.includes('autocomplete') || componentType.includes('entity-search')) { relationType = 'lookup'; } else if (componentType.includes('modal-repeater') || componentType.includes('selected-items')) { relationType = 'source'; } else if (componentType.includes('table')) { relationType = 'join'; } screenSubTables[screenId].subTables.push({ tableName: subTable, componentType: componentType, relationType: relationType, }); } }); logger.info("화면 서브 테이블 정보 조회", { screenIds, resultCount: Object.keys(screenSubTables).length }); res.json({ success: true, data: screenSubTables, }); } catch (error: any) { logger.error("화면 서브 테이블 정보 조회 실패:", error); res.status(500).json({ success: false, message: "화면 서브 테이블 정보 조회에 실패했습니다.", error: error.message }); } };