mirror of
https://github.com/dalbodeule/snap-admin.git
synced 2025-06-08 21:38:21 +00:00
Compare commits
2 Commits
2e3e11aafb
...
50f2844319
Author | SHA1 | Date | |
---|---|---|---|
|
50f2844319 | ||
|
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,7 +354,8 @@ 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);
|
||||||
field.setConnectedType(connectedType);
|
field.setConnectedType(connectedType);
|
||||||
|
|
||||||
@ -363,6 +366,8 @@ public class SnapAdmin {
|
|||||||
|
|
||||||
if (field.isPrimaryKey())
|
if (field.isPrimaryKey())
|
||||||
field.setNullable(false);
|
field.setNullable(false);
|
||||||
|
|
||||||
|
field.setDisableEditField(disableEdit != null);
|
||||||
|
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -87,6 +87,8 @@ public class DbField {
|
|||||||
* annotation has been applied.
|
* annotation has been applied.
|
||||||
*/
|
*/
|
||||||
private String format;
|
private String format;
|
||||||
|
|
||||||
|
private boolean disableEditField = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The schema this field belongs to
|
* The schema this field belongs to
|
||||||
@ -189,6 +191,14 @@ public class DbField {
|
|||||||
public boolean isText() {
|
public boolean isText() {
|
||||||
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
|
||||||
|
@ -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;
|
||||||
}
|
|
||||||
|
// 이미 Instant 타입인 경우
|
||||||
@Override
|
if (value instanceof Instant) {
|
||||||
public List<CompareOperator> getCompareOperators() {
|
return value;
|
||||||
return List.of(CompareOperator.AFTER, CompareOperator.STRING_EQ, CompareOperator.BEFORE);
|
}
|
||||||
}
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
}
|
|
||||||
|
// 이미 LocalDateTime 객체인 경우
|
||||||
@Override
|
if (value instanceof LocalDateTime) {
|
||||||
public List<CompareOperator> getCompareOperators() {
|
return value;
|
||||||
return List.of(CompareOperator.AFTER, CompareOperator.STRING_EQ, CompareOperator.BEFORE);
|
}
|
||||||
}
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
@ -44,4 +157,4 @@ public class OffsetDateTimeFieldType extends DbFieldType {
|
|||||||
public List<CompareOperator> getCompareOperators() {
|
public List<CompareOperator> getCompareOperators() {
|
||||||
return List.of(CompareOperator.AFTER, CompareOperator.STRING_EQ, CompareOperator.BEFORE);
|
return List.of(CompareOperator.AFTER, CompareOperator.STRING_EQ, CompareOperator.BEFORE);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
@ -35,11 +36,18 @@ 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) {
|
||||||
|
@ -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 } )}">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user