Files
invyone/backend-spring/src/main/java/com/erp/service/DynamicFormService.java
T

786 lines
37 KiB
Java

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<String, Object> params = Map.of("table_name", tableName);
Map<String, Object> tableInfo = sqlSession.selectOne(NS + "selectTableType", params);
if (tableInfo == null || !"VIEW".equals(tableInfo.get("table_type"))) {
return tableName;
}
Map<String, Object> 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<String> getColumnNames(String tableName) {
Map<String, Object> params = Map.of("table_name", tableName);
List<Map<String, Object>> 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<String, String> getColumnTypeMap(String tableName) {
Map<String, Object> params = Map.of("table_name", tableName);
List<Map<String, Object>> rows = sqlSession.selectList(NS + "selectColumnTypes", params);
Map<String, String> result = new LinkedHashMap<>();
for (Map<String, Object> row : rows) {
result.put((String) row.get("column_name"), (String) row.get("data_type"));
}
return result;
}
/**
* 테이블 기본키 목록 조회
*/
private List<String> getPrimaryKeyList(String tableName) {
Map<String, Object> params = Map.of("table_name", tableName);
List<Map<String, Object>> 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<String, Object> saveFormData(int screenId, String tableNameInput,
Map<String, Object> data, String ipAddress) {
String tableName = resolveBaseTable(tableNameInput);
validateName(tableName);
List<String> tableColumns = getColumnNames(tableName);
List<String> primaryKeys = getPrimaryKeyList(tableName);
Map<String, String> 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<String, Object> 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<Map<String, Object>> separateRepeaters = new ArrayList<>();
List<Map<String, Object>> mergedRepeaters = new ArrayList<>();
List<String> keysToRemove = new ArrayList<>();
for (Map.Entry<String, Object> entry : dataToInsert.entrySet()) {
Object val = entry.getValue();
List<Object> parsedArray = null;
if (val instanceof List) {
parsedArray = (List<Object>) 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<Object> 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<String, Object> copy = new LinkedHashMap<>((Map<String, Object>) item);
copy.remove("_targetTable");
return (Object) copy;
}).collect(Collectors.toList());
}
}
keysToRemove.add(entry.getKey());
Map<String, Object> 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<Map<String, Object>> results = new ArrayList<>();
if (!mergedRepeaters.isEmpty()) {
// 병합 모드: 헤더 + 품목 각각 INSERT
for (Map<String, Object> repeater : mergedRepeaters) {
List<Object> items = (List<Object>) repeater.get("data");
for (Object item : items) {
Map<String, Object> itemMap = (Map<String, Object>) item;
Map<String, Object> mergedData = new LinkedHashMap<>(dataToInsert);
Map<String, Object> 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<String, Object> inserted = executeUpsert(tableName, mergedData, primaryKeys);
results.add(inserted);
}
}
} else {
Map<String, Object> inserted = executeUpsert(tableName, dataToInsert, primaryKeys);
results.add(inserted);
}
// 별도 테이블 Repeater 저장
for (Map<String, Object> repeater : separateRepeaters) {
String targetTable = (String) repeater.get("target_table");
validateName(targetTable);
List<String> targetCols = getColumnNames(targetTable);
List<String> targetPks = getPrimaryKeyList(targetTable);
Map<String, String> targetTypes = getColumnTypeMap(targetTable);
List<Object> items = (List<Object>) repeater.get("data");
for (Object item : items) {
Map<String, Object> itemData = new LinkedHashMap<>((Map<String, Object>) 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<String, Object> insertedRecord = results.isEmpty() ? new HashMap<>() : results.get(0);
Map<String, Object> 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<String, Object> executeUpsert(String tableName, Map<String, Object> data,
List<String> primaryKeys) {
if (data.isEmpty()) return new HashMap<>();
List<String> cols = new ArrayList<>(data.keySet());
List<Object> 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<String> 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<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, vals.toArray());
return rows.isEmpty() ? new HashMap<>() : rows.get(0);
}
// ── 폼 저장 (enhanced, 동일 로직) ─────────────────────────────────────────
@Transactional
public Map<String, Object> saveFormDataEnhanced(int screenId, String tableName,
Map<String, Object> data) {
return saveFormData(screenId, tableName, data, null);
}
// ── 폼 업데이트 ────────────────────────────────────────────────────────────
@Transactional
public Map<String, Object> updateFormData(String id, String tableNameInput,
Map<String, Object> data) {
String tableName = resolveBaseTable(tableNameInput);
validateName(tableName);
List<String> tableColumns = getColumnNames(tableName);
Map<String, String> columnTypes = getColumnTypeMap(tableName);
List<String> primaryKeys = getPrimaryKeyList(tableName);
if (primaryKeys.isEmpty()) {
throw new IllegalStateException("테이블 " + tableName + "의 기본키를 찾을 수 없습니다.");
}
Object updatedBy = data.get("updated_by");
Map<String, Object> 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<String> cols = new ArrayList<>(dataToUpdate.keySet());
List<Object> 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<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, vals.toArray());
Map<String, Object> updated = rows.isEmpty() ? new HashMap<>() : rows.get(0);
Map<String, Object> 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<String, Object> updateFormDataPartial(String id, String tableNameInput,
Map<String, Object> originalData,
Map<String, Object> newData) {
String tableName = resolveBaseTable(tableNameInput);
validateName(tableName);
List<String> tableColumns = getColumnNames(tableName);
Map<String, String> columnTypes = getColumnTypeMap(tableName);
List<String> primaryKeys = getPrimaryKeyList(tableName);
if (primaryKeys.isEmpty()) {
throw new IllegalStateException("테이블 " + tableName + "의 기본키를 찾을 수 없습니다.");
}
Map<String, Object> changedFields = new LinkedHashMap<>();
for (Map.Entry<String, Object> 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<String, Object> 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<String> cols = new ArrayList<>(changedFields.keySet());
List<Object> 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<String> 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<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, vals.toArray());
Map<String, Object> updated = rows.isEmpty() ? new HashMap<>() : rows.get(0);
Map<String, Object> 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<String, Object> 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<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, id);
if (rows.isEmpty()) {
throw new IllegalStateException("테이블 " + actualTable + "에서 ID '" + id + "'에 해당하는 레코드를 찾을 수 없습니다.");
}
}
// ── 단건 조회 ─────────────────────────────────────────────────────────────
public Map<String, Object> getFormData(int id) {
Map<String, Object> params = Map.of("id", id);
Map<String, Object> 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<Map<String, Object>>() {}));
} catch (Exception ignored) {}
}
Map<String, Object> 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<String, Object> getFormDataList(int screenId, Map<String, Object> 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<String, Object> 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<Map<String, Object>> rows = sqlSession.selectList(NS + "selectFormDataList", params);
Integer totalObj = sqlSession.selectOne(NS + "countFormDataList", params);
int total = totalObj != null ? totalObj : 0;
// form_data 파싱
List<Map<String, Object>> 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<Map<String, Object>>() {}));
} catch (Exception ignored) {}
}
Map<String, Object> 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<String, Object> 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<String, Object> validateFormData(String tableName, Map<String, Object> data) {
List<Map<String, Object>> errors = new ArrayList<>();
Map<String, Object> result = new LinkedHashMap<>();
result.put("valid", errors.isEmpty());
result.put("errors", errors);
return result;
}
// ── 테이블 컬럼 조회 ──────────────────────────────────────────────────────
public List<Map<String, Object>> getTableColumns(String tableName) {
Map<String, Object> params = Map.of("table_name", tableName);
List<Map<String, Object>> columns = sqlSession.selectList(NS + "selectTableColumns", params);
List<Map<String, Object>> pks = sqlSession.selectList(NS + "selectPrimaryKeys", params);
Set<String> pkSet = pks.stream().map(r -> (String) r.get("column_name")).collect(Collectors.toSet());
return columns.stream().map(col -> {
Map<String, Object> 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<String> getTablePrimaryKeys(String tableName) {
return getPrimaryKeyList(tableName);
}
// ── 단일 필드 업데이트 ────────────────────────────────────────────────────
@Transactional
public Map<String, Object> 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<String> tableColumns = getColumnNames(tableName);
boolean hasUpdatedBy = tableColumns.contains("updated_by");
boolean hasUpdatedAt = tableColumns.contains("updated_date");
boolean hasCompanyCode = tableColumns.contains("company_code");
List<Object> 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<String, Object> result = new LinkedHashMap<>();
result.put("affected_rows", affected);
return result;
}
// ── 위치 이력 ─────────────────────────────────────────────────────────────
@Transactional
public Map<String, Object> saveLocationHistory(Map<String, Object> data) {
Map<String, Object> 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<String, Object> result = new LinkedHashMap<>();
result.put("id", params.get("id"));
return result;
}
public List<Map<String, Object>> getLocationHistory(Map<String, Object> 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<String, Object> 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 "";
}
}