/** * 화면 임베딩 및 데이터 전달 시스템 컨트롤러 */ import { Request, Response } from "express"; import { getPool } from "../database/db"; import { logger } from "../utils/logger"; import { AuthenticatedRequest } from "../types/auth"; const pool = getPool(); // ============================================ // 1. 화면 임베딩 API // ============================================ /** * 화면 임베딩 목록 조회 * GET /api/screen-embedding?parentScreenId=1 */ export async function getScreenEmbeddings(req: AuthenticatedRequest, res: Response) { try { const { parentScreenId } = req.query; const companyCode = req.user!.companyCode; if (!parentScreenId) { return res.status(400).json({ success: false, message: "부모 화면 ID가 필요합니다.", }); } const query = ` SELECT se.*, ps.screen_name as parent_screen_name, cs.screen_name as child_screen_name FROM screen_embedding se LEFT JOIN screen_definitions ps ON se.parent_screen_id = ps.screen_id LEFT JOIN screen_definitions cs ON se.child_screen_id = cs.screen_id WHERE se.parent_screen_id = $1 AND se.company_code = $2 ORDER BY se.position, se.created_at `; const result = await pool.query(query, [parentScreenId, companyCode]); logger.info("화면 임베딩 목록 조회", { companyCode, parentScreenId, count: result.rowCount, }); return res.json({ success: true, data: result.rows, }); } catch (error: any) { logger.error("화면 임베딩 목록 조회 실패", error); return res.status(500).json({ success: false, message: "화면 임베딩 목록 조회 중 오류가 발생했습니다.", error: error.message, }); } } /** * 화면 임베딩 상세 조회 * GET /api/screen-embedding/:id */ export async function getScreenEmbeddingById(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; const companyCode = req.user!.companyCode; const query = ` SELECT se.*, ps.screen_name as parent_screen_name, cs.screen_name as child_screen_name FROM screen_embedding se LEFT JOIN screen_definitions ps ON se.parent_screen_id = ps.screen_id LEFT JOIN screen_definitions cs ON se.child_screen_id = cs.screen_id WHERE se.id = $1 AND se.company_code = $2 `; const result = await pool.query(query, [id, companyCode]); if (result.rowCount === 0) { return res.status(404).json({ success: false, message: "화면 임베딩 설정을 찾을 수 없습니다.", }); } logger.info("화면 임베딩 상세 조회", { companyCode, id }); return res.json({ success: true, data: result.rows[0], }); } catch (error: any) { logger.error("화면 임베딩 상세 조회 실패", error); return res.status(500).json({ success: false, message: "화면 임베딩 상세 조회 중 오류가 발생했습니다.", error: error.message, }); } } /** * 화면 임베딩 생성 * POST /api/screen-embedding */ export async function createScreenEmbedding(req: AuthenticatedRequest, res: Response) { try { const { parentScreenId, childScreenId, position, mode, config = {}, } = req.body; const companyCode = req.user!.companyCode; const userId = req.user!.userId; // 필수 필드 검증 if (!parentScreenId || !childScreenId || !position || !mode) { return res.status(400).json({ success: false, message: "필수 필드가 누락되었습니다.", }); } const query = ` INSERT INTO screen_embedding ( parent_screen_id, child_screen_id, position, mode, config, company_code, created_by, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) RETURNING * `; const result = await pool.query(query, [ parentScreenId, childScreenId, position, mode, JSON.stringify(config), companyCode, userId, ]); logger.info("화면 임베딩 생성", { companyCode, userId, id: result.rows[0].id, }); return res.status(201).json({ success: true, data: result.rows[0], }); } catch (error: any) { logger.error("화면 임베딩 생성 실패", error); // 유니크 제약조건 위반 if (error.code === "23505") { return res.status(409).json({ success: false, message: "이미 동일한 임베딩 설정이 존재합니다.", }); } return res.status(500).json({ success: false, message: "화면 임베딩 생성 중 오류가 발생했습니다.", error: error.message, }); } } /** * 화면 임베딩 수정 * PUT /api/screen-embedding/:id */ export async function updateScreenEmbedding(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; const { position, mode, config } = req.body; const companyCode = req.user!.companyCode; const updates: string[] = []; const values: any[] = []; let paramIndex = 1; if (position) { updates.push(`position = $${paramIndex++}`); values.push(position); } if (mode) { updates.push(`mode = $${paramIndex++}`); values.push(mode); } if (config) { updates.push(`config = $${paramIndex++}`); values.push(JSON.stringify(config)); } if (updates.length === 0) { return res.status(400).json({ success: false, message: "수정할 내용이 없습니다.", }); } updates.push(`updated_at = NOW()`); values.push(id, companyCode); const query = ` UPDATE screen_embedding SET ${updates.join(", ")} WHERE id = $${paramIndex++} AND company_code = $${paramIndex++} RETURNING * `; const result = await pool.query(query, values); if (result.rowCount === 0) { return res.status(404).json({ success: false, message: "화면 임베딩 설정을 찾을 수 없습니다.", }); } logger.info("화면 임베딩 수정", { companyCode, id }); return res.json({ success: true, data: result.rows[0], }); } catch (error: any) { logger.error("화면 임베딩 수정 실패", error); return res.status(500).json({ success: false, message: "화면 임베딩 수정 중 오류가 발생했습니다.", error: error.message, }); } } /** * 화면 임베딩 삭제 * DELETE /api/screen-embedding/:id */ export async function deleteScreenEmbedding(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; const companyCode = req.user!.companyCode; const query = ` DELETE FROM screen_embedding WHERE id = $1 AND company_code = $2 RETURNING id `; const result = await pool.query(query, [id, companyCode]); if (result.rowCount === 0) { return res.status(404).json({ success: false, message: "화면 임베딩 설정을 찾을 수 없습니다.", }); } logger.info("화면 임베딩 삭제", { companyCode, id }); return res.json({ success: true, message: "화면 임베딩이 삭제되었습니다.", }); } catch (error: any) { logger.error("화면 임베딩 삭제 실패", error); return res.status(500).json({ success: false, message: "화면 임베딩 삭제 중 오류가 발생했습니다.", error: error.message, }); } } // ============================================ // 2. 데이터 전달 API // ============================================ /** * 데이터 전달 설정 조회 * GET /api/screen-data-transfer?sourceScreenId=1&targetScreenId=2 */ export async function getScreenDataTransfer(req: AuthenticatedRequest, res: Response) { try { const { sourceScreenId, targetScreenId } = req.query; const companyCode = req.user!.companyCode; if (!sourceScreenId || !targetScreenId) { return res.status(400).json({ success: false, message: "소스 화면 ID와 타겟 화면 ID가 필요합니다.", }); } const query = ` SELECT sdt.*, ss.screen_name as source_screen_name, ts.screen_name as target_screen_name FROM screen_data_transfer sdt LEFT JOIN screen_definitions ss ON sdt.source_screen_id = ss.screen_id LEFT JOIN screen_definitions ts ON sdt.target_screen_id = ts.screen_id WHERE sdt.source_screen_id = $1 AND sdt.target_screen_id = $2 AND sdt.company_code = $3 `; const result = await pool.query(query, [ sourceScreenId, targetScreenId, companyCode, ]); if (result.rowCount === 0) { return res.status(404).json({ success: false, message: "데이터 전달 설정을 찾을 수 없습니다.", }); } logger.info("데이터 전달 설정 조회", { companyCode, sourceScreenId, targetScreenId, }); return res.json({ success: true, data: result.rows[0], }); } catch (error: any) { logger.error("데이터 전달 설정 조회 실패", error); return res.status(500).json({ success: false, message: "데이터 전달 설정 조회 중 오류가 발생했습니다.", error: error.message, }); } } /** * 데이터 전달 설정 생성 * POST /api/screen-data-transfer */ export async function createScreenDataTransfer(req: AuthenticatedRequest, res: Response) { try { const { sourceScreenId, targetScreenId, sourceComponentId, sourceComponentType, dataReceivers, buttonConfig, } = req.body; const companyCode = req.user!.companyCode; const userId = req.user!.userId; // 필수 필드 검증 if (!sourceScreenId || !targetScreenId || !dataReceivers) { return res.status(400).json({ success: false, message: "필수 필드가 누락되었습니다.", }); } const query = ` INSERT INTO screen_data_transfer ( source_screen_id, target_screen_id, source_component_id, source_component_type, data_receivers, button_config, company_code, created_by, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW()) RETURNING * `; const result = await pool.query(query, [ sourceScreenId, targetScreenId, sourceComponentId, sourceComponentType, JSON.stringify(dataReceivers), JSON.stringify(buttonConfig || {}), companyCode, userId, ]); logger.info("데이터 전달 설정 생성", { companyCode, userId, id: result.rows[0].id, }); return res.status(201).json({ success: true, data: result.rows[0], }); } catch (error: any) { logger.error("데이터 전달 설정 생성 실패", error); // 유니크 제약조건 위반 if (error.code === "23505") { return res.status(409).json({ success: false, message: "이미 동일한 데이터 전달 설정이 존재합니다.", }); } return res.status(500).json({ success: false, message: "데이터 전달 설정 생성 중 오류가 발생했습니다.", error: error.message, }); } } /** * 데이터 전달 설정 수정 * PUT /api/screen-data-transfer/:id */ export async function updateScreenDataTransfer(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; const { dataReceivers, buttonConfig } = req.body; const companyCode = req.user!.companyCode; const updates: string[] = []; const values: any[] = []; let paramIndex = 1; if (dataReceivers) { updates.push(`data_receivers = $${paramIndex++}`); values.push(JSON.stringify(dataReceivers)); } if (buttonConfig) { updates.push(`button_config = $${paramIndex++}`); values.push(JSON.stringify(buttonConfig)); } if (updates.length === 0) { return res.status(400).json({ success: false, message: "수정할 내용이 없습니다.", }); } updates.push(`updated_at = NOW()`); values.push(id, companyCode); const query = ` UPDATE screen_data_transfer SET ${updates.join(", ")} WHERE id = $${paramIndex++} AND company_code = $${paramIndex++} RETURNING * `; const result = await pool.query(query, values); if (result.rowCount === 0) { return res.status(404).json({ success: false, message: "데이터 전달 설정을 찾을 수 없습니다.", }); } logger.info("데이터 전달 설정 수정", { companyCode, id }); return res.json({ success: true, data: result.rows[0], }); } catch (error: any) { logger.error("데이터 전달 설정 수정 실패", error); return res.status(500).json({ success: false, message: "데이터 전달 설정 수정 중 오류가 발생했습니다.", error: error.message, }); } } /** * 데이터 전달 설정 삭제 * DELETE /api/screen-data-transfer/:id */ export async function deleteScreenDataTransfer(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; const companyCode = req.user!.companyCode; const query = ` DELETE FROM screen_data_transfer WHERE id = $1 AND company_code = $2 RETURNING id `; const result = await pool.query(query, [id, companyCode]); if (result.rowCount === 0) { return res.status(404).json({ success: false, message: "데이터 전달 설정을 찾을 수 없습니다.", }); } logger.info("데이터 전달 설정 삭제", { companyCode, id }); return res.json({ success: true, message: "데이터 전달 설정이 삭제되었습니다.", }); } catch (error: any) { logger.error("데이터 전달 설정 삭제 실패", error); return res.status(500).json({ success: false, message: "데이터 전달 설정 삭제 중 오류가 발생했습니다.", error: error.message, }); } } // ============================================ // 3. 분할 패널 API // ============================================ /** * 분할 패널 설정 조회 * GET /api/screen-split-panel/:screenId */ export async function getScreenSplitPanel(req: AuthenticatedRequest, res: Response) { try { const { screenId } = req.params; const companyCode = req.user!.companyCode; const query = ` SELECT ssp.*, le.parent_screen_id as le_parent_screen_id, le.child_screen_id as le_child_screen_id, le.position as le_position, le.mode as le_mode, le.config as le_config, re.parent_screen_id as re_parent_screen_id, re.child_screen_id as re_child_screen_id, re.position as re_position, re.mode as re_mode, re.config as re_config, sdt.source_screen_id, sdt.target_screen_id, sdt.source_component_id, sdt.source_component_type, sdt.data_receivers, sdt.button_config FROM screen_split_panel ssp LEFT JOIN screen_embedding le ON ssp.left_embedding_id = le.id LEFT JOIN screen_embedding re ON ssp.right_embedding_id = re.id LEFT JOIN screen_data_transfer sdt ON ssp.data_transfer_id = sdt.id WHERE ssp.screen_id = $1 AND ssp.company_code = $2 `; const result = await pool.query(query, [screenId, companyCode]); if (result.rowCount === 0) { return res.status(404).json({ success: false, message: "분할 패널 설정을 찾을 수 없습니다.", }); } const row = result.rows[0]; // 데이터 구조화 const data = { id: row.id, screenId: row.screen_id, leftEmbeddingId: row.left_embedding_id, rightEmbeddingId: row.right_embedding_id, dataTransferId: row.data_transfer_id, layoutConfig: row.layout_config, companyCode: row.company_code, createdAt: row.created_at, updatedAt: row.updated_at, leftEmbedding: row.le_child_screen_id ? { id: row.left_embedding_id, parentScreenId: row.le_parent_screen_id, childScreenId: row.le_child_screen_id, position: row.le_position, mode: row.le_mode, config: row.le_config, } : null, rightEmbedding: row.re_child_screen_id ? { id: row.right_embedding_id, parentScreenId: row.re_parent_screen_id, childScreenId: row.re_child_screen_id, position: row.re_position, mode: row.re_mode, config: row.re_config, } : null, dataTransfer: row.source_screen_id ? { id: row.data_transfer_id, sourceScreenId: row.source_screen_id, targetScreenId: row.target_screen_id, sourceComponentId: row.source_component_id, sourceComponentType: row.source_component_type, dataReceivers: row.data_receivers, buttonConfig: row.button_config, } : null, }; logger.info("분할 패널 설정 조회", { companyCode, screenId }); return res.json({ success: true, data, }); } catch (error: any) { logger.error("분할 패널 설정 조회 실패", error); return res.status(500).json({ success: false, message: "분할 패널 설정 조회 중 오류가 발생했습니다.", error: error.message, }); } } /** * 분할 패널 설정 생성 * POST /api/screen-split-panel */ export async function createScreenSplitPanel(req: AuthenticatedRequest, res: Response) { const client = await pool.connect(); try { const { screenId, leftEmbedding, rightEmbedding, dataTransfer, layoutConfig, } = req.body; const companyCode = req.user!.companyCode; const userId = req.user!.userId; // 필수 필드 검증 if (!screenId || !leftEmbedding || !rightEmbedding || !dataTransfer) { return res.status(400).json({ success: false, message: "필수 필드가 누락되었습니다.", }); } await client.query("BEGIN"); // 1. 좌측 임베딩 생성 const leftEmbeddingQuery = ` INSERT INTO screen_embedding ( parent_screen_id, child_screen_id, position, mode, config, company_code, created_by, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) RETURNING id `; const leftResult = await client.query(leftEmbeddingQuery, [ screenId, leftEmbedding.childScreenId, leftEmbedding.position, leftEmbedding.mode, JSON.stringify(leftEmbedding.config || {}), companyCode, userId, ]); const leftEmbeddingId = leftResult.rows[0].id; // 2. 우측 임베딩 생성 const rightEmbeddingQuery = ` INSERT INTO screen_embedding ( parent_screen_id, child_screen_id, position, mode, config, company_code, created_by, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) RETURNING id `; const rightResult = await client.query(rightEmbeddingQuery, [ screenId, rightEmbedding.childScreenId, rightEmbedding.position, rightEmbedding.mode, JSON.stringify(rightEmbedding.config || {}), companyCode, userId, ]); const rightEmbeddingId = rightResult.rows[0].id; // 3. 데이터 전달 설정 생성 const dataTransferQuery = ` INSERT INTO screen_data_transfer ( source_screen_id, target_screen_id, source_component_id, source_component_type, data_receivers, button_config, company_code, created_by, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW()) RETURNING id `; const dataTransferResult = await client.query(dataTransferQuery, [ dataTransfer.sourceScreenId, dataTransfer.targetScreenId, dataTransfer.sourceComponentId, dataTransfer.sourceComponentType, JSON.stringify(dataTransfer.dataReceivers), JSON.stringify(dataTransfer.buttonConfig || {}), companyCode, userId, ]); const dataTransferId = dataTransferResult.rows[0].id; // 4. 분할 패널 생성 const splitPanelQuery = ` INSERT INTO screen_split_panel ( screen_id, left_embedding_id, right_embedding_id, data_transfer_id, layout_config, company_code, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW()) RETURNING * `; const splitPanelResult = await client.query(splitPanelQuery, [ screenId, leftEmbeddingId, rightEmbeddingId, dataTransferId, JSON.stringify(layoutConfig || {}), companyCode, ]); await client.query("COMMIT"); logger.info("분할 패널 설정 생성", { companyCode, userId, screenId, id: splitPanelResult.rows[0].id, }); return res.status(201).json({ success: true, data: splitPanelResult.rows[0], }); } catch (error: any) { await client.query("ROLLBACK"); logger.error("분할 패널 설정 생성 실패", error); return res.status(500).json({ success: false, message: "분할 패널 설정 생성 중 오류가 발생했습니다.", error: error.message, }); } finally { client.release(); } } /** * 분할 패널 설정 수정 * PUT /api/screen-split-panel/:id */ export async function updateScreenSplitPanel(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; const { layoutConfig } = req.body; const companyCode = req.user!.companyCode; if (!layoutConfig) { return res.status(400).json({ success: false, message: "수정할 내용이 없습니다.", }); } const query = ` UPDATE screen_split_panel SET layout_config = $1, updated_at = NOW() WHERE id = $2 AND company_code = $3 RETURNING * `; const result = await pool.query(query, [ JSON.stringify(layoutConfig), id, companyCode, ]); if (result.rowCount === 0) { return res.status(404).json({ success: false, message: "분할 패널 설정을 찾을 수 없습니다.", }); } logger.info("분할 패널 설정 수정", { companyCode, id }); return res.json({ success: true, data: result.rows[0], }); } catch (error: any) { logger.error("분할 패널 설정 수정 실패", error); return res.status(500).json({ success: false, message: "분할 패널 설정 수정 중 오류가 발생했습니다.", error: error.message, }); } } /** * 분할 패널 설정 삭제 * DELETE /api/screen-split-panel/:id */ export async function deleteScreenSplitPanel(req: AuthenticatedRequest, res: Response) { const client = await pool.connect(); try { const { id } = req.params; const companyCode = req.user!.companyCode; await client.query("BEGIN"); // 1. 분할 패널 조회 const selectQuery = ` SELECT left_embedding_id, right_embedding_id, data_transfer_id FROM screen_split_panel WHERE id = $1 AND company_code = $2 `; const selectResult = await client.query(selectQuery, [id, companyCode]); if (selectResult.rowCount === 0) { await client.query("ROLLBACK"); return res.status(404).json({ success: false, message: "분할 패널 설정을 찾을 수 없습니다.", }); } const { left_embedding_id, right_embedding_id, data_transfer_id } = selectResult.rows[0]; // 2. 분할 패널 삭제 await client.query( "DELETE FROM screen_split_panel WHERE id = $1 AND company_code = $2", [id, companyCode] ); // 3. 관련 임베딩 및 데이터 전달 설정 삭제 (CASCADE로 자동 삭제되지만 명시적으로) if (left_embedding_id) { await client.query( "DELETE FROM screen_embedding WHERE id = $1 AND company_code = $2", [left_embedding_id, companyCode] ); } if (right_embedding_id) { await client.query( "DELETE FROM screen_embedding WHERE id = $1 AND company_code = $2", [right_embedding_id, companyCode] ); } if (data_transfer_id) { await client.query( "DELETE FROM screen_data_transfer WHERE id = $1 AND company_code = $2", [data_transfer_id, companyCode] ); } await client.query("COMMIT"); logger.info("분할 패널 설정 삭제", { companyCode, id }); return res.json({ success: true, message: "분할 패널 설정이 삭제되었습니다.", }); } catch (error: any) { await client.query("ROLLBACK"); logger.error("분할 패널 설정 삭제 실패", error); return res.status(500).json({ success: false, message: "분할 패널 설정 삭제 중 오류가 발생했습니다.", error: error.message, }); } finally { client.release(); } }