package com.erp.service; import com.erp.common.BaseService; import com.erp.constants.InputTypeConstants; import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionTemplate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; @Service @Slf4j public class DdlService extends BaseService { private static final String NS = "ddl."; private final JdbcTemplate jdbcTemplate; private final TransactionTemplate transactionTemplate; private static final Set SYSTEM_TABLES = Set.of( "user_info", "company_mng", "menu_info", "auth_group", "table_labels", "column_labels", "screen_definitions", "screen_layouts", "common_code", "multi_lang_key_master", "multi_lang_text", "button_action_standards", "ddl_execution_log" ); private static final Set RESERVED_WORDS = Set.of( "user", "order", "group", "table", "column", "index", "select", "insert", "update", "delete", "from", "where", "join", "on", "as", "and", "or", "not", "null", "true", "false", "create", "alter", "drop", "primary", "key", "foreign", "references", "constraint", "default", "unique", "check", "view", "procedure", "function" ); private static final Set RESERVED_COLUMNS = Set.of( "id", "created_date", "updated_date", "company_code" ); public DdlService(JdbcTemplate jdbcTemplate, PlatformTransactionManager transactionManager) { this.jdbcTemplate = jdbcTemplate; this.transactionTemplate = new TransactionTemplate(transactionManager); } // ───────────────────────────────────────────────────────────────────────── // CREATE TABLE // ───────────────────────────────────────────────────────────────────────── public Map createTable(String tableName, List> columns, String companyCode, String userId, String description) { // 1. 검증 Map validation = validateTableCreation(tableName, columns); if (!(Boolean) validation.get("is_valid")) { @SuppressWarnings("unchecked") List errors = (List) validation.get("errors"); String errorMsg = "테이블 생성 검증 실패: " + String.join(", ", errors); logDdlOperation(userId, companyCode, "CREATE_TABLE", tableName, "VALIDATION_FAILED", false, errorMsg); return Map.of("success", false, "message", errorMsg, "error_code", "VALIDATION_FAILED"); } // 2. 테이블 존재 여부 확인 if (tableExists(tableName)) { String errorMsg = "테이블 '" + tableName + "'이 이미 존재합니다."; logDdlOperation(userId, companyCode, "CREATE_TABLE", tableName, "TABLE_EXISTS", false, errorMsg); return Map.of("success", false, "message", errorMsg, "error_code", "TABLE_EXISTS"); } // 3. DDL 쿼리 생성 String ddlQuery = generateCreateTableQuery(tableName, columns); // 4. 트랜잭션으로 DDL 실행 + 메타데이터 저장 try { final String finalTableName = tableName; final List> finalColumns = columns; final String finalCompanyCode = companyCode; final String finalDescription = description; transactionTemplate.execute(status -> { jdbcTemplate.execute(ddlQuery); saveTableMetadata(finalTableName, finalDescription); saveColumnMetadata(finalTableName, finalColumns, finalCompanyCode); return null; }); // 5. 성공 로그 (트랜잭션 밖) logDdlOperation(userId, companyCode, "CREATE_TABLE", tableName, ddlQuery, true, null); log.info("테이블 생성 성공: {}, 사용자: {}, 컬럼수: {}", tableName, userId, columns.size()); return Map.of( "success", true, "message", "테이블 '" + tableName + "'이 성공적으로 생성되었습니다.", "table_name", tableName, "column_count", columns.size(), "executed_query", ddlQuery ); } catch (Exception e) { String errorMsg = "테이블 생성 실패: " + e.getMessage(); logDdlOperation(userId, companyCode, "CREATE_TABLE", tableName, "FAILED: " + e.getMessage(), false, errorMsg); log.error("테이블 생성 실패: {}, 사용자: {}, 오류: {}", tableName, userId, e.getMessage(), e); return Map.of("success", false, "message", errorMsg, "error_code", "EXECUTION_FAILED"); } } // ───────────────────────────────────────────────────────────────────────── // ADD COLUMN // ───────────────────────────────────────────────────────────────────────── public Map addColumn(String tableName, Map column, String companyCode, String userId) { // 1. 검증 List errors = validateColumnForAddition(tableName, column); if (!errors.isEmpty()) { String errorMsg = "컬럼 추가 검증 실패: " + String.join(", ", errors); logDdlOperation(userId, companyCode, "ADD_COLUMN", tableName, "VALIDATION_FAILED", false, errorMsg); return Map.of("success", false, "message", errorMsg, "error_code", "VALIDATION_FAILED"); } // 2. 테이블 존재 여부 확인 if (!tableExists(tableName)) { String errorMsg = "테이블 '" + tableName + "'이 존재하지 않습니다."; logDdlOperation(userId, companyCode, "ADD_COLUMN", tableName, "TABLE_NOT_EXISTS", false, errorMsg); return Map.of("success", false, "message", errorMsg, "error_code", "TABLE_NOT_EXISTS"); } // 3. 컬럼 존재 여부 확인 String colName = (String) column.get("name"); if (columnExists(tableName, colName)) { String errorMsg = "컬럼 '" + colName + "'이 이미 존재합니다."; logDdlOperation(userId, companyCode, "ADD_COLUMN", tableName, "COLUMN_EXISTS", false, errorMsg); return Map.of("success", false, "message", errorMsg, "error_code", "COLUMN_EXISTS"); } // 4. DDL 쿼리 생성 String ddlQuery = generateAddColumnQuery(tableName, column); // 5. 트랜잭션으로 DDL 실행 + 메타데이터 저장 try { transactionTemplate.execute(status -> { jdbcTemplate.execute(ddlQuery); String inputType = convertToInputType(column); if (!InputTypeConstants.USER_SELECTABLE_INPUT_TYPES.contains(inputType)) { throw new IllegalArgumentException( "INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + InputTypeConstants.USER_SELECTABLE_INPUT_TYPES + " (받은 값: " + inputType + ")" ); } String detailSettings = column.containsKey("detail_settings") ? column.get("detail_settings").toString() : "{}"; Integer maxOrder = jdbcTemplate.queryForObject( "SELECT COALESCE(MAX(display_order), 0) FROM table_type_columns " + "WHERE table_name = ? AND company_code = ?", Integer.class, tableName, companyCode); saveColumnMeta(tableName, colName, companyCode, inputType, detailSettings, (maxOrder != null ? maxOrder : 0) + 1); return null; }); // 6. 성공 로그 (트랜잭션 밖) logDdlOperation(userId, companyCode, "ADD_COLUMN", tableName, ddlQuery, true, null); log.info("컬럼 추가 성공: {}.{}, 사용자: {}", tableName, colName, userId); return Map.of( "success", true, "message", "컬럼 '" + colName + "'이 성공적으로 추가되었습니다.", "table_name", tableName, "column_name", colName, "executed_query", ddlQuery ); } catch (Exception e) { String errorMsg = "컬럼 추가 실패: " + e.getMessage(); logDdlOperation(userId, companyCode, "ADD_COLUMN", tableName, "FAILED: " + e.getMessage(), false, errorMsg); log.error("컬럼 추가 실패: {}.{}, 사용자: {}, 오류: {}", tableName, colName, userId, e.getMessage(), e); return Map.of("success", false, "message", errorMsg, "error_code", "EXECUTION_FAILED"); } } // ───────────────────────────────────────────────────────────────────────── // DROP TABLE // ───────────────────────────────────────────────────────────────────────── public Map dropTable(String tableName, String companyCode, String userId) { // 1. 시스템 테이블 보호 if (SYSTEM_TABLES.contains(tableName.toLowerCase())) { String errorMsg = "'" + tableName + "'은 시스템 테이블이므로 삭제할 수 없습니다."; logDdlOperation(userId, companyCode, "DROP_TABLE", tableName, "SYSTEM_TABLE_PROTECTED", false, errorMsg); return Map.of("success", false, "message", errorMsg, "error_code", "SYSTEM_TABLE_PROTECTED"); } // 2. 테이블 존재 여부 확인 if (!tableExists(tableName)) { String errorMsg = "테이블 '" + tableName + "'이 존재하지 않습니다."; logDdlOperation(userId, companyCode, "DROP_TABLE", tableName, "TABLE_NOT_FOUND", false, errorMsg); return Map.of("success", false, "message", errorMsg, "error_code", "TABLE_NOT_FOUND"); } String ddlQuery = "DROP TABLE IF EXISTS \"" + sanitize(tableName) + "\" CASCADE"; try { transactionTemplate.execute(status -> { jdbcTemplate.execute(ddlQuery); jdbcTemplate.update("DELETE FROM table_type_columns WHERE table_name = ?", tableName); jdbcTemplate.update("DELETE FROM table_labels WHERE table_name = ?", tableName); return null; }); logDdlOperation(userId, companyCode, "DROP_TABLE", tableName, ddlQuery, true, null); log.info("테이블 삭제 성공: {}, 사용자: {}", tableName, userId); return Map.of( "success", true, "message", "테이블 '" + tableName + "'이 성공적으로 삭제되었습니다.", "table_name", tableName, "executed_query", ddlQuery ); } catch (Exception e) { String errorMsg = "테이블 삭제 실패: " + e.getMessage(); logDdlOperation(userId, companyCode, "DROP_TABLE", tableName, "FAILED: " + e.getMessage(), false, errorMsg); log.error("테이블 삭제 실패: {}, 사용자: {}, 오류: {}", tableName, userId, e.getMessage(), e); return Map.of("success", false, "message", errorMsg, "error_code", "EXECUTION_FAILED"); } } // ───────────────────────────────────────────────────────────────────────── // DROP COLUMN (DBeaver 방식: FK 등 위반은 Postgres 가 던지는 에러를 그대로 노출) // ───────────────────────────────────────────────────────────────────────── public Map dropColumn(String tableName, String columnName, String companyCode, String userId) { // 1. 시스템 테이블 보호 if (SYSTEM_TABLES.contains(tableName.toLowerCase())) { String errorMsg = "'" + tableName + "'은 시스템 테이블이므로 컬럼을 삭제할 수 없습니다."; logDdlOperation(userId, companyCode, "DROP_COLUMN", tableName, "SYSTEM_TABLE_PROTECTED", false, errorMsg); return Map.of("success", false, "message", errorMsg, "error_code", "SYSTEM_TABLE_PROTECTED"); } // 2. 예약 컬럼 보호 (id / created_date / updated_date / company_code / writer) if (RESERVED_COLUMNS.contains(columnName.toLowerCase()) || "writer".equalsIgnoreCase(columnName)) { String errorMsg = "'" + columnName + "'은 시스템 예약 컬럼이므로 삭제할 수 없습니다."; logDdlOperation(userId, companyCode, "DROP_COLUMN", tableName, "RESERVED_COLUMN_PROTECTED", false, errorMsg); return Map.of("success", false, "message", errorMsg, "error_code", "RESERVED_COLUMN_PROTECTED"); } // 3. 테이블/컬럼 존재 여부 if (!tableExists(tableName)) { String errorMsg = "테이블 '" + tableName + "'이 존재하지 않습니다."; logDdlOperation(userId, companyCode, "DROP_COLUMN", tableName, "TABLE_NOT_FOUND", false, errorMsg); return Map.of("success", false, "message", errorMsg, "error_code", "TABLE_NOT_FOUND"); } if (!columnExists(tableName, columnName)) { String errorMsg = "컬럼 '" + columnName + "'이 존재하지 않습니다."; logDdlOperation(userId, companyCode, "DROP_COLUMN", tableName, "COLUMN_NOT_FOUND", false, errorMsg); return Map.of("success", false, "message", errorMsg, "error_code", "COLUMN_NOT_FOUND"); } // 4. DDL 실행 — CASCADE 안 붙임 → FK 참조 있으면 Postgres 가 거부 (DBeaver 와 동일) String ddlQuery = "ALTER TABLE \"" + sanitize(tableName) + "\" DROP COLUMN \"" + sanitize(columnName) + "\""; try { transactionTemplate.execute(status -> { jdbcTemplate.execute(ddlQuery); // 컬럼 메타 청소 jdbcTemplate.update( "DELETE FROM table_type_columns WHERE table_name = ? AND column_name = ?", tableName, columnName); jdbcTemplate.update( "DELETE FROM column_labels WHERE table_name = ? AND column_name = ?", tableName, columnName); return null; }); logDdlOperation(userId, companyCode, "DROP_COLUMN", tableName, ddlQuery, true, null); log.info("컬럼 삭제 성공: {}.{}, 사용자: {}", tableName, columnName, userId); return Map.of( "success", true, "message", "컬럼 '" + columnName + "'이 성공적으로 삭제되었습니다.", "table_name", tableName, "column_name", columnName, "executed_query", ddlQuery ); } catch (Exception e) { String rawMsg = e.getMessage() != null ? e.getMessage() : ""; String guidance = rawMsg.toLowerCase().contains("depend") || rawMsg.toLowerCase().contains("foreign key") ? " (다른 테이블에서 외래키로 참조 중인 컬럼은 삭제할 수 없습니다)" : ""; String errorMsg = "컬럼 삭제 실패: " + rawMsg + guidance; logDdlOperation(userId, companyCode, "DROP_COLUMN", tableName, "FAILED: " + rawMsg, false, errorMsg); log.error("컬럼 삭제 실패: {}.{}, 사용자: {}, 오류: {}", tableName, columnName, userId, rawMsg, e); return Map.of("success", false, "message", errorMsg, "error_code", "EXECUTION_FAILED"); } } // ───────────────────────────────────────────────────────────────────────── // VALIDATE // ───────────────────────────────────────────────────────────────────────── public Map validateTableCreation(String tableName, List> columns) { List errors = new ArrayList<>(); List warnings = new ArrayList<>(); errors.addAll(validateTableName(tableName)); if (columns == null || columns.isEmpty()) { errors.add("최소 1개의 컬럼이 필요합니다."); } else { Set seen = new LinkedHashSet<>(); Set duplicates = new LinkedHashSet<>(); for (int i = 0; i < columns.size(); i++) { Map col = columns.get(i); errors.addAll(validateSingleColumn(col, i + 1)); String colName = col.get("name") != null ? col.get("name").toString().toLowerCase() : ""; if (!colName.isEmpty() && !seen.add(colName)) { duplicates.add(colName); } } if (!duplicates.isEmpty()) { errors.add("중복된 컬럼명이 있습니다: " + String.join(", ", duplicates)); } } int colCount = columns != null ? columns.size() : 0; return Map.of( "is_valid", errors.isEmpty(), "errors", errors, "warnings", warnings, "summary", buildValidationSummary(tableName, colCount, errors, warnings) ); } // ───────────────────────────────────────────────────────────────────────── // LOG QUERIES // ───────────────────────────────────────────────────────────────────────── public List> getDdlLogs(int limit, String userId, String ddlType) { Map params = new HashMap<>(); params.put("limit", Math.min(limit, 200)); params.put("user_id", userId); params.put("ddl_type", ddlType); return sqlSession.selectList(NS + "selectDdlLogs", params); } public Map getDdlStatistics(String fromDate, String toDate) { Map params = new HashMap<>(); params.put("from_date", fromDate); params.put("to_date", toDate); Map totalStats = sqlSession.selectOne(NS + "selectDdlTotalStats", params); List> byType = sqlSession.selectList(NS + "selectDdlStatsByType", params); List> byUser = sqlSession.selectList(NS + "selectDdlStatsByUser", params); List> recentFailures = sqlSession.selectList(NS + "selectRecentFailures", params); Map byDdlType = new LinkedHashMap<>(); for (Map row : byType) { byDdlType.put(String.valueOf(row.get("ddl_type")), toLong(row.get("count"))); } Map byUserMap = new LinkedHashMap<>(); for (Map row : byUser) { byUserMap.put(String.valueOf(row.get("user_id")), toLong(row.get("count"))); } return Map.of( "total_executions", toLong(totalStats != null ? totalStats.get("total_executions") : null), "successful_executions", toLong(totalStats != null ? totalStats.get("successful_executions") : null), "failed_executions", toLong(totalStats != null ? totalStats.get("failed_executions") : null), "by_ddl_type", byDdlType, "by_user", byUserMap, "recent_failures", recentFailures ); } public List> getTableDdlHistory(String tableName) { Map params = new HashMap<>(); params.put("table_name", tableName); return sqlSession.selectList(NS + "selectTableDdlHistory", params); } public Map getTableInfo(String tableName) { Map params = new HashMap<>(); params.put("table_name", tableName); Map tableInfo = sqlSession.selectOne(NS + "selectTableInfo", params); if (tableInfo == null) return null; List> columns = sqlSession.selectList(NS + "selectTableColumns", params); return Map.of("table_info", tableInfo, "columns", columns); } public int cleanupOldLogs(int retentionDays) { LocalDateTime cutoff = LocalDateTime.now().minusDays(retentionDays); Map params = new HashMap<>(); params.put("cutoff_date", cutoff.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); int deleted = sqlSession.delete(NS + "deleteOldDdlLogs", params); log.info("DDL 로그 정리 완료: {}개 삭제, 보존 기간: {}일", deleted, retentionDays); return deleted; } // ───────────────────────────────────────────────────────────────────────── // DDL QUERY GENERATION // ───────────────────────────────────────────────────────────────────────── private String generateCreateTableQuery(String tableName, List> columns) { StringBuilder sb = new StringBuilder(); sb.append("CREATE TABLE \"").append(sanitize(tableName)).append("\" (\n"); sb.append(" \"id\" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,\n"); sb.append(" \"created_date\" timestamp DEFAULT now(),\n"); sb.append(" \"updated_date\" timestamp DEFAULT now(),\n"); sb.append(" \"writer\" varchar(500) DEFAULT NULL,\n"); sb.append(" \"company_code\" varchar(500)"); for (Map col : columns) { sb.append(",\n \"").append(sanitize((String) col.get("name"))).append("\" varchar(500)"); Boolean nullable = col.containsKey("nullable") ? (Boolean) col.get("nullable") : true; if (Boolean.FALSE.equals(nullable)) sb.append(" NOT NULL"); if (col.get("default_value") != null && !col.get("default_value").toString().isBlank()) { sb.append(" DEFAULT '").append(col.get("default_value").toString().replace("'", "''")).append("'"); } } sb.append("\n);"); return sb.toString(); } private String generateAddColumnQuery(String tableName, Map column) { StringBuilder sb = new StringBuilder(); sb.append("ALTER TABLE \"").append(sanitize(tableName)).append("\" ADD COLUMN "); sb.append("\"").append(sanitize((String) column.get("name"))).append("\" varchar(500)"); Boolean nullable = column.containsKey("nullable") ? (Boolean) column.get("nullable") : true; if (Boolean.FALSE.equals(nullable)) sb.append(" NOT NULL"); if (column.get("default_value") != null && !column.get("default_value").toString().isBlank()) { sb.append(" DEFAULT '").append(column.get("default_value").toString().replace("'", "''")).append("'"); } sb.append(";"); return sb.toString(); } // ───────────────────────────────────────────────────────────────────────── // TABLE / COLUMN EXISTS // ───────────────────────────────────────────────────────────────────────── private boolean tableExists(String tableName) { Integer cnt = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM information_schema.tables " + "WHERE table_schema = 'public' AND table_name = ?", Integer.class, tableName); return cnt != null && cnt > 0; } private boolean columnExists(String tableName, String columnName) { Integer cnt = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM information_schema.columns " + "WHERE table_schema = 'public' AND table_name = ? AND column_name = ?", Integer.class, tableName, columnName); return cnt != null && cnt > 0; } // ───────────────────────────────────────────────────────────────────────── // METADATA SAVE // ───────────────────────────────────────────────────────────────────────── private void saveTableMetadata(String tableName, String description) { String desc = description != null && !description.isBlank() ? description : "사용자 생성 테이블: " + tableName; jdbcTemplate.update( "INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date) " + "VALUES (?, ?, ?, now(), now()) " + "ON CONFLICT (table_name) DO UPDATE SET table_label = ?, description = ?, updated_date = now()", tableName, tableName, desc, tableName, desc); } private void saveColumnMetadata(String tableName, List> columns, String companyCode) { // 기본 시스템 컬럼 List defaultCols = List.of( new Object[]{"id", "text", -5}, new Object[]{"created_date", "date", -4}, new Object[]{"updated_date", "date", -3}, new Object[]{"writer", "text", -2}, new Object[]{"company_code", "text", -1} ); for (Object[] dc : defaultCols) { saveColumnMeta(tableName, (String) dc[0], companyCode, (String) dc[1], "{}", (int) dc[2]); } // 사용자 정의 컬럼 for (int i = 0; i < columns.size(); i++) { Map col = columns.get(i); String inputType = convertToInputType(col); if (!InputTypeConstants.USER_SELECTABLE_INPUT_TYPES.contains(inputType)) { throw new IllegalArgumentException( "INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + InputTypeConstants.USER_SELECTABLE_INPUT_TYPES + " (받은 값: " + inputType + ")" ); } String detailSettings = col.containsKey("detail_settings") ? col.get("detail_settings").toString() : "{}"; saveColumnMeta(tableName, (String) col.get("name"), companyCode, inputType, detailSettings, i); } } private void saveColumnMeta(String tableName, String colName, String companyCode, String inputType, String detailSettings, int order) { jdbcTemplate.update( "INSERT INTO table_type_columns " + " (table_name, column_name, company_code, input_type, detail_settings, is_nullable, display_order, created_date, updated_date) " + "VALUES (?, ?, ?, ?, ?::jsonb, 'Y', ?, now(), now()) " + "ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET " + " input_type = ?, detail_settings = ?::jsonb, display_order = ?, updated_date = now()", tableName, colName, companyCode, inputType, detailSettings, order, inputType, detailSettings, order); } // ───────────────────────────────────────────────────────────────────────── // VALIDATION HELPERS // ───────────────────────────────────────────────────────────────────────── private List validateTableName(String tableName) { List errors = new ArrayList<>(); if (tableName == null || tableName.isBlank()) { errors.add("테이블명은 필수입니다."); return errors; } if (!tableName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) { errors.add("유효하지 않은 테이블명입니다. 영문자로 시작하고 영문자, 숫자, 언더스코어만 사용 가능합니다."); } if (tableName.length() > 63) errors.add("테이블명은 63자를 초과할 수 없습니다."); if (tableName.length() < 2) errors.add("테이블명은 최소 2자 이상이어야 합니다."); if (SYSTEM_TABLES.contains(tableName.toLowerCase())) { errors.add("'" + tableName + "'은 시스템 테이블명으로 사용할 수 없습니다."); } if (RESERVED_WORDS.contains(tableName.toLowerCase())) { errors.add("'" + tableName + "'은 SQL 예약어이므로 테이블명으로 사용할 수 없습니다."); } if (tableName.startsWith("_") || tableName.endsWith("_")) { errors.add("테이블명은 언더스코어로 시작하거나 끝날 수 없습니다."); } if (tableName.contains("__")) { errors.add("테이블명에 연속된 언더스코어는 사용할 수 없습니다."); } return errors; } private List validateSingleColumn(Map column, int position) { List errors = new ArrayList<>(); String name = column.get("name") != null ? column.get("name").toString().trim() : ""; String prefix = "컬럼 " + position + "(" + name + "): "; if (name.isBlank()) { errors.add(prefix + "컬럼명은 필수입니다."); return errors; } if (!name.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) { errors.add(prefix + "유효하지 않은 컬럼명입니다. 영문자로 시작하고 영문자, 숫자, 언더스코어만 사용 가능합니다."); } if (name.length() > 63) errors.add(prefix + "컬럼명은 63자를 초과할 수 없습니다."); if (name.length() < 2) errors.add(prefix + "컬럼명은 최소 2자 이상이어야 합니다."); if (RESERVED_COLUMNS.contains(name.toLowerCase())) { errors.add(prefix + "'" + name + "'은 예약된 컬럼명입니다 (id, created_date, updated_date, company_code)."); } if (RESERVED_WORDS.contains(name.toLowerCase())) { errors.add(prefix + "'" + name + "'은 SQL 예약어이므로 컬럼명으로 사용할 수 없습니다."); } if (name.contains("__")) { errors.add(prefix + "컬럼명에 연속된 언더스코어는 사용할 수 없습니다."); } return errors; } private List validateColumnForAddition(String tableName, Map column) { List errors = new ArrayList<>(); if (SYSTEM_TABLES.contains(tableName.toLowerCase())) { errors.add("'" + tableName + "'은 시스템 테이블이므로 컬럼을 추가할 수 없습니다."); } errors.addAll(validateSingleColumn(column, 1)); return errors; } // ───────────────────────────────────────────────────────────────────────── // UTILITIES // ───────────────────────────────────────────────────────────────────────── private String sanitize(String name) { return name.replaceAll("[^a-zA-Z0-9_]", ""); } private String convertToInputType(Map col) { Object inputType = col.get("input_type"); Object webType = col.get("web_type"); String type = inputType != null ? inputType.toString() : (webType != null ? webType.toString() : "text"); return switch (type) { case "number", "decimal" -> "number"; case "date", "datetime" -> "date"; case "select", "dropdown" -> "select"; case "checkbox", "boolean" -> "checkbox"; case "radio" -> "radio"; case "code" -> "code"; case "entity" -> "entity"; case "file" -> "file"; case "image" -> "image"; case "numbering" -> "numbering"; default -> "text"; }; } private void logDdlOperation(String userId, String companyCode, String ddlType, String tableName, String ddlQuery, boolean success, String errorMessage) { try { Map params = new HashMap<>(); params.put("user_id", userId); params.put("company_code", companyCode); params.put("ddl_type", ddlType); params.put("table_name", tableName); params.put("ddl_query", ddlQuery); params.put("success", success); params.put("error_message", errorMessage); sqlSession.insert(NS + "insertDdlLog", params); } catch (Exception e) { log.error("DDL 로그 기록 실패: userId={}, ddlType={}, tableName={}", userId, ddlType, tableName, e); } } private long toLong(Object value) { if (value == null) return 0L; if (value instanceof Number) return ((Number) value).longValue(); try { return Long.parseLong(value.toString()); } catch (NumberFormatException ex) { return 0L; } } private String buildValidationSummary(String tableName, int columnCount, List errors, List warnings) { StringBuilder sb = new StringBuilder("테이블 '").append(tableName).append("' 검증 완료. "); sb.append("컬럼 ").append(columnCount).append("개 중 "); sb.append(errors.isEmpty() ? "모든 검증 통과." : errors.size() + "개 오류 발견."); if (!warnings.isEmpty()) sb.append(" ").append(warnings.size()).append("개 경고 있음."); return sb.toString(); } }