package com.erp.service; import com.erp.common.BaseService; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.regex.Pattern; import java.util.stream.Collectors; @Service @RequiredArgsConstructor @Slf4j public class DynamicFormService extends BaseService { private static final String NS = "dynamicForm."; private final JdbcTemplate jdbcTemplate; private final ObjectMapper objectMapper; private static final Pattern VALID_NAME = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$"); // ── 내부 유틸 ────────────────────────────────────────────────────────────── private void validateName(String name) { if (name == null || !VALID_NAME.matcher(name).matches()) { throw new IllegalArgumentException("유효하지 않은 테이블명 또는 컬럼명: " + name); } } /** * VIEW인 경우 원본 기본 테이블을 반환 */ public String resolveBaseTable(String tableName) { try { Map params = Map.of("table_name", tableName); Map tableInfo = sqlSession.selectOne(NS + "selectTableType", params); if (tableInfo == null || !"VIEW".equals(tableInfo.get("table_type"))) { return tableName; } Map viewDef = sqlSession.selectOne(NS + "selectViewDefinition", params); if (viewDef != null) { String definition = (String) viewDef.get("view_definition"); if (definition != null) { java.util.regex.Matcher m = Pattern.compile("FROM\\s+\\(?(?:public\\.)?([a-zA-Z_][a-zA-Z0-9_]*)\\s", java.util.regex.Pattern.CASE_INSENSITIVE).matcher(definition); if (m.find()) { String baseTable = m.group(1); log.info("VIEW {} → 원본 테이블 {}", tableName, baseTable); return baseTable; } } } } catch (Exception e) { log.warn("VIEW 원본 테이블 조회 실패: {}", e.getMessage()); } return tableName; } /** * 테이블 컬럼명 목록 조회 */ private List getColumnNames(String tableName) { Map params = Map.of("table_name", tableName); List> rows = sqlSession.selectList(NS + "selectColumnNames", params); return rows.stream() .map(r -> (String) r.get("column_name")) .collect(Collectors.toList()); } /** * 테이블 컬럼 타입 맵 (column_name → data_type) */ private Map getColumnTypeMap(String tableName) { Map params = Map.of("table_name", tableName); List> rows = sqlSession.selectList(NS + "selectColumnTypes", params); Map result = new LinkedHashMap<>(); for (Map row : rows) { result.put((String) row.get("column_name"), (String) row.get("data_type")); } return result; } /** * 테이블 기본키 목록 조회 */ private List getPrimaryKeyList(String tableName) { Map params = Map.of("table_name", tableName); List> rows = sqlSession.selectList(NS + "selectPrimaryKeys", params); return rows.stream() .map(r -> (String) r.get("column_name")) .collect(Collectors.toList()); } /** * 값을 PostgreSQL 타입에 맞게 변환 */ private Object convertValue(Object value, String dataType) { if (value == null || "".equals(value)) return null; String dt = dataType.toLowerCase(); if (dt.contains("integer") || dt.contains("bigint") || dt.contains("serial")) { try { return Long.parseLong(value.toString()); } catch (NumberFormatException e) { return null; } } if (dt.contains("numeric") || dt.contains("decimal") || dt.contains("real") || dt.contains("double")) { try { return Double.parseDouble(value.toString()); } catch (NumberFormatException e) { return null; } } if (dt.contains("boolean")) { if (value instanceof Boolean) return value; String s = value.toString().toLowerCase(); return "true".equals(s) || "1".equals(s); } if (dt.contains("date") || dt.contains("timestamp") || dt.contains("time")) { if (value instanceof java.util.Date) return value; String s = value.toString().trim(); if (s.isEmpty()) return null; try { if (s.matches("\\d{4}-\\d{2}-\\d{2}")) { if ("date".equals(dt)) return s; return LocalDateTime.of(LocalDate.parse(s), java.time.LocalTime.MIDNIGHT); } if (s.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}")) { return LocalDateTime.parse(s, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); } return LocalDateTime.parse(s); } catch (Exception e) { return null; } } return value; } /** * company_code UUID 정규화 (36자 → 32자) */ private String normalizeCompanyCode(String code) { if (code == null) return null; if (code.length() == 36 && code.contains("-")) { code = code.replace("-", ""); } if (code.length() > 32) { code = code.substring(0, 32); } return code; } // ── 폼 저장 (기본) ───────────────────────────────────────────────────────── /** * 폼 데이터 저장 (실제 테이블에 UPSERT) */ @Transactional public Map saveFormData(int screenId, String tableNameInput, Map data, String ipAddress) { String tableName = resolveBaseTable(tableNameInput); validateName(tableName); List tableColumns = getColumnNames(tableName); List primaryKeys = getPrimaryKeyList(tableName); Map columnTypes = getColumnTypeMap(tableName); // 메타 필드 분리 Object createdBy = data.get("created_by"); Object updatedBy = data.get("updated_by"); Object writer = data.get("writer"); Object companyCodeRaw = data.get("company_code"); Map dataToInsert = new LinkedHashMap<>(data); dataToInsert.remove("created_by"); dataToInsert.remove("updated_by"); dataToInsert.remove("writer"); dataToInsert.remove("company_code"); dataToInsert.remove("screen_id"); // 공통 타임스탬프 if (tableColumns.contains("created_date")) dataToInsert.put("created_date", new java.util.Date()); if (tableColumns.contains("updated_date")) dataToInsert.put("updated_date", new java.util.Date()); if (tableColumns.contains("created_date")) dataToInsert.put("created_date", new java.util.Date()); if (tableColumns.contains("updated_date") && !dataToInsert.containsKey("updated_date")) dataToInsert.put("updated_date", new java.util.Date()); if (tableColumns.contains("created_date") && !dataToInsert.containsKey("created_date")) dataToInsert.put("created_date", new java.util.Date()); // 작성자 정보 if (writer != null && tableColumns.contains("writer")) dataToInsert.put("writer", writer); if (createdBy != null && tableColumns.contains("created_by")) dataToInsert.put("created_by", createdBy); if (updatedBy != null && tableColumns.contains("updated_by")) dataToInsert.put("updated_by", updatedBy); if (companyCodeRaw != null && tableColumns.contains("company_code")) { dataToInsert.put("company_code", normalizeCompanyCode(companyCodeRaw.toString())); } // Repeater 데이터 분리 (배열 타입) List> separateRepeaters = new ArrayList<>(); List> mergedRepeaters = new ArrayList<>(); List keysToRemove = new ArrayList<>(); for (Map.Entry entry : dataToInsert.entrySet()) { Object val = entry.getValue(); List parsedArray = null; if (val instanceof List) { parsedArray = (List) val; } else if (val instanceof String) { String s = ((String) val).trim(); if (s.startsWith("[") && s.endsWith("]")) { try { parsedArray = objectMapper.readValue(s, new TypeReference<>() {}); } catch (Exception ignored) {} } } if (parsedArray != null && !parsedArray.isEmpty()) { String targetTable = null; List actualItems = parsedArray; if (!parsedArray.isEmpty() && parsedArray.get(0) instanceof Map) { Map firstItem = (Map) parsedArray.get(0); if (firstItem.containsKey("_targetTable")) { targetTable = (String) firstItem.get("_targetTable"); actualItems = parsedArray.stream() .map(item -> { Map copy = new LinkedHashMap<>((Map) item); copy.remove("_targetTable"); return (Object) copy; }).collect(Collectors.toList()); } } keysToRemove.add(entry.getKey()); Map repeaterMeta = new LinkedHashMap<>(); repeaterMeta.put("component_id", entry.getKey()); repeaterMeta.put("target_table", targetTable); repeaterMeta.put("data", actualItems); if (targetTable != null && !targetTable.equals(tableName)) { separateRepeaters.add(repeaterMeta); } else { mergedRepeaters.add(repeaterMeta); } } } keysToRemove.forEach(dataToInsert::remove); // 존재하지 않는 컬럼 제거 dataToInsert.keySet().removeIf(key -> !tableColumns.contains(key)); // 타입 변환 for (String col : new ArrayList<>(dataToInsert.keySet())) { String dt = columnTypes.get(col); if (dt != null) { dataToInsert.put(col, convertValue(dataToInsert.get(col), dt)); } } List> results = new ArrayList<>(); if (!mergedRepeaters.isEmpty()) { // 병합 모드: 헤더 + 품목 각각 INSERT for (Map repeater : mergedRepeaters) { List items = (List) repeater.get("data"); for (Object item : items) { Map itemMap = (Map) item; Map mergedData = new LinkedHashMap<>(dataToInsert); Map itemCopy = new LinkedHashMap<>(itemMap); itemCopy.remove("created_date"); itemCopy.remove("_isNewItem"); boolean isExisting = Boolean.TRUE.equals(itemCopy.remove("_existingRecord")); if (!isExisting) itemCopy.remove("id"); // 실제 컬럼만 필터링 + 타입 변환 itemCopy.keySet().removeIf(k -> !tableColumns.contains(k)); for (String col : new ArrayList<>(itemCopy.keySet())) { String dt = columnTypes.get(col); if (dt != null) itemCopy.put(col, convertValue(itemCopy.get(col), dt)); } mergedData.putAll(itemCopy); Map inserted = executeUpsert(tableName, mergedData, primaryKeys); results.add(inserted); } } } else { Map inserted = executeUpsert(tableName, dataToInsert, primaryKeys); results.add(inserted); } // 별도 테이블 Repeater 저장 for (Map repeater : separateRepeaters) { String targetTable = (String) repeater.get("target_table"); validateName(targetTable); List targetCols = getColumnNames(targetTable); List targetPks = getPrimaryKeyList(targetTable); Map targetTypes = getColumnTypeMap(targetTable); List items = (List) repeater.get("data"); for (Object item : items) { Map itemData = new LinkedHashMap<>((Map) item); if (createdBy != null && targetCols.contains("created_by")) itemData.put("created_by", createdBy); if (updatedBy != null && targetCols.contains("updated_by")) itemData.put("updated_by", updatedBy); if (companyCodeRaw != null && targetCols.contains("company_code")) { itemData.put("company_code", normalizeCompanyCode(companyCodeRaw.toString())); } if (targetCols.contains("created_date") && !itemData.containsKey("created_date")) itemData.put("created_date", new java.util.Date()); itemData.keySet().removeIf(k -> !targetCols.contains(k)); for (String col : new ArrayList<>(itemData.keySet())) { String dt = targetTypes.get(col); if (dt != null) itemData.put(col, convertValue(itemData.get(col), dt)); } executeUpsert(targetTable, itemData, targetPks); } } Map insertedRecord = results.isEmpty() ? new HashMap<>() : results.get(0); Map result = new LinkedHashMap<>(); result.put("id", insertedRecord.getOrDefault("id", insertedRecord.get("objid"))); result.put("screen_id", screenId); result.put("table_name", tableName); result.put("data", insertedRecord); result.put("created_at", insertedRecord.get("created_date")); result.put("updated_at", insertedRecord.get("updated_date")); result.put("created_by", insertedRecord.getOrDefault("created_by", createdBy)); result.put("updated_by", insertedRecord.getOrDefault("updated_by", updatedBy)); return result; } /** * UPSERT 실행 (JdbcTemplate) */ private Map executeUpsert(String tableName, Map data, List primaryKeys) { if (data.isEmpty()) return new HashMap<>(); List cols = new ArrayList<>(data.keySet()); List vals = cols.stream().map(data::get).collect(Collectors.toList()); String placeholders = cols.stream().map(c -> "?").collect(Collectors.joining(", ")); String colList = cols.stream().map(c -> "\"" + c + "\"").collect(Collectors.joining(", ")); String sql; if (!primaryKeys.isEmpty()) { String conflict = primaryKeys.stream().collect(Collectors.joining(", ")); List updateCols = cols.stream().filter(c -> !primaryKeys.contains(c)).collect(Collectors.toList()); String updateSet = updateCols.stream().map(c -> "\"" + c + "\" = EXCLUDED.\"" + c + "\"").collect(Collectors.joining(", ")); if (!updateSet.isEmpty()) { sql = String.format("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO UPDATE SET %s RETURNING *", tableName, colList, placeholders, conflict, updateSet); } else { sql = String.format("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO NOTHING RETURNING *", tableName, colList, placeholders, conflict); } } else { sql = String.format("INSERT INTO %s (%s) VALUES (%s) RETURNING *", tableName, colList, placeholders); } log.debug("UPSERT SQL: {}", sql); List> rows = jdbcTemplate.queryForList(sql, vals.toArray()); return rows.isEmpty() ? new HashMap<>() : rows.get(0); } // ── 폼 저장 (enhanced, 동일 로직) ───────────────────────────────────────── @Transactional public Map saveFormDataEnhanced(int screenId, String tableName, Map data) { return saveFormData(screenId, tableName, data, null); } // ── 폼 업데이트 ──────────────────────────────────────────────────────────── @Transactional public Map updateFormData(String id, String tableNameInput, Map data) { String tableName = resolveBaseTable(tableNameInput); validateName(tableName); List tableColumns = getColumnNames(tableName); Map columnTypes = getColumnTypeMap(tableName); List primaryKeys = getPrimaryKeyList(tableName); if (primaryKeys.isEmpty()) { throw new IllegalStateException("테이블 " + tableName + "의 기본키를 찾을 수 없습니다."); } Object updatedBy = data.get("updated_by"); Map dataToUpdate = new LinkedHashMap<>(data); dataToUpdate.remove("created_by"); dataToUpdate.remove("company_code"); dataToUpdate.remove("screen_id"); if (tableColumns.contains("updated_date")) dataToUpdate.put("updated_date", new java.util.Date()); if (tableColumns.contains("updated_date")) dataToUpdate.put("updated_date", new java.util.Date()); if (tableColumns.contains("created_date") && !dataToUpdate.containsKey("created_date")) dataToUpdate.put("created_date", new java.util.Date()); if (updatedBy != null && tableColumns.contains("updated_by")) dataToUpdate.put("updated_by", updatedBy); dataToUpdate.keySet().removeIf(key -> !tableColumns.contains(key)); // 타입 변환 for (String col : new ArrayList<>(dataToUpdate.keySet())) { String dt = columnTypes.get(col); if (dt != null) dataToUpdate.put(col, convertValue(dataToUpdate.get(col), dt)); } String primaryKeyColumn = primaryKeys.get(0); String pkType = columnTypes.getOrDefault(primaryKeyColumn, "text"); String pkCast = buildPkCast(pkType); List cols = new ArrayList<>(dataToUpdate.keySet()); List vals = cols.stream().map(dataToUpdate::get).collect(Collectors.toList()); String setClause = cols.stream() .map(c -> "\"" + c + "\" = ?") .collect(Collectors.joining(", ")); vals.add(id); String sql = String.format("UPDATE %s SET %s WHERE %s = ?%s RETURNING *", tableName, setClause, primaryKeyColumn, pkCast); log.debug("UPDATE SQL: {}", sql); List> rows = jdbcTemplate.queryForList(sql, vals.toArray()); Map updated = rows.isEmpty() ? new HashMap<>() : rows.get(0); Map result = new LinkedHashMap<>(); result.put("id", updated.getOrDefault("id", updated.getOrDefault("objid", id))); result.put("screen_id", 0); result.put("table_name", tableName); result.put("data", updated); result.put("created_at", updated.get("created_date")); result.put("updated_at", updated.get("updated_date")); result.put("created_by", updated.get("created_by")); result.put("updated_by", updated.getOrDefault("updated_by", updatedBy)); return result; } // ── 부분 업데이트 ────────────────────────────────────────────────────────── @Transactional public Map updateFormDataPartial(String id, String tableNameInput, Map originalData, Map newData) { String tableName = resolveBaseTable(tableNameInput); validateName(tableName); List tableColumns = getColumnNames(tableName); Map columnTypes = getColumnTypeMap(tableName); List primaryKeys = getPrimaryKeyList(tableName); if (primaryKeys.isEmpty()) { throw new IllegalStateException("테이블 " + tableName + "의 기본키를 찾을 수 없습니다."); } Map changedFields = new LinkedHashMap<>(); for (Map.Entry entry : newData.entrySet()) { String key = entry.getKey(); if (List.of("created_by", "updated_by", "company_code", "screen_id").contains(key)) continue; if (!tableColumns.contains(key)) continue; if (!Objects.equals(originalData.get(key), entry.getValue())) { changedFields.put(key, entry.getValue()); } } if (changedFields.isEmpty()) { Map noChange = new LinkedHashMap<>(); noChange.put("success", true); noChange.put("data", originalData); noChange.put("message", "변경사항이 없어 업데이트하지 않았습니다."); return noChange; } if (tableColumns.contains("updated_date")) changedFields.put("updated_date", new java.util.Date()); if (tableColumns.contains("updated_date")) changedFields.put("updated_date", new java.util.Date()); String primaryKeyColumn = primaryKeys.get(0); String pkType = columnTypes.getOrDefault(primaryKeyColumn, "text"); String pkCast = buildPkCast(pkType); List cols = new ArrayList<>(changedFields.keySet()); List vals = cols.stream().map(c -> { Object v = changedFields.get(c); String dt = columnTypes.getOrDefault(c, "text"); // 빈 값 → null if (v == null || "".equals(v)) return null; // jsonb: 객체/배열은 JSON 문자열로 if ((dt.equals("jsonb") || dt.equals("json")) && (v instanceof Map || v instanceof List)) { try { return objectMapper.writeValueAsString(v); } catch (Exception e) { return v; } } return v; }).collect(Collectors.toList()); // SET 절 with type cast List setClauses = new ArrayList<>(); for (int i = 0; i < cols.size(); i++) { String col = cols.get(i); String dt = columnTypes.getOrDefault(col, "text"); String cast = buildCast(dt); setClauses.add("\"" + col + "\" = ?" + cast); } vals.add(id); String sql = String.format("UPDATE %s SET %s WHERE %s = ?%s RETURNING *", tableName, String.join(", ", setClauses), primaryKeyColumn, pkCast); log.debug("PARTIAL UPDATE SQL: {}", sql); List> rows = jdbcTemplate.queryForList(sql, vals.toArray()); Map updated = rows.isEmpty() ? new HashMap<>() : rows.get(0); Map result = new LinkedHashMap<>(); result.put("success", true); result.put("data", updated); result.put("message", "데이터가 성공적으로 업데이트되었습니다."); return result; } // ── 폼 삭제 ─────────────────────────────────────────────────────────────── @Transactional public void deleteFormData(String id, String tableName, String companyCode, String userId, Integer screenId) { String actualTable = resolveBaseTable(tableName); validateName(actualTable); Map pkInfo = sqlSession.selectOne(NS + "selectPrimaryKeyWithType", Map.of("table_name", actualTable)); if (pkInfo == null) { throw new IllegalStateException("테이블 " + actualTable + "의 기본키를 찾을 수 없습니다."); } String pkColumn = (String) pkInfo.get("column_name"); String pkDataType = (String) pkInfo.get("data_type"); String pkCast = buildPkCast(pkDataType); String sql = String.format("DELETE FROM %s WHERE %s = ?%s RETURNING *", actualTable, pkColumn, pkCast); log.debug("DELETE SQL: {}", sql); List> rows = jdbcTemplate.queryForList(sql, id); if (rows.isEmpty()) { throw new IllegalStateException("테이블 " + actualTable + "에서 ID '" + id + "'에 해당하는 레코드를 찾을 수 없습니다."); } } // ── 단건 조회 ───────────────────────────────────────────────────────────── public Map getFormData(int id) { Map params = Map.of("id", id); Map row = sqlSession.selectOne(NS + "selectFormData", params); if (row == null) return null; // form_data jsonb를 Map으로 파싱 Object formDataRaw = row.get("form_data"); if (formDataRaw instanceof String) { try { row.put("form_data", objectMapper.readValue((String) formDataRaw, new TypeReference>() {})); } catch (Exception ignored) {} } Map result = new LinkedHashMap<>(); result.put("id", row.get("id")); result.put("screen_id", row.get("screen_id")); result.put("table_name", row.get("table_name")); result.put("data", row.get("form_data")); result.put("created_at", row.get("created_date")); result.put("updated_at", row.get("updated_date")); result.put("created_by", row.get("created_by")); result.put("updated_by", row.get("updated_by")); return result; } // ── 목록 조회 (페이징) ──────────────────────────────────────────────────── public Map getFormDataList(int screenId, Map queryParams) { int page = ((Number) queryParams.getOrDefault("page", 1)).intValue(); int size = ((Number) queryParams.getOrDefault("size", 10)).intValue(); String search = (String) queryParams.get("search"); String sortBy = (String) queryParams.getOrDefault("sort_by", "created_date"); String sortOrder = (String) queryParams.getOrDefault("sort_order", "desc"); int offset = (page - 1) * size; String searchParam = (search != null && !search.isEmpty()) ? "%" + search + "%" : null; Map params = new LinkedHashMap<>(); params.put("screen_id", screenId); params.put("search", searchParam); params.put("sort_by", sortBy); params.put("sort_order", sortOrder); params.put("size", size); params.put("offset", offset); List> rows = sqlSession.selectList(NS + "selectFormDataList", params); Integer totalObj = sqlSession.selectOne(NS + "countFormDataList", params); int total = totalObj != null ? totalObj : 0; // form_data 파싱 List> content = rows.stream().map(row -> { Object raw = row.get("form_data"); if (raw instanceof String) { try { row.put("form_data", objectMapper.readValue((String) raw, new TypeReference>() {})); } catch (Exception ignored) {} } Map item = new LinkedHashMap<>(); item.put("id", row.get("id")); item.put("screen_id", row.get("screen_id")); item.put("table_name", row.get("table_name")); item.put("data", row.get("form_data")); item.put("created_at", row.get("created_date")); item.put("updated_at", row.get("updated_date")); item.put("created_by", row.get("created_by")); item.put("updated_by", row.get("updated_by")); return item; }).collect(Collectors.toList()); int totalPages = (int) Math.ceil((double) total / size); Map result = new LinkedHashMap<>(); result.put("content", content); result.put("total_elements", total); result.put("total_pages", totalPages); result.put("current_page", page); result.put("size", size); return result; } // ── 검증 ────────────────────────────────────────────────────────────────── public Map validateFormData(String tableName, Map data) { List> errors = new ArrayList<>(); Map result = new LinkedHashMap<>(); result.put("valid", errors.isEmpty()); result.put("errors", errors); return result; } // ── 테이블 컬럼 조회 ────────────────────────────────────────────────────── public List> getTableColumns(String tableName) { Map params = Map.of("table_name", tableName); List> columns = sqlSession.selectList(NS + "selectTableColumns", params); List> pks = sqlSession.selectList(NS + "selectPrimaryKeys", params); Set pkSet = pks.stream().map(r -> (String) r.get("column_name")).collect(Collectors.toSet()); return columns.stream().map(col -> { Map item = new LinkedHashMap<>(col); item.put("primary_key", pkSet.contains(col.get("column_name"))); item.put("nullable", "YES".equals(col.get("is_nullable"))); return item; }).collect(Collectors.toList()); } // ── 기본키 조회 ─────────────────────────────────────────────────────────── public List getTablePrimaryKeys(String tableName) { return getPrimaryKeyList(tableName); } // ── 단일 필드 업데이트 ──────────────────────────────────────────────────── @Transactional public Map updateFieldValue(String tableName, String keyField, Object keyValue, String updateField, Object updateValue, String companyCode, String userId) { validateName(tableName); validateName(keyField); validateName(updateField); // 컬럼 존재 여부 확인 (updated_by, updated_at, company_code) List tableColumns = getColumnNames(tableName); boolean hasUpdatedBy = tableColumns.contains("updated_by"); boolean hasUpdatedAt = tableColumns.contains("updated_date"); boolean hasCompanyCode = tableColumns.contains("company_code"); List params = new ArrayList<>(); StringBuilder setClause = new StringBuilder("\"" + updateField + "\" = ?"); params.add(updateValue); if (hasUpdatedBy) { setClause.append(", updated_by = ?"); params.add(userId); } if (hasUpdatedAt) { setClause.append(", updated_at = NOW()"); } StringBuilder whereClause = new StringBuilder("\"" + keyField + "\" = ?"); params.add(keyValue); if (hasCompanyCode && companyCode != null && !"*".equals(companyCode)) { whereClause.append(" AND company_code = ?"); params.add(companyCode); } String sql = String.format("UPDATE \"%s\" SET %s WHERE %s", tableName, setClause, whereClause); log.debug("UPDATE FIELD SQL: {}", sql); int affected = jdbcTemplate.update(sql, params.toArray()); Map result = new LinkedHashMap<>(); result.put("affected_rows", affected); return result; } // ── 위치 이력 ───────────────────────────────────────────────────────────── @Transactional public Map saveLocationHistory(Map data) { Map params = new HashMap<>(data); // recordedAt String → Date Object recAt = params.get("recorded_at"); if (recAt instanceof String) { try { params.put("recorded_at", new java.util.Date( java.time.Instant.parse((String) recAt).toEpochMilli())); } catch (Exception e) { params.put("recorded_at", new java.util.Date()); } } else if (recAt == null) { params.put("recorded_at", new java.util.Date()); } if (!params.containsKey("trip_status")) params.put("trip_status", "active"); sqlSession.insert(NS + "insertLocationHistory", params); Map result = new LinkedHashMap<>(); result.put("id", params.get("id")); return result; } public List> getLocationHistory(Map params) { if (!params.containsKey("limit")) params.put("limit", 1000); // startDate / endDate String → Date if present convertDateParam(params, "start_date"); convertDateParam(params, "end_date"); return sqlSession.selectList(NS + "selectLocationHistory", params); } private void convertDateParam(Map params, String key) { Object val = params.get(key); if (val instanceof String && !((String) val).isEmpty()) { try { params.put(key, new java.util.Date(java.time.Instant.parse((String) val).toEpochMilli())); } catch (Exception e) { try { params.put(key, java.sql.Date.valueOf((String) val)); } catch (Exception ignored) {} } } } // ── PK cast 유틸 ───────────────────────────────────────────────────────── private String buildPkCast(String dataType) { if (dataType == null) return ""; String dt = dataType.toLowerCase(); if (dt.contains("character") || dt.contains("text") || dt.contains("varchar")) return "::text"; if (dt.contains("bigint")) return "::bigint"; if (dt.contains("integer") || dt.contains("numeric")) return "::numeric"; if (dt.contains("uuid")) return "::uuid"; return ""; } private String buildCast(String dataType) { if (dataType == null) return ""; String dt = dataType.toLowerCase(); if (dt.equals("integer") || dt.equals("smallint")) return "::integer"; if (dt.equals("bigint")) return "::bigint"; if (dt.contains("numeric") || dt.contains("decimal") || dt.contains("real") || dt.contains("double")) return "::numeric"; if (dt.equals("boolean")) return "::boolean"; if (dt.equals("jsonb") || dt.equals("json")) return "::jsonb"; return ""; } }