Enable flexible date parsing, add field edit restrictions, and hide controllers.

Enhances date/time field parsing with support for multiple formats across `InstantFieldType`, `LocalDateTimeFieldType`, `OffsetDateTimeFieldType`, and others. Introduces `DisableEditField` annotation to restrict editing of specific fields and updates templates, controllers, and schemas accordingly. Hides controllers from swagger documentation and adds error handling for unexpected scenarios.
This commit is contained in:
dalbodeule 2025-05-20 15:51:33 +09:00
parent 2e3e11aafb
commit 28063ed583
No known key found for this signature in database
GPG Key ID: EFA860D069C9FA65
17 changed files with 353 additions and 54 deletions

11
.idea/snap-admin.iml generated Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Chameleon" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/build/resources/main/templates" />
</list>
</option>
</component>
</module>

View File

@ -49,6 +49,8 @@ dependencies {
api(libs.org.springframework.boot.spring.boot.starter.validation) api(libs.org.springframework.boot.spring.boot.starter.validation)
api(libs.org.springframework.boot.spring.boot.starter.web) api(libs.org.springframework.boot.spring.boot.starter.web)
api(libs.org.springframework.boot.spring.boot.configuration.processor) api(libs.org.springframework.boot.spring.boot.configuration.processor)
api("io.swagger.core.v3:swagger-annotations:2.2.15")
testImplementation(libs.org.springframework.boot.spring.boot.starter.test) testImplementation(libs.org.springframework.boot.spring.boot.starter.test)
} }

View File

