mirror of
https://github.com/dalbodeule/snap-admin.git
synced 2025-08-11 14:31:12 +00:00
Compare commits
5 Commits
2e3e11aafb
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
9963f504c5 | ||
|
61ff240456 | ||
|
35b02d156b | ||
|
50f2844319 | ||
|
28063ed583 |
17
.idea/dataSources.xml
generated
Normal file
17
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="mydatabase@localhost" uuid="2f05e8c9-fa97-4b20-aa60-3de9e52721d6">
|
||||||
|
<driver-ref>postgresql</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:postgresql://localhost:55432/mydatabase</jdbc-url>
|
||||||
|
<jdbc-additional-properties>
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.container.port" />
|
||||||
|
</jdbc-additional-properties>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -8,6 +8,7 @@
|
|||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/debugBackend" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
|
6
.idea/kotlinc.xml
generated
Normal file
6
.idea/kotlinc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="version" value="2.1.10" />
|
||||||
|
</component>
|
||||||
|
</project>
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
76
debugBackend/build.gradle.kts
Normal file
76
debugBackend/build.gradle.kts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("jvm") version "2.1.10"
|
||||||
|
kotlin("plugin.spring") version "2.1.10"
|
||||||
|
id("org.hibernate.orm") version "6.5.2.Final"
|
||||||
|
id("org.springframework.boot") version "3.4.5"
|
||||||
|
id("io.spring.dependency-management") version "1.1.7"
|
||||||
|
id("org.graalvm.buildtools.native") version "0.10.5"
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "space.mori.dalbodeule"
|
||||||
|
version = "0.5.1"
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hibernate {
|
||||||
|
enhancement {
|
||||||
|
enableAssociationManagement.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-batch")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-data-redis")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-websocket")
|
||||||
|
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6")
|
||||||
|
|
||||||
|
implementation("io.swagger.core.v3:swagger-core:2.2.30")
|
||||||
|
implementation("io.swagger.core.v3:swagger-annotations:2.2.30")
|
||||||
|
|
||||||
|
implementation("io.github.cdimascio:dotenv-kotlin:6.4.1")
|
||||||
|
|
||||||
|
developmentOnly("org.springframework.boot:spring-boot-docker-compose")
|
||||||
|
runtimeOnly("org.postgresql:postgresql:42.7.4")
|
||||||
|
|
||||||
|
implementation("jakarta.xml.bind:jakarta.xml.bind-api:4.0.2")
|
||||||
|
implementation("javax.xml.bind:jaxb-api:2.3.1")
|
||||||
|
|
||||||
|
// HTTP 클라이언트
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-webflux")
|
||||||
|
|
||||||
|
implementation(rootProject)
|
||||||
|
|
||||||
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
||||||
|
testImplementation("org.springframework.batch:spring-batch-test")
|
||||||
|
testImplementation("org.springframework.security:spring-security-test")
|
||||||
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named<org.springframework.boot.gradle.tasks.run.BootRun>("bootRun") {
|
||||||
|
systemProperty("spring.profiles.active", "dev")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll("-Xjsr305=strict")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Test> {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
21
debugBackend/compose.yaml
Normal file
21
debugBackend/compose.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
services:
|
||||||
|
pgvector:
|
||||||
|
image: 'pgvector/pgvector:pg16'
|
||||||
|
environment:
|
||||||
|
- 'POSTGRES_DB=mydatabase'
|
||||||
|
- 'POSTGRES_PASSWORD=secret'
|
||||||
|
- 'POSTGRES_USER=myuser'
|
||||||
|
labels:
|
||||||
|
- "org.springframework.boot.service-connection=postgres"
|
||||||
|
ports:
|
||||||
|
- target: 5432
|
||||||
|
published: 55432
|
||||||
|
protocol: tcp
|
||||||
|
volumes:
|
||||||
|
- postgresql:/var/lib/postgresql/data
|
||||||
|
redis:
|
||||||
|
image: 'redis:latest'
|
||||||
|
ports:
|
||||||
|
- '6379'
|
||||||
|
volumes:
|
||||||
|
postgresql:
|
@@ -0,0 +1,32 @@
|
|||||||
|
package space.mori.dalbodeule.debug
|
||||||
|
|
||||||
|
import io.github.cdimascio.dotenv.dotenv
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
|
import org.springframework.boot.autoconfigure.domain.EntityScan
|
||||||
|
import org.springframework.boot.runApplication
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
|
||||||
|
import space.mori.dalbodeule.snapadmin.external.annotations.SnapAdminEnabled
|
||||||
|
|
||||||
|
val dotenv = dotenv {
|
||||||
|
ignoreIfMissing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@SnapAdminEnabled
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableJpaRepositories(basePackages = ["space.mori.dalbodeule.debug.repository"])
|
||||||
|
@EntityScan(basePackages = ["space.mori.dalbodeule.debug.model"])
|
||||||
|
class DebugApplication
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
val envVars = mapOf(
|
||||||
|
"DB_HOST" to dotenv["DB_HOST"],
|
||||||
|
"DB_PORT" to dotenv["DB_PORT"],
|
||||||
|
"DB_NAME" to dotenv["DB_NAME"],
|
||||||
|
"DB_USER" to dotenv["DB_USER"],
|
||||||
|
"DB_PASSWORD" to dotenv["DB_PASSWORD"]
|
||||||
|
)
|
||||||
|
|
||||||
|
runApplication<DebugApplication>(*args) {
|
||||||
|
setDefaultProperties(envVars)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,43 @@
|
|||||||
|
package space.mori.dalbodeule.debug.config
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
|
import org.springframework.security.core.userdetails.User
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||||
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
|
return http
|
||||||
|
.csrf { it.disable() }
|
||||||
|
.authorizeHttpRequests {
|
||||||
|
it.anyRequest().authenticated()
|
||||||
|
}
|
||||||
|
.httpBasic {}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun userDetailsService(passwordEncoder: PasswordEncoder): UserDetailsService {
|
||||||
|
val admin = User.builder()
|
||||||
|
.username("test@gmail.com")
|
||||||
|
.password(passwordEncoder.encode("password"))
|
||||||
|
.roles("ADMIN")
|
||||||
|
.build()
|
||||||
|
return InMemoryUserDetailsManager(admin)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun passwordEncoder(): PasswordEncoder {
|
||||||
|
return BCryptPasswordEncoder()
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
package space.mori.dalbodeule.debug.model
|
||||||
|
|
||||||
|
import jakarta.persistence.Column
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.GeneratedValue
|
||||||
|
import jakarta.persistence.GenerationType
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import jakarta.persistence.Table
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name="test_table")
|
||||||
|
data class TestTable(
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
var id: String? = null,
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 32)
|
||||||
|
var name: String
|
||||||
|
) {
|
||||||
|
constructor(): this(null, "")
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
package space.mori.dalbodeule.debug.repository
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import space.mori.dalbodeule.debug.model.TestTable
|
||||||
|
|
||||||
|
interface TestTableRepository: JpaRepository<TestTable, String> {
|
||||||
|
}
|
18
debugBackend/src/main/resources/application.yml
Normal file
18
debugBackend/src/main/resources/application.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}
|
||||||
|
username: ${DB_USER}
|
||||||
|
password: ${DB_PASSWORD}
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
jpa:
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: update
|
||||||
|
show-sql: true
|
||||||
|
database-platform: org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
snapadmin:
|
||||||
|
enabled: true
|
||||||
|
baseUrl: admin
|
||||||
|
models-package: space.mori.dalbodeule.debug.model
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
root: INFO
|
@@ -5,3 +5,5 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
rootProject.name = "snap-admin"
|
rootProject.name = "snap-admin"
|
||||||
|
|
||||||
|
include("debugBackend")
|
@@ -0,0 +1,135 @@
|
|||||||
|
package space.mori.dalbodeule.snapadmin.aot;
|
||||||
|
|
||||||
|
import org.springframework.aot.hint.RuntimeHints;
|
||||||
|
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||||
|
import space.mori.dalbodeule.snapadmin.external.SnapAdmin;
|
||||||
|
import space.mori.dalbodeule.snapadmin.external.SnapAdminProperties;
|
||||||
|
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.DisplayImage; // Assuming this is used
|
||||||
|
import space.mori.dalbodeule.snapadmin.external.annotations.HiddenEditForm; // Assuming this is used
|
||||||
|
import space.mori.dalbodeule.snapadmin.external.dbmapping.CustomJpaRepository;
|
||||||
|
import space.mori.dalbodeule.snapadmin.external.dbmapping.DbObjectSchema;
|
||||||
|
import space.mori.dalbodeule.snapadmin.external.dbmapping.fields.*;
|
||||||
|
import space.mori.dalbodeule.snapadmin.external.dto.MappingError;
|
||||||
|
import space.mori.dalbodeule.snapadmin.external.misc.Utils;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.springframework.aot.hint.MemberCategory.*;
|
||||||
|
|
||||||
|
public class SnapAdminRuntimeHints implements RuntimeHintsRegistrar {
|
||||||
|
|
||||||
|
private static final Set<Class<?>> dbFieldTypes = new HashSet<>(Arrays.asList(
|
||||||
|
BooleanFieldType.class, LongFieldType.class, IntegerFieldType.class,
|
||||||
|
BigIntegerFieldType.class, ShortFieldType.class, StringFieldType.class,
|
||||||
|
LocalDateFieldType.class, DateFieldType.class, LocalDateTimeFieldType.class,
|
||||||
|
InstantFieldType.class, FloatFieldType.class, DoubleFieldType.class,
|
||||||
|
BigDecimalFieldType.class, ByteArrayFieldType.class, OffsetDateTimeFieldType.class,
|
||||||
|
ByteFieldType.class, UUIDFieldType.class, CharFieldType.class,
|
||||||
|
EnumFieldType.class, TextFieldType.class
|
||||||
|
// Add any other concrete DbFieldType implementations here
|
||||||
|
));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
|
||||||
|
// Register SnapAdmin's own classes
|
||||||
|
hints.reflection().registerType(SnapAdmin.class, INTROSPECT_DECLARED_METHODS, INVOKE_DECLARED_METHODS);
|
||||||
|
hints.reflection().registerType(SnapAdminProperties.class, INVOKE_DECLARED_CONSTRUCTORS, INVOKE_PUBLIC_METHODS); // For Spring binding
|
||||||
|
|
||||||
|
hints.reflection().registerType(DbObjectSchema.class, INVOKE_DECLARED_CONSTRUCTORS, INTROSPECT_DECLARED_METHODS, INVOKE_PUBLIC_METHODS);
|
||||||
|
// CustomJpaRepository 인터페이스 자체는 생성자 호출 힌트가 불필요할 수 있음
|
||||||
|
hints.reflection().registerType(DbField.class, INVOKE_DECLARED_CONSTRUCTORS, INTROSPECT_DECLARED_METHODS, INVOKE_PUBLIC_METHODS);
|
||||||
|
hints.reflection().registerType(MappingError.class, INVOKE_DECLARED_CONSTRUCTORS);
|
||||||
|
// hints.reflection().registerType(Utils.class); // 사용 패턴 확인 후 필요하면 활성화
|
||||||
|
|
||||||
|
// Register DbFieldType and its subclasses for default constructor invocation
|
||||||
|
hints.reflection().registerType(DbFieldType.class);
|
||||||
|
for (Class<?> dbFieldTypeClass : dbFieldTypes) {
|
||||||
|
hints.reflection().registerType(dbFieldTypeClass, INVOKE_DECLARED_CONSTRUCTORS);
|
||||||
|
}
|
||||||
|
// EnumFieldType has a special constructor too
|
||||||
|
hints.reflection().registerType(EnumFieldType.class, INVOKE_DECLARED_CONSTRUCTORS);
|
||||||
|
|
||||||
|
|
||||||
|
// Register SnapAdmin's custom annotations (and assume their attributes might be read)
|
||||||
|
registerAnnotation(hints, Disable.class);
|
||||||
|
registerAnnotation(hints, DisableEditField.class);
|
||||||
|
registerAnnotation(hints, DisplayFormat.class);
|
||||||
|
registerAnnotation(hints, DisplayImage.class);
|
||||||
|
registerAnnotation(hints, HiddenEditForm.class);
|
||||||
|
|
||||||
|
// Register Jakarta Persistence annotations (and assume their attributes might be read)
|
||||||
|
registerAnnotation(hints, jakarta.persistence.Entity.class);
|
||||||
|
registerAnnotation(hints, jakarta.persistence.Id.class);
|
||||||
|
registerAnnotation(hints, jakarta.persistence.Column.class);
|
||||||
|
registerAnnotation(hints, jakarta.persistence.Lob.class);
|
||||||
|
registerAnnotation(hints, jakarta.persistence.Enumerated.class);
|
||||||
|
registerAnnotation(hints, jakarta.persistence.EnumType.class); // TYPE_VISIBLE 제거
|
||||||
|
registerAnnotation(hints, jakarta.persistence.OneToMany.class);
|
||||||
|
registerAnnotation(hints, jakarta.persistence.ManyToMany.class);
|
||||||
|
registerAnnotation(hints, jakarta.persistence.ManyToOne.class);
|
||||||
|
registerAnnotation(hints, jakarta.persistence.OneToOne.class);
|
||||||
|
registerAnnotation(hints, jakarta.persistence.JoinColumn.class);
|
||||||
|
// Add other JPA annotations if used, e.g. @Table, @Transient
|
||||||
|
|
||||||
|
// Hints for operations on arbitrary (user-defined) @Entity classes
|
||||||
|
// 가능하면 스캔 범위를 제한하거나, 필요한 메서드만 등록
|
||||||
|
// 예시: 특정 패키지 내의 @Entity 클래스 스캔
|
||||||
|
// ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
|
||||||
|
// scanner.addIncludeFilter(new AnnotationTypeFilter(jakarta.persistence.Entity.class));
|
||||||
|
// for (BeanDefinition bd : scanner.findCandidateComponents("com.example.entities")) {
|
||||||
|
// try {
|
||||||
|
// Class<?> entityClass = Class.forName(bd.getBeanClassName());
|
||||||
|
// hints.reflection().registerType(entityClass, INTROSPECT_DECLARED_FIELDS, INVOKE_DECLARED_METHODS);
|
||||||
|
// } catch (ClassNotFoundException e) {
|
||||||
|
// // Handle exception
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Register SnapAdmin's own classes
|
||||||
|
hints.reflection().registerType(SnapAdmin.class, INTROSPECT_DECLARED_METHODS, INVOKE_DECLARED_METHODS);
|
||||||
|
hints.reflection().registerType(SnapAdminProperties.class, INVOKE_DECLARED_CONSTRUCTORS, INVOKE_PUBLIC_METHODS); // For Spring binding
|
||||||
|
|
||||||
|
hints.reflection().registerType(DbObjectSchema.class, INVOKE_DECLARED_CONSTRUCTORS, INTROSPECT_DECLARED_METHODS, INVOKE_PUBLIC_METHODS);
|
||||||
|
hints.reflection().registerType(CustomJpaRepository.class, INVOKE_DECLARED_CONSTRUCTORS);
|
||||||
|
hints.reflection().registerType(DbField.class, INVOKE_DECLARED_CONSTRUCTORS, INTROSPECT_DECLARED_METHODS, INVOKE_PUBLIC_METHODS);
|
||||||
|
hints.reflection().registerType(MappingError.class, INVOKE_DECLARED_CONSTRUCTORS);
|
||||||
|
hints.reflection().registerType(Utils.class); // If it contains static methods called, or if instantiated
|
||||||
|
|
||||||
|
|
||||||
|
// For Class.forName(className) on unknown classes (typically user entities)
|
||||||
|
// and subsequent operations like getDeclaredFields(), getAnnotation(), newInstance()
|
||||||
|
// This is a general hint. Users should still ensure their entities are hinted.
|
||||||
|
// Consider making this more specific if possible, e.g., by scanning packages if configured.
|
||||||
|
hints.reflection().registerType(Object.class,
|
||||||
|
INTROSPECT_DECLARED_CONSTRUCTORS, INVOKE_DECLARED_CONSTRUCTORS,
|
||||||
|
INTROSPECT_DECLARED_METHODS, INVOKE_DECLARED_METHODS, // For getters/setters if library invokes them
|
||||||
|
DECLARED_FIELDS // For field access
|
||||||
|
);
|
||||||
|
|
||||||
|
// For ClassPathScanningCandidateComponentProvider
|
||||||
|
hints.reflection().registerType(org.springframework.beans.factory.config.BeanDefinition.class, INVOKE_PUBLIC_METHODS); // For getBeanClassName()
|
||||||
|
hints.reflection().registerType(org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.class, INVOKE_DECLARED_CONSTRUCTORS);
|
||||||
|
hints.reflection().registerType(org.springframework.core.type.filter.AnnotationTypeFilter.class, INVOKE_DECLARED_CONSTRUCTORS);
|
||||||
|
|
||||||
|
|
||||||
|
// Resource hints if any .properties or .xml files are loaded from classpath by the library
|
||||||
|
// hints.resources().registerPattern("my-library-config.xml");
|
||||||
|
|
||||||
|
// Proxy hints if JDK proxies are created for library interfaces
|
||||||
|
// hints.proxies().registerJdkProxy(MyLibraryInterface.class);
|
||||||
|
|
||||||
|
// Serialization hints if objects are serialized by the library
|
||||||
|
// hints.serialization().registerType(MySerializableObject.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerAnnotation(RuntimeHints hints, Class<?> annotationType) {
|
||||||
|
hints.reflection().registerType(annotationType, INVOKE_DECLARED_METHODS);
|
||||||
|
}
|
||||||
|
private void registerAnnotation(RuntimeHints hints, Class<?> annotationType, org.springframework.aot.hint.MemberCategory... categories) {
|
||||||
|
hints.reflection().registerType(annotationType, categories);
|
||||||
|
}
|
||||||
|
}
|
@@ -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;
|
||||||
@@ -88,7 +89,7 @@ public class SnapAdmin {
|
|||||||
|
|
||||||
private boolean authenticated;
|
private boolean authenticated;
|
||||||
|
|
||||||
private static final String VERSION = "0.4.1";
|
private static final String VERSION = "0.6.2";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the SnapAdmin instance by scanning the `@Entity` beans and loading
|
* Builds the SnapAdmin instance by scanning the `@Entity` beans and loading
|
||||||
@@ -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,6 +354,7 @@ 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);
|
||||||
@@ -364,6 +367,8 @@ public class SnapAdmin {
|
|||||||
if (field.isPrimaryKey())
|
if (field.isPrimaryKey())
|
||||||
field.setNullable(false);
|
field.setNullable(false);
|
||||||
|
|
||||||
|
field.setDisableEditField(disableEdit != null);
|
||||||
|
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,25 +396,32 @@ public class SnapAdmin {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private DbFieldType mapForeignKeyType(Class<?> entityClass) {
|
private DbFieldType mapForeignKeyType(Class<?> entityClass) {
|
||||||
try {
|
try {
|
||||||
Object linkedEntity = entityClass.getConstructor().newInstance();
|
Object linkedEntity = entityClass.getConstructor().newInstance();
|
||||||
Class<?> linkType = null;
|
Class<?> clazz = linkedEntity.getClass();
|
||||||
|
Field idField = null;
|
||||||
|
|
||||||
for (Field ef : linkedEntity.getClass().getDeclaredFields()) {
|
// 상속 계층 전체를 스캔하여 @Id 필드 탐색
|
||||||
if (ef.getAnnotationsByType(Id.class).length != 0) {
|
while (clazz != null && clazz != Object.class) {
|
||||||
linkType = ef.getType();
|
for (Field ef : clazz.getDeclaredFields()) {
|
||||||
}
|
if (ef.getAnnotationsByType(jakarta.persistence.Id.class).length != 0) {
|
||||||
}
|
idField = ef;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (idField != null) break;
|
||||||
|
clazz = clazz.getSuperclass();
|
||||||
|
}
|
||||||
|
|
||||||
if (linkType == null)
|
if (idField == null)
|
||||||
throw new SnapAdminException("Unable to find @Id field in Entity class " + entityClass);
|
throw new SnapAdminException("Unable to find @Id field in Entity class hierarchy: " + entityClass);
|
||||||
|
|
||||||
return DbFieldType.fromClass(linkType).getConstructor().newInstance();
|
return DbFieldType.fromClass(idField.getType()).getConstructor().newInstance();
|
||||||
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
|
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
|
||||||
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
|
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
|
||||||
throw new SnapAdminException(e);
|
throw new SnapAdminException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAuthenticated() {
|
public boolean isAuthenticated() {
|
||||||
return authenticated;
|
return authenticated;
|
||||||
|
@@ -1,118 +1,55 @@
|
|||||||
/*
|
|
||||||
* SnapAdmin - An automatically generated CRUD admin UI for Spring Boot apps
|
|
||||||
* Copyright (C) 2023 Ailef (http://ailef.tech)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package space.mori.dalbodeule.snapadmin.external;
|
package space.mori.dalbodeule.snapadmin.external;
|
||||||
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.boot.jdbc.DataSourceBuilder;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.ComponentScan;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
import org.springframework.orm.jpa.JpaTransactionManager;
|
import space.mori.dalbodeule.snapadmin.external.controller.DataExportController;
|
||||||
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
|
import space.mori.dalbodeule.snapadmin.external.controller.FileDownloadController;
|
||||||
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
|
import space.mori.dalbodeule.snapadmin.external.controller.GlobalController;
|
||||||
import org.springframework.transaction.PlatformTransactionManager;
|
import space.mori.dalbodeule.snapadmin.external.controller.SnapAdminController;
|
||||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
import space.mori.dalbodeule.snapadmin.external.controller.rest.AutocompleteController;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import space.mori.dalbodeule.snapadmin.external.dbmapping.CustomJpaRepository;
|
||||||
import org.springframework.transaction.support.TransactionTemplate;
|
import space.mori.dalbodeule.snapadmin.external.dbmapping.DbObjectSchema;
|
||||||
|
import space.mori.dalbodeule.snapadmin.external.dbmapping.SnapAdminRepository;
|
||||||
import space.mori.dalbodeule.snapadmin.internal.InternalSnapAdminConfiguration;
|
import space.mori.dalbodeule.snapadmin.internal.InternalSnapAdminConfiguration;
|
||||||
|
import space.mori.dalbodeule.snapadmin.internal.UserConfiguration;
|
||||||
|
import space.mori.dalbodeule.snapadmin.internal.service.ConsoleQueryService;
|
||||||
|
import space.mori.dalbodeule.snapadmin.internal.service.UserActionService;
|
||||||
|
import space.mori.dalbodeule.snapadmin.internal.service.UserSettingsService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The configuration class for "internal" data source. This is not the
|
* SnapAdmin 자동 설정 클래스. 메인 애플리케이션의 JPA 설정을 재사용합니다.
|
||||||
* source connected to the user's data/entities, but rather an internal
|
|
||||||
* H2 database which is used by SnapAdmin to store user
|
|
||||||
* settings and other information like operations history.
|
|
||||||
*/
|
*/
|
||||||
@ConditionalOnProperty(name = "snapadmin.enabled", matchIfMissing = false)
|
|
||||||
@ComponentScan
|
|
||||||
@EnableConfigurationProperties(SnapAdminProperties.class)
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableJpaRepositories(
|
@ConditionalOnProperty(name = "snapadmin.enabled", havingValue = "true", matchIfMissing = false)
|
||||||
entityManagerFactoryRef = "internalEntityManagerFactory",
|
@EnableConfigurationProperties(SnapAdminProperties.class)
|
||||||
basePackages = { "space.mori.dalbodeule.snapadmin.internal.repository" }
|
@EnableJpaRepositories(basePackages = "space.mori.dalbodeule.snapadmin.internal.repository")
|
||||||
)
|
@EntityScan(basePackages = "space.mori.dalbodeule.snapadmin.internal.model")
|
||||||
@EnableTransactionManagement
|
@Import({
|
||||||
@Import(InternalSnapAdminConfiguration.class)
|
SnapAdmin.class,
|
||||||
|
SnapAdminMvcConfig.class,
|
||||||
|
StartupAuthCheckRunner.class,
|
||||||
|
ThymeleafUtils.class,
|
||||||
|
|
||||||
|
// controllers
|
||||||
|
SnapAdminController.class,
|
||||||
|
DataExportController.class,
|
||||||
|
FileDownloadController.class,
|
||||||
|
GlobalController.class,
|
||||||
|
AutocompleteController.class,
|
||||||
|
|
||||||
|
// dbmapping
|
||||||
|
SnapAdminRepository.class,
|
||||||
|
|
||||||
|
// internals
|
||||||
|
ConsoleQueryService.class,
|
||||||
|
UserActionService.class,
|
||||||
|
UserSettingsService.class,
|
||||||
|
InternalSnapAdminConfiguration.class,
|
||||||
|
UserConfiguration.class
|
||||||
|
})
|
||||||
public class SnapAdminAutoConfiguration {
|
public class SnapAdminAutoConfiguration {
|
||||||
@Autowired
|
|
||||||
private SnapAdminProperties props;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds and returns the internal data source.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
DataSource internalDataSource() {
|
|
||||||
DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();
|
|
||||||
dataSourceBuilder.driverClassName("org.h2.Driver");
|
|
||||||
if (props.isTestMode()) {
|
|
||||||
dataSourceBuilder.url("jdbc:h2:mem:test");
|
|
||||||
} else {
|
|
||||||
dataSourceBuilder.url("jdbc:h2:file:./snapadmin_internal");
|
|
||||||
}
|
|
||||||
|
|
||||||
dataSourceBuilder.username("sa");
|
|
||||||
dataSourceBuilder.password("password");
|
|
||||||
return dataSourceBuilder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
LocalContainerEntityManagerFactoryBean internalEntityManagerFactory() {
|
|
||||||
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
|
|
||||||
factoryBean.setDataSource(internalDataSource());
|
|
||||||
factoryBean.setPersistenceUnitName("internal");
|
|
||||||
factoryBean.setPackagesToScan("space.mori.dalbodeule.snapadmin.internal.model");
|
|
||||||
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
|
|
||||||
Properties properties = new Properties();
|
|
||||||
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
|
|
||||||
properties.setProperty("hibernate.hbm2ddl.auto", "update");
|
|
||||||
factoryBean.setJpaProperties(properties);
|
|
||||||
factoryBean.afterPropertiesSet();
|
|
||||||
return factoryBean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The internal transaction manager. It is not defined as a bean
|
|
||||||
* in order to avoid "colliding" with the default transactionManager
|
|
||||||
* registered by the user. Internally, we use this to instantiate a
|
|
||||||
* TransactionTemplate and run all transactions manually instead of
|
|
||||||
* relying on the @link {@link Transactional} annotation.
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
PlatformTransactionManager internalTransactionManager() {
|
|
||||||
JpaTransactionManager transactionManager = new JpaTransactionManager();
|
|
||||||
transactionManager.setEntityManagerFactory(internalEntityManagerFactory().getObject());
|
|
||||||
return transactionManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
TransactionTemplate internalTransactionTemplate() {
|
|
||||||
return new TransactionTemplate(internalTransactionManager());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@@ -22,12 +22,10 @@ package space.mori.dalbodeule.snapadmin.external;
|
|||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebMvc
|
|
||||||
public class SnapAdminMvcConfig implements WebMvcConfigurer {
|
public class SnapAdminMvcConfig implements WebMvcConfigurer {
|
||||||
@Autowired
|
@Autowired
|
||||||
private SnapAdminProperties properties;
|
private SnapAdminProperties properties;
|
||||||
|
@@ -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 {
|
||||||
|
}
|
14
src/main/java/space/mori/dalbodeule/snapadmin/external/annotations/SnapAdminEnabled.java
vendored
Normal file
14
src/main/java/space/mori/dalbodeule/snapadmin/external/annotations/SnapAdminEnabled.java
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package space.mori.dalbodeule.snapadmin.external.annotations;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import space.mori.dalbodeule.snapadmin.external.SnapAdminAutoConfiguration;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
@Import(SnapAdminAutoConfiguration.class) // SnapAdmin 설정 클래스를 Import
|
||||||
|
public @interface SnapAdminEnabled {
|
||||||
|
// 필요한 속성이 있다면 정의
|
||||||
|
}
|
@@ -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;
|
||||||
|
@@ -35,6 +35,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.ManyToMany;
|
import jakarta.persistence.ManyToMany;
|
||||||
import jakarta.persistence.OneToMany;
|
import jakarta.persistence.OneToMany;
|
||||||
import jakarta.persistence.OneToOne;
|
import jakarta.persistence.OneToOne;
|
||||||
@@ -43,7 +44,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;
|
||||||
@@ -99,6 +100,10 @@ public class DbObjectSchema {
|
|||||||
* @param snapAdmin the SnapAdmin instance
|
* @param snapAdmin the SnapAdmin instance
|
||||||
*/
|
*/
|
||||||
public DbObjectSchema(Class<?> klass, SnapAdmin snapAdmin) {
|
public DbObjectSchema(Class<?> klass, SnapAdmin snapAdmin) {
|
||||||
|
if (klass.getAnnotation(Entity.class) == null) {
|
||||||
|
throw new SnapAdminException("Class " + klass.getName() + " is not an @Entity");
|
||||||
|
}
|
||||||
|
|
||||||
this.snapAdmin = snapAdmin;
|
this.snapAdmin = snapAdmin;
|
||||||
this.entityClass = klass;
|
this.entityClass = klass;
|
||||||
|
|
||||||
@@ -351,7 +356,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,6 +88,8 @@ 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
|
||||||
*/
|
*/
|
||||||
@@ -190,6 +192,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,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);
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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
|
||||||
|
@@ -22,19 +22,20 @@ package space.mori.dalbodeule.snapadmin.internal.model;
|
|||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
import org.hibernate.annotations.UuidGenerator;
|
import org.hibernate.annotations.UuidGenerator;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import space.mori.dalbodeule.snapadmin.external.annotations.Disable;
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.Lob;
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Disable
|
||||||
|
@Table(name="snapadmin_console_query")
|
||||||
public class ConsoleQuery {
|
public class ConsoleQuery {
|
||||||
@Id
|
@Id
|
||||||
@UuidGenerator
|
@UuidGenerator
|
||||||
private String id;
|
private String id;
|
||||||
|
|
||||||
@Lob
|
@Column(columnDefinition = "TEXT")
|
||||||
private String sql;
|
private String sql;
|
||||||
|
|
||||||
private String title;
|
private String title;
|
||||||
|
@@ -21,14 +21,10 @@ package space.mori.dalbodeule.snapadmin.internal.model;
|
|||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
import org.springframework.format.datetime.standard.DateTimeFormatterFactory;
|
import org.springframework.format.datetime.standard.DateTimeFormatterFactory;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import space.mori.dalbodeule.snapadmin.external.annotations.Disable;
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.GenerationType;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.Lob;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An write operation executed by a user from the web UI. This class
|
* An write operation executed by a user from the web UI. This class
|
||||||
@@ -36,6 +32,8 @@ import jakarta.persistence.Lob;
|
|||||||
* concrete yet (e.g. a diff or SQL query) about what change was performed.
|
* concrete yet (e.g. a diff or SQL query) about what change was performed.
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
|
@Disable
|
||||||
|
@Table(name="snapadmin_user_action")
|
||||||
public class UserAction {
|
public class UserAction {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
@@ -51,8 +49,7 @@ public class UserAction {
|
|||||||
* The SQL query generated by the operation.
|
* The SQL query generated by the operation.
|
||||||
* This field is here but it's NOT currently supported
|
* This field is here but it's NOT currently supported
|
||||||
*/
|
*/
|
||||||
@Lob
|
@Column(nullable = false, columnDefinition = "TEXT")
|
||||||
@Column(nullable = false)
|
|
||||||
private String sql;
|
private String sql;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -21,11 +21,15 @@ package space.mori.dalbodeule.snapadmin.internal.model;
|
|||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import space.mori.dalbodeule.snapadmin.external.annotations.Disable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A single variable in the user settings.
|
* A single variable in the user settings.
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
|
@Disable
|
||||||
|
@Table(name="snapadmin_user_setting")
|
||||||
public class UserSetting {
|
public class UserSetting {
|
||||||
/**
|
/**
|
||||||
* The id of the variable (its name)
|
* The id of the variable (its name)
|
||||||
|
@@ -39,8 +39,6 @@ import space.mori.dalbodeule.snapadmin.internal.model.UserAction;
|
|||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class CustomActionRepositoryImpl implements CustomActionRepository {
|
public class CustomActionRepositoryImpl implements CustomActionRepository {
|
||||||
|
|
||||||
@PersistenceContext(unitName = "internal")
|
|
||||||
private EntityManager entityManager;
|
private EntityManager entityManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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) {
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
space.mori.dalbodeule.snapadmin.aot.SnapAdminRuntimeHints=space.mori.dalbodeule.snapadmin.aot.SnapAdminRuntimeHints
|
@@ -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 } )}">
|
||||||
|
Reference in New Issue
Block a user