import { Request, Response } from "express"; import { getPool } from "../database/db"; import { logger } from "../utils/logger"; const pool = getPool(); /** * 연쇄 관계 목록 조회 */ export const getCascadingRelations = async (req: Request, res: Response) => { try { const companyCode = req.user?.companyCode || "*"; const { isActive } = req.query; let query = ` SELECT relation_id, relation_code, relation_name, description, parent_table, parent_value_column, parent_label_column, child_table, child_filter_column, child_value_column, child_label_column, child_order_column, child_order_direction, empty_parent_message, no_options_message, loading_message, clear_on_parent_change, company_code, is_active, created_by, created_date, updated_by, updated_date FROM cascading_relation WHERE 1=1 `; const params: any[] = []; let paramIndex = 1; // 멀티테넌시 필터링 if (companyCode !== "*") { query += ` AND (company_code = $${paramIndex} OR company_code = '*')`; params.push(companyCode); paramIndex++; } // 활성 상태 필터링 if (isActive !== undefined) { query += ` AND is_active = $${paramIndex}`; params.push(isActive); paramIndex++; } query += ` ORDER BY relation_name ASC`; const result = await pool.query(query, params); logger.info("연쇄 관계 목록 조회", { companyCode, count: result.rowCount, }); return res.json({ success: true, data: result.rows, }); } catch (error: any) { logger.error("연쇄 관계 목록 조회 실패", { error: error.message }); return res.status(500).json({ success: false, message: "연쇄 관계 목록 조회에 실패했습니다.", error: error.message, }); } }; /** * 연쇄 관계 상세 조회 */ export const getCascadingRelationById = async (req: Request, res: Response) => { try { const { id } = req.params; const companyCode = req.user?.companyCode || "*"; let query = ` SELECT relation_id, relation_code, relation_name, description, parent_table, parent_value_column, parent_label_column, child_table, child_filter_column, child_value_column, child_label_column, child_order_column, child_order_direction, empty_parent_message, no_options_message, loading_message, clear_on_parent_change, company_code, is_active, created_by, created_date, updated_by, updated_date FROM cascading_relation WHERE relation_id = $1 `; const params: any[] = [id]; // 멀티테넌시 필터링 if (companyCode !== "*") { query += ` AND (company_code = $2 OR company_code = '*')`; params.push(companyCode); } const result = await pool.query(query, params); if (result.rowCount === 0) { return res.status(404).json({ success: false, message: "연쇄 관계를 찾을 수 없습니다.", }); } return res.json({ success: true, data: result.rows[0], }); } catch (error: any) { logger.error("연쇄 관계 상세 조회 실패", { error: error.message }); return res.status(500).json({ success: false, message: "연쇄 관계 조회에 실패했습니다.", error: error.message, }); } }; /** * 연쇄 관계 코드로 조회 */ export const getCascadingRelationByCode = async (req: Request, res: Response) => { try { const { code } = req.params; const companyCode = req.user?.companyCode || "*"; let query = ` SELECT relation_id, relation_code, relation_name, description, parent_table, parent_value_column, parent_label_column, child_table, child_filter_column, child_value_column, child_label_column, child_order_column, child_order_direction, empty_parent_message, no_options_message, loading_message, clear_on_parent_change, company_code, is_active FROM cascading_relation WHERE relation_code = $1 AND is_active = 'Y' `; const params: any[] = [code]; // 멀티테넌시 필터링 (회사 전용 관계 우선, 없으면 공통 관계) if (companyCode !== "*") { query += ` AND (company_code = $2 OR company_code = '*')`; params.push(companyCode); query += ` ORDER BY CASE WHEN company_code = $2 THEN 0 ELSE 1 END LIMIT 1`; } else { query += ` LIMIT 1`; } const result = await pool.query(query, params); if (result.rowCount === 0) { return res.status(404).json({ success: false, message: "연쇄 관계를 찾을 수 없습니다.", }); } return res.json({ success: true, data: result.rows[0], }); } catch (error: any) { logger.error("연쇄 관계 코드 조회 실패", { error: error.message }); return res.status(500).json({ success: false, message: "연쇄 관계 조회에 실패했습니다.", error: error.message, }); } }; /** * 연쇄 관계 생성 */ export const createCascadingRelation = async (req: Request, res: Response) => { try { const companyCode = req.user?.companyCode || "*"; const userId = req.user?.userId || "system"; const { relationCode, relationName, description, parentTable, parentValueColumn, parentLabelColumn, childTable, childFilterColumn, childValueColumn, childLabelColumn, childOrderColumn, childOrderDirection, emptyParentMessage, noOptionsMessage, loadingMessage, clearOnParentChange, } = req.body; // 필수 필드 검증 if (!relationCode || !relationName || !parentTable || !parentValueColumn || !childTable || !childFilterColumn || !childValueColumn || !childLabelColumn) { return res.status(400).json({ success: false, message: "필수 필드가 누락되었습니다.", }); } // 중복 코드 체크 const duplicateCheck = await pool.query( `SELECT relation_id FROM cascading_relation WHERE relation_code = $1 AND company_code = $2`, [relationCode, companyCode] ); if (duplicateCheck.rowCount && duplicateCheck.rowCount > 0) { return res.status(400).json({ success: false, message: "이미 존재하는 관계 코드입니다.", }); } const query = ` INSERT INTO cascading_relation ( relation_code, relation_name, description, parent_table, parent_value_column, parent_label_column, child_table, child_filter_column, child_value_column, child_label_column, child_order_column, child_order_direction, empty_parent_message, no_options_message, loading_message, clear_on_parent_change, company_code, is_active, created_by, created_date ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, 'Y', $18, CURRENT_TIMESTAMP) RETURNING * `; const result = await pool.query(query, [ relationCode, relationName, description || null, parentTable, parentValueColumn, parentLabelColumn || null, childTable, childFilterColumn, childValueColumn, childLabelColumn, childOrderColumn || null, childOrderDirection || "ASC", emptyParentMessage || "상위 항목을 먼저 선택하세요", noOptionsMessage || "선택 가능한 항목이 없습니다", loadingMessage || "로딩 중...", clearOnParentChange !== false ? "Y" : "N", companyCode, userId, ]); logger.info("연쇄 관계 생성", { relationId: result.rows[0].relation_id, relationCode, companyCode, userId, }); return res.status(201).json({ success: true, data: result.rows[0], message: "연쇄 관계가 생성되었습니다.", }); } catch (error: any) { logger.error("연쇄 관계 생성 실패", { error: error.message }); return res.status(500).json({ success: false, message: "연쇄 관계 생성에 실패했습니다.", error: error.message, }); } }; /** * 연쇄 관계 수정 */ export const updateCascadingRelation = async (req: Request, res: Response) => { try { const { id } = req.params; const companyCode = req.user?.companyCode || "*"; const userId = req.user?.userId || "system"; const { relationName, description, parentTable, parentValueColumn, parentLabelColumn, childTable, childFilterColumn, childValueColumn, childLabelColumn, childOrderColumn, childOrderDirection, emptyParentMessage, noOptionsMessage, loadingMessage, clearOnParentChange, isActive, } = req.body; // 권한 체크 const existingCheck = await pool.query( `SELECT relation_id, company_code FROM cascading_relation WHERE relation_id = $1`, [id] ); if (existingCheck.rowCount === 0) { return res.status(404).json({ success: false, message: "연쇄 관계를 찾을 수 없습니다.", }); } // 다른 회사의 데이터는 수정 불가 (최고 관리자 제외) const existingCompanyCode = existingCheck.rows[0].company_code; if (companyCode !== "*" && existingCompanyCode !== companyCode && existingCompanyCode !== "*") { return res.status(403).json({ success: false, message: "수정 권한이 없습니다.", }); } const query = ` UPDATE cascading_relation SET relation_name = COALESCE($1, relation_name), description = COALESCE($2, description), parent_table = COALESCE($3, parent_table), parent_value_column = COALESCE($4, parent_value_column), parent_label_column = COALESCE($5, parent_label_column), child_table = COALESCE($6, child_table), child_filter_column = COALESCE($7, child_filter_column), child_value_column = COALESCE($8, child_value_column), child_label_column = COALESCE($9, child_label_column), child_order_column = COALESCE($10, child_order_column), child_order_direction = COALESCE($11, child_order_direction), empty_parent_message = COALESCE($12, empty_parent_message), no_options_message = COALESCE($13, no_options_message), loading_message = COALESCE($14, loading_message), clear_on_parent_change = COALESCE($15, clear_on_parent_change), is_active = COALESCE($16, is_active), updated_by = $17, updated_date = CURRENT_TIMESTAMP WHERE relation_id = $18 RETURNING * `; const result = await pool.query(query, [ relationName, description, parentTable, parentValueColumn, parentLabelColumn, childTable, childFilterColumn, childValueColumn, childLabelColumn, childOrderColumn, childOrderDirection, emptyParentMessage, noOptionsMessage, loadingMessage, clearOnParentChange !== undefined ? (clearOnParentChange ? "Y" : "N") : null, isActive !== undefined ? (isActive ? "Y" : "N") : null, userId, id, ]); logger.info("연쇄 관계 수정", { relationId: id, companyCode, userId, }); return res.json({ success: true, data: result.rows[0], message: "연쇄 관계가 수정되었습니다.", }); } catch (error: any) { logger.error("연쇄 관계 수정 실패", { error: error.message }); return res.status(500).json({ success: false, message: "연쇄 관계 수정에 실패했습니다.", error: error.message, }); } }; /** * 연쇄 관계 삭제 */ export const deleteCascadingRelation = async (req: Request, res: Response) => { try { const { id } = req.params; const companyCode = req.user?.companyCode || "*"; const userId = req.user?.userId || "system"; // 권한 체크 const existingCheck = await pool.query( `SELECT relation_id, company_code FROM cascading_relation WHERE relation_id = $1`, [id] ); if (existingCheck.rowCount === 0) { return res.status(404).json({ success: false, message: "연쇄 관계를 찾을 수 없습니다.", }); } // 다른 회사의 데이터는 삭제 불가 (최고 관리자 제외) const existingCompanyCode = existingCheck.rows[0].company_code; if (companyCode !== "*" && existingCompanyCode !== companyCode && existingCompanyCode !== "*") { return res.status(403).json({ success: false, message: "삭제 권한이 없습니다.", }); } // 소프트 삭제 (is_active = 'N') await pool.query( `UPDATE cascading_relation SET is_active = 'N', updated_by = $1, updated_date = CURRENT_TIMESTAMP WHERE relation_id = $2`, [userId, id] ); logger.info("연쇄 관계 삭제", { relationId: id, companyCode, userId, }); return res.json({ success: true, message: "연쇄 관계가 삭제되었습니다.", }); } catch (error: any) { logger.error("연쇄 관계 삭제 실패", { error: error.message }); return res.status(500).json({ success: false, message: "연쇄 관계 삭제에 실패했습니다.", error: error.message, }); } }; /** * 🆕 연쇄 관계로 부모 옵션 조회 (상위 선택 역할용) * parent_table에서 전체 옵션을 조회합니다. */ export const getParentOptions = async (req: Request, res: Response) => { try { const { code } = req.params; const companyCode = req.user?.companyCode || "*"; // 관계 정보 조회 let relationQuery = ` SELECT parent_table, parent_value_column, parent_label_column FROM cascading_relation WHERE relation_code = $1 AND is_active = 'Y' `; const relationParams: any[] = [code]; if (companyCode !== "*") { relationQuery += ` AND (company_code = $2 OR company_code = '*')`; relationParams.push(companyCode); relationQuery += ` ORDER BY CASE WHEN company_code = $2 THEN 0 ELSE 1 END LIMIT 1`; } else { relationQuery += ` LIMIT 1`; } const relationResult = await pool.query(relationQuery, relationParams); if (relationResult.rowCount === 0) { return res.status(404).json({ success: false, message: "연쇄 관계를 찾을 수 없습니다.", }); } const relation = relationResult.rows[0]; // 라벨 컬럼이 없으면 값 컬럼 사용 const labelColumn = relation.parent_label_column || relation.parent_value_column; // 부모 옵션 조회 let optionsQuery = ` SELECT ${relation.parent_value_column} as value, ${labelColumn} as label FROM ${relation.parent_table} WHERE 1=1 `; // 멀티테넌시 적용 (테이블에 company_code가 있는 경우) const tableInfoResult = await pool.query( `SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND column_name = 'company_code'`, [relation.parent_table] ); const optionsParams: any[] = []; if (tableInfoResult.rowCount && tableInfoResult.rowCount > 0 && companyCode !== "*") { optionsQuery += ` AND (company_code = $1 OR company_code = '*')`; optionsParams.push(companyCode); } // status 컬럼이 있으면 활성 상태만 조회 const statusInfoResult = await pool.query( `SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND column_name = 'status'`, [relation.parent_table] ); if (statusInfoResult.rowCount && statusInfoResult.rowCount > 0) { optionsQuery += ` AND (status IS NULL OR status != 'N')`; } // 정렬 optionsQuery += ` ORDER BY ${labelColumn} ASC`; const optionsResult = await pool.query(optionsQuery, optionsParams); logger.info("부모 옵션 조회", { relationCode: code, parentTable: relation.parent_table, optionsCount: optionsResult.rowCount, }); return res.json({ success: true, data: optionsResult.rows, }); } catch (error: any) { logger.error("부모 옵션 조회 실패", { error: error.message }); return res.status(500).json({ success: false, message: "부모 옵션 조회에 실패했습니다.", error: error.message, }); } }; /** * 연쇄 관계로 자식 옵션 조회 * 실제 연쇄 드롭다운에서 사용하는 API */ export const getCascadingOptions = async (req: Request, res: Response) => { try { const { code } = req.params; const { parentValue } = req.query; const companyCode = req.user?.companyCode || "*"; if (!parentValue) { return res.json({ success: true, data: [], message: "부모 값이 없습니다.", }); } // 관계 정보 조회 let relationQuery = ` SELECT child_table, child_filter_column, child_value_column, child_label_column, child_order_column, child_order_direction FROM cascading_relation WHERE relation_code = $1 AND is_active = 'Y' `; const relationParams: any[] = [code]; if (companyCode !== "*") { relationQuery += ` AND (company_code = $2 OR company_code = '*')`; relationParams.push(companyCode); relationQuery += ` ORDER BY CASE WHEN company_code = $2 THEN 0 ELSE 1 END LIMIT 1`; } else { relationQuery += ` LIMIT 1`; } const relationResult = await pool.query(relationQuery, relationParams); if (relationResult.rowCount === 0) { return res.status(404).json({ success: false, message: "연쇄 관계를 찾을 수 없습니다.", }); } const relation = relationResult.rows[0]; // 자식 옵션 조회 let optionsQuery = ` SELECT ${relation.child_value_column} as value, ${relation.child_label_column} as label FROM ${relation.child_table} WHERE ${relation.child_filter_column} = $1 `; // 멀티테넌시 적용 (테이블에 company_code가 있는 경우) const tableInfoResult = await pool.query( `SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND column_name = 'company_code'`, [relation.child_table] ); const optionsParams: any[] = [parentValue]; if (tableInfoResult.rowCount && tableInfoResult.rowCount > 0 && companyCode !== "*") { optionsQuery += ` AND (company_code = $2 OR company_code = '*')`; optionsParams.push(companyCode); } // 정렬 if (relation.child_order_column) { optionsQuery += ` ORDER BY ${relation.child_order_column} ${relation.child_order_direction || "ASC"}`; } else { optionsQuery += ` ORDER BY ${relation.child_label_column} ASC`; } const optionsResult = await pool.query(optionsQuery, optionsParams); logger.info("연쇄 옵션 조회", { relationCode: code, parentValue, optionsCount: optionsResult.rowCount, }); return res.json({ success: true, data: optionsResult.rows, }); } catch (error: any) { logger.error("연쇄 옵션 조회 실패", { error: error.message }); return res.status(500).json({ success: false, message: "연쇄 옵션 조회에 실패했습니다.", error: error.message, }); } };