786 lines
37 KiB
Java
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 "";
|
|
}
|
|
}
|