@ -51,6 +51,7 @@ import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne; import jakarta.persistence.OneToOne;
import space.mori.dalbodeule.snapadmin.external.annotations.Disable; import space.mori.dalbodeule.snapadmin.external.annotations.Disable;
import space.mori.dalbodeule.snapadmin.external.annotations.DisableEditField;
import space.mori.dalbodeule.snapadmin.external.annotations.DisplayFormat; import space.mori.dalbodeule.snapadmin.external.annotations.DisplayFormat;
import space.mori.dalbodeule.snapadmin.external.dbmapping.CustomJpaRepository; import space.mori.dalbodeule.snapadmin.external.dbmapping.CustomJpaRepository;
import space.mori.dalbodeule.snapadmin.external.dbmapping.DbObjectSchema; import space.mori.dalbodeule.snapadmin.external.dbmapping.DbObjectSchema;
@ -219,6 +220,7 @@ public class SnapAdmin {
Field[] fields = klass.getDeclaredFields(); Field[] fields = klass.getDeclaredFields();
for (Field f : fields) { for (Field f : fields) {
try { try {
if(f.getName().contains("hibernate")) continue;
DbField field = mapField(f, schema); DbField field = mapField(f, schema);
field.setSchema(schema); field.setSchema(schema);
schema.addField(field); schema.addField(field);
@ -352,8 +354,9 @@ public class SnapAdmin {
} }
DisplayFormat displayFormat = f.getAnnotation(DisplayFormat.class); DisplayFormat displayFormat = f.getAnnotation(DisplayFormat.class);
DisableEditField disableEdit = f.getAnnotation(DisableEditField.class);
DbField field = new DbField(f.getName(), fieldName, f, fieldType, schema, displayFormat != null ? displayFormat.format() : null); DbField field = new DbField(f.getName(), fieldName, f, fieldType, schema, displayFormat != null ? displayFormat.format() : null, disableEdit != null);
field.setConnectedType(connectedType); field.setConnectedType(connectedType);
Id[] idAnnotations = f.getAnnotationsByType(Id.class); Id[] idAnnotations = f.getAnnotationsByType(Id.class);

View File

@ -28,6 +28,6 @@ import java.lang.annotation.Target;
* Disables edit actions on the Entity class. * Disables edit actions on the Entity class.
*/ */
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) @Target(ElementType.FIELD)
public @interface DisableEdit { public @interface DisableEditField {
} }

View File

@ -0,0 +1,11 @@
package space.mori.dalbodeule.snapadmin.external.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface HiddenEditForm {
}

View File

@ -29,6 +29,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import io.swagger.v3.oas.annotations.Hidden;
import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.CSVPrinter;
import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Cell;
@ -75,6 +76,7 @@ import space.mori.dalbodeule.snapadmin.internal.repository.ConsoleQueryRepositor
@Controller @Controller
@RequestMapping(value = { "/${snapadmin.baseUrl}/", "/${snapadmin.baseUrl}" }) @RequestMapping(value = { "/${snapadmin.baseUrl}/", "/${snapadmin.baseUrl}" })
@Import(ObjectMapper.class) @Import(ObjectMapper.class)
@Hidden
public class DataExportController { public class DataExportController {
private static final Logger logger = LoggerFactory.getLogger(DataExportFormat.class); private static final Logger logger = LoggerFactory.getLogger(DataExportFormat.class);
private final SnapAdmin snapAdmin; private final SnapAdmin snapAdmin;

View File

@ -21,6 +21,7 @@ package space.mori.dalbodeule.snapadmin.external.controller;
import java.util.Optional; import java.util.Optional;
import io.swagger.v3.oas.annotations.Hidden;
import org.apache.tika.Tika; import org.apache.tika.Tika;
import org.apache.tika.mime.MimeTypeException; import org.apache.tika.mime.MimeTypeException;
import org.apache.tika.mime.MimeTypes; import org.apache.tika.mime.MimeTypes;
@ -48,6 +49,7 @@ import space.mori.dalbodeule.snapadmin.external.exceptions.SnapAdminException;
*/ */
@Controller @Controller
@RequestMapping(value = {"/${snapadmin.baseUrl}/download", "/${snapadmin.baseUrl}/download/"}) @RequestMapping(value = {"/${snapadmin.baseUrl}/download", "/${snapadmin.baseUrl}/download/"})
@Hidden
public class FileDownloadController { public class FileDownloadController {
@Autowired @Autowired
private SnapAdminRepository repository; private SnapAdminRepository repository;

View File

@ -32,6 +32,7 @@ import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import io.swagger.v3.oas.annotations.Hidden;
import org.hibernate.id.IdentifierGenerationException; import org.hibernate.id.IdentifierGenerationException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -86,6 +87,7 @@ import space.mori.dalbodeule.snapadmin.internal.service.UserSettingsService;
*/ */
@Controller @Controller
@RequestMapping(value= {"/${snapadmin.baseUrl}", "/${snapadmin.baseUrl}/"}) @RequestMapping(value= {"/${snapadmin.baseUrl}", "/${snapadmin.baseUrl}/"})
@Hidden
public class SnapAdminController { public class SnapAdminController {
private static final Logger logger = LoggerFactory.getLogger(SnapAdminController.class); private static final Logger logger = LoggerFactory.getLogger(SnapAdminController.class);
@ -527,9 +529,14 @@ public class SnapAdminController {
} else { } else {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} catch (Exception e) {
// 추가: 일반적인 예외 처리
logger.error("Unexpected error during data submission: ", e);
attr.addFlashAttribute("errorTitle", "System Error");
attr.addFlashAttribute("error", e.getMessage());
attr.addFlashAttribute("params", params);
} }
if (attr.getFlashAttributes().containsKey("error")) { if (attr.getFlashAttributes().containsKey("error")) {
if (create) if (create)
return "redirect:/" + properties.getBaseUrl() + "/model/" + schema.getClassName() + "/create"; return "redirect:/" + properties.getBaseUrl() + "/model/" + schema.getClassName() + "/create";

View File

@ -22,6 +22,7 @@ package space.mori.dalbodeule.snapadmin.external.controller.rest;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import io.swagger.v3.oas.annotations.Hidden;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@ -40,6 +41,7 @@ import space.mori.dalbodeule.snapadmin.external.dto.AutocompleteSearchResult;
*/ */
@RestController @RestController
@RequestMapping(value= {"/${snapadmin.baseUrl}/api/autocomplete", "/${snapadmin.baseUrl}/api/autocomplete/"}) @RequestMapping(value= {"/${snapadmin.baseUrl}/api/autocomplete", "/${snapadmin.baseUrl}/api/autocomplete/"})
@Hidden
public class AutocompleteController { public class AutocompleteController {
@Autowired @Autowired
private SnapAdmin snapAdmin; private SnapAdmin snapAdmin;

View File

@ -123,6 +123,7 @@ public class CustomJpaRepository extends SimpleJpaRepository {
for (DbField field : schema.getSortedFields()) { for (DbField field : schema.getSortedFields()) {
if (field.isPrimaryKey()) continue; if (field.isPrimaryKey()) continue;
if (field.isReadOnly()) continue; if (field.isReadOnly()) continue;
if (field.isDisableEditField()) continue;
boolean keepValue = params.getOrDefault("__keep_" + field.getName(), "off").equals("on"); boolean keepValue = params.getOrDefault("__keep_" + field.getName(), "off").equals("on");
if (keepValue) continue; if (keepValue) continue;

View File

@ -43,7 +43,7 @@ import space.mori.dalbodeule.snapadmin.external.SnapAdmin;
import space.mori.dalbodeule.snapadmin.external.annotations.ComputedColumn; import space.mori.dalbodeule.snapadmin.external.annotations.ComputedColumn;
import space.mori.dalbodeule.snapadmin.external.annotations.DisableCreate; import space.mori.dalbodeule.snapadmin.external.annotations.DisableCreate;
import space.mori.dalbodeule.snapadmin.external.annotations.DisableDelete; import space.mori.dalbodeule.snapadmin.external.annotations.DisableDelete;
import space.mori.dalbodeule.snapadmin.external.annotations.DisableEdit; import space.mori.dalbodeule.snapadmin.external.annotations.DisableEditField;
import space.mori.dalbodeule.snapadmin.external.annotations.DisableExport; import space.mori.dalbodeule.snapadmin.external.annotations.DisableExport;
import space.mori.dalbodeule.snapadmin.external.annotations.HiddenColumn; import space.mori.dalbodeule.snapadmin.external.annotations.HiddenColumn;
import space.mori.dalbodeule.snapadmin.external.dbmapping.fields.DbField; import space.mori.dalbodeule.snapadmin.external.dbmapping.fields.DbField;
@ -351,7 +351,7 @@ public class DbObjectSchema {
} }
public boolean isEditEnabled() { public boolean isEditEnabled() {
return entityClass.getAnnotation(DisableEdit.class) == null; return entityClass.getAnnotation(DisableEditField.class) == null;
} }
public boolean isCreateEnabled() { public boolean isCreateEnabled() {

View File

@ -88,19 +88,22 @@ public class DbField {
*/ */
private String format; private String format;
private boolean disableEditField = false;
/** /**
* The schema this field belongs to * The schema this field belongs to
*/ */
@JsonIgnore @JsonIgnore
private DbObjectSchema schema; private DbObjectSchema schema;
public DbField(String javaName, String name, Field field, DbFieldType type, DbObjectSchema schema, String format) { public DbField(String javaName, String name, Field field, DbFieldType type, DbObjectSchema schema, String format, boolean isDisable) {
this.javaName = javaName; this.javaName = javaName;
this.dbName = name; this.dbName = name;
this.schema = schema; this.schema = schema;
this.field = field; this.field = field;
this.type = type; this.type = type;
this.format = format; this.format = format;
this.disableEditField = isDisable;
} }
public String getJavaName() { public String getJavaName() {
@ -190,6 +193,14 @@ public class DbField {
return type instanceof TextFieldType; return type instanceof TextFieldType;
} }
public boolean isDisableEditField() {
return disableEditField;
}
public void setDisableEditField(boolean managed) {
this.disableEditField = managed;
}
/** /**
* Returns the value to use in the "step" HTML attribute * Returns the value to use in the "step" HTML attribute
* for numeric data fields. For fields that are not numeric, * for numeric data fields. For fields that are not numeric,

View File

@ -24,26 +24,116 @@ import java.time.ZoneOffset;
import java.util.List; import java.util.List;
import space.mori.dalbodeule.snapadmin.external.dto.CompareOperator; import space.mori.dalbodeule.snapadmin.external.dto.CompareOperator;
import java.time.*;
import java.time.format.*;
import java.time.temporal.TemporalAccessor;
import java.util.Date;
public class InstantFieldType extends DbFieldType { public class InstantFieldType extends DbFieldType {
@Override // 다양한 날짜/시간 형식을 처리할 있는 포맷터들
public String getFragmentName() { private static final DateTimeFormatter[] FORMATTERS = {
return "datetime"; DateTimeFormatter.ISO_INSTANT,
} DateTimeFormatter.ISO_DATE_TIME,
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"),
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
};
@Override @Override
public Object parseValue(Object value) { public String getFragmentName() {
if (value == null || value.toString().isBlank()) return null; return "datetime";
return LocalDateTime.parse(value.toString()).toInstant(ZoneOffset.UTC); }
}
@Override @Override
public Class<?> getJavaClass() { public Object parseValue(Object value) {
return Instant.class; if (value == null) return null;
}
@Override // 이미 Instant 타입인 경우
public List<CompareOperator> getCompareOperators() { if (value instanceof Instant) {
return List.of(CompareOperator.AFTER, CompareOperator.STRING_EQ, CompareOperator.BEFORE); return value;
} }
// LocalDateTime에서 Instant로 변환
if (value instanceof LocalDateTime) {
return ((LocalDateTime) value)
.atZone(ZoneId.systemDefault())
.toInstant();
}
// Date에서 Instant로 변환
if (value instanceof Date) {
return ((Date) value).toInstant();
}
// LocalDate에서 Instant로 변환
if (value instanceof LocalDate) {
return ((LocalDate) value)
.atStartOfDay(ZoneId.systemDefault())
.toInstant();
}
// 문자열 처리
String stringValue = value.toString();
if (stringValue.isBlank()) return null;
stringValue = stringValue.trim();
// 직접 Instant 파싱 시도
try {
return Instant.parse(stringValue);
} catch (DateTimeParseException e) {
// 실패, 다른 방법으로 시도
}
// 다른 날짜/시간 형식을 통해 변환 시도
for (DateTimeFormatter formatter : FORMATTERS) {
try {
// ISO_INSTANT의 경우 Instant.parse() 동일하므로 스킵
if (formatter == DateTimeFormatter.ISO_INSTANT) {
continue;
}
// ISO_DATE_TIME 형식은 ZoneId가 필요
if (formatter == DateTimeFormatter.ISO_DATE_TIME) {
return ZonedDateTime.parse(stringValue, formatter).toInstant();
}
// Z로 끝나는 형식은 UTC 시간으로 파싱
if (stringValue.endsWith("Z")) {
return ZonedDateTime.parse(stringValue, formatter.withZone(ZoneOffset.UTC))
.toInstant();
}
// 기타 형식은 시스템 기본 시간대 사용
TemporalAccessor ta = formatter.parse(stringValue);
return LocalDateTime.from(ta)
.atZone(ZoneId.systemDefault())
.toInstant();
} catch (DateTimeParseException e) {
// 다음 형식 시도
continue;
}
}
// 밀리초 타임스탬프로 시도
try {
long timestamp = Long.parseLong(stringValue);
return Instant.ofEpochMilli(timestamp);
} catch (NumberFormatException e) {
// 숫자 파싱 실패, 무시
}
// 모든 파싱 시도 실패
return null;
}
@Override
public Class<?> getJavaClass() {
return Instant.class;
}
@Override
public List<CompareOperator> getCompareOperators() {
return List.of(CompareOperator.AFTER, CompareOperator.STRING_EQ, CompareOperator.BEFORE);
}
} }

View File

@ -18,30 +18,65 @@
package space.mori.dalbodeule.snapadmin.external.dbmapping.fields; package space.mori.dalbodeule.snapadmin.external.dbmapping.fields;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List; import java.util.List;
import space.mori.dalbodeule.snapadmin.external.dto.CompareOperator; import space.mori.dalbodeule.snapadmin.external.dto.CompareOperator;
public class LocalDateTimeFieldType extends DbFieldType { public class LocalDateTimeFieldType extends DbFieldType {
@Override private static final DateTimeFormatter[] FORMATTERS = {
public String getFragmentName() { DateTimeFormatter.ISO_LOCAL_DATE_TIME,
return "datetime"; DateTimeFormatter.ISO_DATE_TIME,
} DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"),
DateTimeFormatter.ofPattern("yyyy-MM-dd")
};
@Override @Override
public Object parseValue(Object value) { public String getFragmentName() {
if (value == null || value.toString().isBlank()) return null; return "datetime";
return LocalDateTime.parse(value.toString()); }
}
@Override @Override
public Class<?> getJavaClass() { public Object parseValue(Object value) {
return LocalDateTime.class; if (value == null || value.toString().isBlank()) return null;
}
@Override // 이미 LocalDateTime 객체인 경우
public List<CompareOperator> getCompareOperators() { if (value instanceof LocalDateTime) {
return List.of(CompareOperator.AFTER, CompareOperator.STRING_EQ, CompareOperator.BEFORE); return value;
} }
String stringValue = value.toString().trim();
// 여러 형식으로 파싱 시도
for (DateTimeFormatter formatter : FORMATTERS) {
try {
// 날짜만 있는 형식인 경우 시간을 00:00:00으로 설정
if (formatter.equals(DateTimeFormatter.ofPattern("yyyy-MM-dd"))) {
return LocalDate.parse(stringValue, formatter).atStartOfDay();
}
return LocalDateTime.parse(stringValue, formatter);
} catch (DateTimeParseException e) {
// 형식으로 파싱 실패, 다음 형식 시도
continue;
}
}
// 모든 형식 파싱 실패 예외 발생
throw new IllegalArgumentException("날짜/시간 형식을 파싱할 수 없습니다: " + stringValue);
}
@Override
public Class<?> getJavaClass() {
return LocalDateTime.class;
}
@Override
public List<CompareOperator> getCompareOperators() {
return List.of(CompareOperator.AFTER, CompareOperator.STRING_EQ, CompareOperator.BEFORE);
}
} }

View File

@ -18,12 +18,23 @@
package space.mori.dalbodeule.snapadmin.external.dbmapping.fields; package space.mori.dalbodeule.snapadmin.external.dbmapping.fields;
import java.time.OffsetDateTime; import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List; import java.util.List;
import space.mori.dalbodeule.snapadmin.external.dto.CompareOperator; import space.mori.dalbodeule.snapadmin.external.dto.CompareOperator;
public class OffsetDateTimeFieldType extends DbFieldType { public class OffsetDateTimeFieldType extends DbFieldType {
// 다양한 날짜/시간 형식을 처리할 있는 포맷터들
private static final DateTimeFormatter[] FORMATTERS = {
DateTimeFormatter.ISO_OFFSET_DATE_TIME,
DateTimeFormatter.ISO_ZONED_DATE_TIME,
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"),
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssXXX")
};
@Override @Override
public String getFragmentName() { public String getFragmentName() {
return "datetime"; return "datetime";
@ -31,8 +42,110 @@ public class OffsetDateTimeFieldType extends DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
if (value == null || value.toString().isBlank()) return null; if (value == null) return null;
return OffsetDateTime.parse(value.toString());
// 이미 OffsetDateTime 타입인 경우
if (value instanceof OffsetDateTime) {
return value;
}
// ZonedDateTime에서 OffsetDateTime으로 변환
if (value instanceof ZonedDateTime) {
return ((ZonedDateTime) value).toOffsetDateTime();
}
// LocalDateTime에서 OffsetDateTime으로 변환 (시스템 기본 오프셋 사용)
if (value instanceof LocalDateTime) {
return ((LocalDateTime) value)
.atZone(ZoneId.systemDefault())
.toOffsetDateTime();
}
// Instant에서 OffsetDateTime으로 변환
if (value instanceof Instant) {
return ((Instant) value)
.atZone(ZoneId.systemDefault())
.toOffsetDateTime();
}
// LocalDate에서 OffsetDateTime으로 변환
if (value instanceof LocalDate) {
return ((LocalDate) value)
.atStartOfDay()
.atZone(ZoneId.systemDefault())
.toOffsetDateTime();
}
// 문자열 처리
String stringValue = value.toString();
if (stringValue.isBlank()) return null;
stringValue = stringValue.trim();
// 직접 OffsetDateTime 파싱 시도
try {
return OffsetDateTime.parse(stringValue);
} catch (DateTimeParseException e) {
// 실패, 다른 방법으로 시도
}
// 여러 가지 형식으로 파싱 시도
for (DateTimeFormatter formatter : FORMATTERS) {
try {
// ISO_OFFSET_DATE_TIME은 이미 위에서 시도했으므로 스킵
if (formatter == DateTimeFormatter.ISO_OFFSET_DATE_TIME) {
continue;
}
if (formatter == DateTimeFormatter.ISO_ZONED_DATE_TIME) {
return ZonedDateTime.parse(stringValue, formatter).toOffsetDateTime();
}
return OffsetDateTime.parse(stringValue, formatter);
} catch (DateTimeParseException e) {
// 다음 형식 시도
continue;
}
}
// ISO 날짜/시간 형식에 시스템 기본 오프셋 추가 시도
try {
LocalDateTime ldt = LocalDateTime.parse(stringValue);
return ldt.atZone(ZoneId.systemDefault()).toOffsetDateTime();
} catch (DateTimeParseException e) {
// 실패, 다음 방법 시도
}
// 날짜만 있는 경우 (UTC 자정으로 처리)
try {
LocalDate date = LocalDate.parse(stringValue);
return date.atStartOfDay().atZone(ZoneId.systemDefault()).toOffsetDateTime();
} catch (DateTimeParseException e) {
// 날짜 파싱 실패, 무시
}
// 밀리초 타임스탬프로 시도
try {
long timestamp = Long.parseLong(stringValue);
return Instant.ofEpochMilli(timestamp)
.atZone(ZoneId.systemDefault())
.toOffsetDateTime();
} catch (NumberFormatException e) {
// 숫자 파싱 실패, 무시
}
// Z로 끝나는 UTC 시간 문자열 처리 시도
if (stringValue.endsWith("Z")) {
try {
Instant instant = Instant.parse(stringValue);
return instant.atZone(ZoneId.systemDefault()).toOffsetDateTime();
} catch (DateTimeParseException e) {
// 실패, 무시
}
}
// 모든 파싱 시도 실패
return null;
} }
@Override @Override

View File

@ -20,6 +20,7 @@ package space.mori.dalbodeule.snapadmin.internal.service;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.logging.Logger;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -36,10 +37,17 @@ public class ConsoleQueryService {
@Autowired @Autowired
private ConsoleQueryRepository repo; private ConsoleQueryRepository repo;
private final Logger logger = Logger.getLogger(ConsoleQueryService.class.getName());
public ConsoleQuery save(ConsoleQuery q) { public ConsoleQuery save(ConsoleQuery q) {
return internalTransactionTemplate.execute((status) -> { try {
return repo.save(q); return internalTransactionTemplate.execute((status) -> {
}); return repo.save(q);
});
} catch(Exception e) {
logger.severe("Error while saving console query: " + e.getMessage());
throw e;
}
} }
public void delete(String id) { public void delete(String id) {

View File

@ -31,8 +31,8 @@
<form class="form" enctype="multipart/form-data" method="post" th:action="|/${snapadmin_baseUrl}/model/${className}/create|"> <form class="form" enctype="multipart/form-data" method="post" th:action="|/${snapadmin_baseUrl}/model/${className}/create|">
<input type="hidden" name="__snapadmin_create" th:value="${create}"> <input type="hidden" name="__snapadmin_create" th:value="${create}">
<div th:each="field : ${schema.getSortedFields(false)}" class="mt-2" <div th:each="field : ${schema.getSortedFields(false)}" class="mt-2"
th:if="${!field.isGeneratedValue() || !create}" th:unless="${(field.isGeneratedValue() && create) || field.isDisableEditField()}"
th:classAppend="|${validationErrors != null && validationErrors.hasErrors(field.getJavaName()) ? 'invalid' : ''}|"> th:classAppend="|${validationErrors != null && validationErrors.hasErrors(field.getJavaName()) ? 'invalid' : ''}|">
<label th:for="|__id_${field.getName()}|" class="mb-1 fw-bold"> <label th:for="|__id_${field.getName()}|" class="mb-1 fw-bold">
<span th:if="${!field.isNullable() && !field.isPrimaryKey()}"> <span th:if="${!field.isNullable() && !field.isPrimaryKey()}">
* *
@ -66,7 +66,8 @@
<div class="separator mt-3 mb-2 separator-light"></div> <div class="separator mt-3 mb-2 separator-light"></div>
</div> </div>
<div th:each="field : ${schema.getManyToManyOwnedFields()}" class="mt-3"> <div th:each="field : ${schema.getManyToManyOwnedFields()}" class="mt-3"
th:if="${!field.isDisableEditField()}">
<h2><span th:title="|${field.getType()} relationship|"><i class="bi bi-share"></i> [[ ${field.getJavaName()} ]]</span></h2> <h2><span th:title="|${field.getType()} relationship|"><i class="bi bi-share"></i> [[ ${field.getJavaName()} ]]</span></h2>
<div th:replace="~{snapadmin/fragments/forms :: input_autocomplete_multi(field=${field}, <div th:replace="~{snapadmin/fragments/forms :: input_autocomplete_multi(field=${field},
values=${object != null ? object.traverseMany(field) : null } )}"> values=${object != null ? object.traverseMany(field) : null } )}">