mirror of
https://github.com/dalbodeule/snap-admin.git
synced 2025-06-08 21:38:21 +00:00
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:
parent
2e3e11aafb
commit
28063ed583
11
.idea/snap-admin.iml
generated
Normal file
11
.idea/snap-admin.iml
generated
Normal 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>
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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 {
|
||||||
}
|
}
|
11
src/main/java/space/mori/dalbodeule/snapadmin/external/annotations/HiddenEditForm.java
vendored
Normal file
11
src/main/java/space/mori/dalbodeule/snapadmin/external/annotations/HiddenEditForm.java
vendored
Normal 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 {
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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";
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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() {
|
||||||
|
@ -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,
|
||||||
|
@ -24,8 +24,21 @@ 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 {
|
||||||
|
// 다양한 날짜/시간 형식을 처리할 수 있는 포맷터들
|
||||||
|
private static final DateTimeFormatter[] FORMATTERS = {
|
||||||
|
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 String getFragmentName() {
|
public String getFragmentName() {
|
||||||
return "datetime";
|
return "datetime";
|
||||||
@ -33,8 +46,85 @@ public class InstantFieldType 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 LocalDateTime.parse(value.toString()).toInstant(ZoneOffset.UTC);
|
|
||||||
|
// 이미 Instant 타입인 경우
|
||||||
|
if (value instanceof Instant) {
|
||||||
|
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
|
@Override
|
||||||
|
@ -18,12 +18,24 @@
|
|||||||
|
|
||||||
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 {
|
||||||
|
private static final DateTimeFormatter[] FORMATTERS = {
|
||||||
|
DateTimeFormatter.ISO_LOCAL_DATE_TIME,
|
||||||
|
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 String getFragmentName() {
|
public String getFragmentName() {
|
||||||
return "datetime";
|
return "datetime";
|
||||||
@ -32,7 +44,30 @@ public class LocalDateTimeFieldType 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 || value.toString().isBlank()) return null;
|
||||||
return LocalDateTime.parse(value.toString());
|
|
||||||
|
// 이미 LocalDateTime 객체인 경우
|
||||||
|
if (value instanceof LocalDateTime) {
|
||||||
|
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
|
@Override
|
||||||
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
try {
|
||||||
return internalTransactionTemplate.execute((status) -> {
|
return internalTransactionTemplate.execute((status) -> {
|
||||||
return repo.save(q);
|
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) {
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
<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 } )}">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user