/** * 자동 입력 (Auto-Fill) 컨트롤러 * 마스터 선택 시 여러 필드 자동 입력 기능 */ import { Response } from "express"; import { AuthenticatedRequest } from "../types/auth"; import { query, queryOne } from "../database/db"; import logger from "../utils/logger"; // ===================================================== // 자동 입력 그룹 CRUD // ===================================================== /** * 자동 입력 그룹 목록 조회 */ export const getAutoFillGroups = async ( req: AuthenticatedRequest, res: Response ) => { try { const companyCode = req.user?.companyCode || "*"; const { isActive } = req.query; let sql = ` SELECT g.*, COUNT(m.mapping_id) as mapping_count FROM cascading_auto_fill_group g LEFT JOIN cascading_auto_fill_mapping m ON g.group_code = m.group_code AND g.company_code = m.company_code WHERE 1=1 `; const params: any[] = []; let paramIndex = 1; // 회사 필터 if (companyCode !== "*") { sql += ` AND g.company_code = $${paramIndex++}`; params.push(companyCode); } // 활성 상태 필터 if (isActive) { sql += ` AND g.is_active = $${paramIndex++}`; params.push(isActive); } sql += ` GROUP BY g.group_id ORDER BY g.group_name`; const result = await query(sql, params); logger.info("자동 입력 그룹 목록 조회", { count: result.length, companyCode, }); res.json({ success: true, data: result, }); } catch (error: any) { logger.error("자동 입력 그룹 목록 조회 실패", { error: error.message }); res.status(500).json({ success: false, message: "자동 입력 그룹 목록 조회에 실패했습니다.", error: error.message, }); } }; /** * 자동 입력 그룹 상세 조회 (매핑 포함) */ export const getAutoFillGroupDetail = async ( req: AuthenticatedRequest, res: Response ) => { try { const { groupCode } = req.params; const companyCode = req.user?.companyCode || "*"; // 그룹 정보 조회 let groupSql = ` SELECT * FROM cascading_auto_fill_group WHERE group_code = $1 `; const groupParams: any[] = [groupCode]; if (companyCode !== "*") { groupSql += ` AND company_code = $2`; groupParams.push(companyCode); } const groupResult = await queryOne(groupSql, groupParams); if (!groupResult) { return res.status(404).json({ success: false, message: "자동 입력 그룹을 찾을 수 없습니다.", }); } // 매핑 정보 조회 const mappingSql = ` SELECT * FROM cascading_auto_fill_mapping WHERE group_code = $1 AND company_code = $2 ORDER BY sort_order, mapping_id `; const mappingResult = await query(mappingSql, [ groupCode, groupResult.company_code, ]); logger.info("자동 입력 그룹 상세 조회", { groupCode, companyCode }); res.json({ success: true, data: { ...groupResult, mappings: mappingResult, }, }); } catch (error: any) { logger.error("자동 입력 그룹 상세 조회 실패", { error: error.message }); res.status(500).json({ success: false, message: "자동 입력 그룹 상세 조회에 실패했습니다.", error: error.message, }); } }; /** * 그룹 코드 자동 생성 함수 */ const generateAutoFillGroupCode = async ( companyCode: string ): Promise => { const prefix = "AF"; const result = await queryOne( `SELECT COUNT(*) as cnt FROM cascading_auto_fill_group WHERE company_code = $1`, [companyCode] ); const count = parseInt(result?.cnt || "0", 10) + 1; const timestamp = Date.now().toString(36).toUpperCase().slice(-4); return `${prefix}_${timestamp}_${count.toString().padStart(3, "0")}`; }; /** * 자동 입력 그룹 생성 */ export const createAutoFillGroup = async ( req: AuthenticatedRequest, res: Response ) => { try { const companyCode = req.user?.companyCode || "*"; const userId = req.user?.userId || "system"; const { groupName, description, masterTable, masterValueColumn, masterLabelColumn, mappings = [], } = req.body; // 필수 필드 검증 if (!groupName || !masterTable || !masterValueColumn) { return res.status(400).json({ success: false, message: "필수 필드가 누락되었습니다. (groupName, masterTable, masterValueColumn)", }); } // 그룹 코드 자동 생성 const groupCode = await generateAutoFillGroupCode(companyCode); // 그룹 생성 const insertGroupSql = ` INSERT INTO cascading_auto_fill_group ( group_code, group_name, description, master_table, master_value_column, master_label_column, company_code, is_active, created_date ) VALUES ($1, $2, $3, $4, $5, $6, $7, 'Y', CURRENT_TIMESTAMP) RETURNING * `; const groupResult = await queryOne(insertGroupSql, [ groupCode, groupName, description || null, masterTable, masterValueColumn, masterLabelColumn || null, companyCode, ]); // 매핑 생성 if (mappings.length > 0) { for (let i = 0; i < mappings.length; i++) { const m = mappings[i]; await query( `INSERT INTO cascading_auto_fill_mapping ( group_code, company_code, source_column, target_field, target_label, is_editable, is_required, default_value, sort_order ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [ groupCode, companyCode, m.sourceColumn, m.targetField, m.targetLabel || null, m.isEditable || "Y", m.isRequired || "N", m.defaultValue || null, m.sortOrder || i + 1, ] ); } } logger.info("자동 입력 그룹 생성", { groupCode, companyCode, userId }); res.status(201).json({ success: true, message: "자동 입력 그룹이 생성되었습니다.", data: groupResult, }); } catch (error: any) { logger.error("자동 입력 그룹 생성 실패", { error: error.message }); res.status(500).json({ success: false, message: "자동 입력 그룹 생성에 실패했습니다.", error: error.message, }); } }; /** * 자동 입력 그룹 수정 */ export const updateAutoFillGroup = async ( req: AuthenticatedRequest, res: Response ) => { try { const { groupCode } = req.params; const companyCode = req.user?.companyCode || "*"; const userId = req.user?.userId || "system"; const { groupName, description, masterTable, masterValueColumn, masterLabelColumn, isActive, mappings, } = req.body; // 기존 그룹 확인 let checkSql = `SELECT * FROM cascading_auto_fill_group WHERE group_code = $1`; const checkParams: any[] = [groupCode]; if (companyCode !== "*") { checkSql += ` AND company_code = $2`; checkParams.push(companyCode); } const existing = await queryOne(checkSql, checkParams); if (!existing) { return res.status(404).json({ success: false, message: "자동 입력 그룹을 찾을 수 없습니다.", }); } // 그룹 업데이트 const updateSql = ` UPDATE cascading_auto_fill_group SET group_name = COALESCE($1, group_name), description = COALESCE($2, description), master_table = COALESCE($3, master_table), master_value_column = COALESCE($4, master_value_column), master_label_column = COALESCE($5, master_label_column), is_active = COALESCE($6, is_active), updated_date = CURRENT_TIMESTAMP WHERE group_code = $7 AND company_code = $8 RETURNING * `; const updateResult = await queryOne(updateSql, [ groupName, description, masterTable, masterValueColumn, masterLabelColumn, isActive, groupCode, existing.company_code, ]); // 매핑 업데이트 (전체 교체 방식) if (mappings !== undefined) { // 기존 매핑 삭제 await query( `DELETE FROM cascading_auto_fill_mapping WHERE group_code = $1 AND company_code = $2`, [groupCode, existing.company_code] ); // 새 매핑 추가 for (let i = 0; i < mappings.length; i++) { const m = mappings[i]; await query( `INSERT INTO cascading_auto_fill_mapping ( group_code, company_code, source_column, target_field, target_label, is_editable, is_required, default_value, sort_order ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [ groupCode, existing.company_code, m.sourceColumn, m.targetField, m.targetLabel || null, m.isEditable || "Y", m.isRequired || "N", m.defaultValue || null, m.sortOrder || i + 1, ] ); } } logger.info("자동 입력 그룹 수정", { groupCode, companyCode, userId }); res.json({ success: true, message: "자동 입력 그룹이 수정되었습니다.", data: updateResult, }); } catch (error: any) { logger.error("자동 입력 그룹 수정 실패", { error: error.message }); res.status(500).json({ success: false, message: "자동 입력 그룹 수정에 실패했습니다.", error: error.message, }); } }; /** * 자동 입력 그룹 삭제 */ export const deleteAutoFillGroup = async ( req: AuthenticatedRequest, res: Response ) => { try { const { groupCode } = req.params; const companyCode = req.user?.companyCode || "*"; const userId = req.user?.userId || "system"; let deleteSql = `DELETE FROM cascading_auto_fill_group WHERE group_code = $1`; const deleteParams: any[] = [groupCode]; if (companyCode !== "*") { deleteSql += ` AND company_code = $2`; deleteParams.push(companyCode); } deleteSql += ` RETURNING group_code`; const result = await queryOne(deleteSql, deleteParams); if (!result) { return res.status(404).json({ success: false, message: "자동 입력 그룹을 찾을 수 없습니다.", }); } logger.info("자동 입력 그룹 삭제", { groupCode, companyCode, userId }); res.json({ success: true, message: "자동 입력 그룹이 삭제되었습니다.", }); } catch (error: any) { logger.error("자동 입력 그룹 삭제 실패", { error: error.message }); res.status(500).json({ success: false, message: "자동 입력 그룹 삭제에 실패했습니다.", error: error.message, }); } }; // ===================================================== // 자동 입력 데이터 조회 (실제 사용) // ===================================================== /** * 마스터 옵션 목록 조회 * 자동 입력 그룹의 마스터 테이블에서 선택 가능한 옵션 목록 */ export const getAutoFillMasterOptions = async ( req: AuthenticatedRequest, res: Response ) => { try { const { groupCode } = req.params; const companyCode = req.user?.companyCode || "*"; // 그룹 정보 조회 let groupSql = `SELECT * FROM cascading_auto_fill_group WHERE group_code = $1 AND is_active = 'Y'`; const groupParams: any[] = [groupCode]; if (companyCode !== "*") { groupSql += ` AND company_code = $2`; groupParams.push(companyCode); } const group = await queryOne(groupSql, groupParams); if (!group) { return res.status(404).json({ success: false, message: "자동 입력 그룹을 찾을 수 없습니다.", }); } // 마스터 테이블에서 옵션 조회 const labelColumn = group.master_label_column || group.master_value_column; let optionsSql = ` SELECT ${group.master_value_column} as value, ${labelColumn} as label FROM ${group.master_table} WHERE 1=1 `; const optionsParams: any[] = []; let paramIndex = 1; // 멀티테넌시 필터 (테이블에 company_code가 있는 경우) if (companyCode !== "*") { // company_code 컬럼 존재 여부 확인 const columnCheck = await queryOne( `SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND column_name = 'company_code'`, [group.master_table] ); if (columnCheck) { optionsSql += ` AND company_code = $${paramIndex++}`; optionsParams.push(companyCode); } } optionsSql += ` ORDER BY ${labelColumn}`; const optionsResult = await query(optionsSql, optionsParams); logger.info("자동 입력 마스터 옵션 조회", { groupCode, count: optionsResult.length, }); res.json({ success: true, data: optionsResult, }); } catch (error: any) { logger.error("자동 입력 마스터 옵션 조회 실패", { error: error.message }); res.status(500).json({ success: false, message: "자동 입력 마스터 옵션 조회에 실패했습니다.", error: error.message, }); } }; /** * 자동 입력 데이터 조회 * 마스터 값 선택 시 자동으로 입력할 데이터 조회 */ export const getAutoFillData = async ( req: AuthenticatedRequest, res: Response ) => { try { const { groupCode } = req.params; const { masterValue } = req.query; const companyCode = req.user?.companyCode || "*"; if (!masterValue) { return res.status(400).json({ success: false, message: "masterValue 파라미터가 필요합니다.", }); } // 그룹 정보 조회 let groupSql = `SELECT * FROM cascading_auto_fill_group WHERE group_code = $1 AND is_active = 'Y'`; const groupParams: any[] = [groupCode]; if (companyCode !== "*") { groupSql += ` AND company_code = $2`; groupParams.push(companyCode); } const group = await queryOne(groupSql, groupParams); if (!group) { return res.status(404).json({ success: false, message: "자동 입력 그룹을 찾을 수 없습니다.", }); } // 매핑 정보 조회 const mappingSql = ` SELECT * FROM cascading_auto_fill_mapping WHERE group_code = $1 AND company_code = $2 ORDER BY sort_order `; const mappings = await query(mappingSql, [groupCode, group.company_code]); if (mappings.length === 0) { return res.json({ success: true, data: {}, mappings: [], }); } // 마스터 테이블에서 데이터 조회 const sourceColumns = mappings.map((m: any) => m.source_column).join(", "); let dataSql = ` SELECT ${sourceColumns} FROM ${group.master_table} WHERE ${group.master_value_column} = $1 `; const dataParams: any[] = [masterValue]; let paramIndex = 2; // 멀티테넌시 필터 if (companyCode !== "*") { const columnCheck = await queryOne( `SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND column_name = 'company_code'`, [group.master_table] ); if (columnCheck) { dataSql += ` AND company_code = $${paramIndex++}`; dataParams.push(companyCode); } } const dataResult = await queryOne(dataSql, dataParams); // 결과를 target_field 기준으로 변환 const autoFillData: Record = {}; const mappingInfo: any[] = []; for (const mapping of mappings) { const sourceValue = dataResult?.[mapping.source_column]; const finalValue = sourceValue !== null && sourceValue !== undefined ? sourceValue : mapping.default_value; autoFillData[mapping.target_field] = finalValue; mappingInfo.push({ targetField: mapping.target_field, targetLabel: mapping.target_label, value: finalValue, isEditable: mapping.is_editable === "Y", isRequired: mapping.is_required === "Y", }); } logger.info("자동 입력 데이터 조회", { groupCode, masterValue, fieldCount: mappingInfo.length, }); res.json({ success: true, data: autoFillData, mappings: mappingInfo, }); } catch (error: any) { logger.error("자동 입력 데이터 조회 실패", { error: error.message }); res.status(500).json({ success: false, message: "자동 입력 데이터 조회에 실패했습니다.", error: error.message, }); } };