diff --git a/.gitignore b/.gitignore index 46c5936..8968de5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,50 +1,31 @@ -HELP.md -.gradle +# Build build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ - -### STS ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache -bin/ -!**/src/main/**/bin/ -!**/src/test/**/bin/ - -### IntelliJ IDEA ### -.idea -*.iws -*.iml -*.ipr +dist/ +.gradle/ out/ -!**/src/main/**/out/ -!**/src/test/**/out/ -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ - -### VS Code ### +# IDE +.idea/ +*.iml .vscode/ +*.swp -### Runtime ### -*.log -server.log -uploads/ -temp/ +# Node +node_modules/ +wtm-frontend/node_modules/ -### Environment ### -.env +# OS +.DS_Store +Thumbs.db + +# Env (local overrides) .env.local -*.pem -*.key +.env.*.local + +# Claude/OMC .claude/ +.omc/ + +# Logs +*.log +logs/ diff --git a/build.gradle b/build.gradle index 37d43f0..93b113c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,70 +1,39 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.5.0' - id 'io.spring.dependency-management' version '1.1.7' + id 'java' + id 'io.spring.dependency-management' version '1.1.7' } -group = 'kr.co.accura.wbx.spring' -version = '0.0.1-SNAPSHOT' +subprojects { + group = 'kr.co.accura' + version = '1.0.0-SNAPSHOT' -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } -} - -configurations { - compileOnly { - extendsFrom annotationProcessor - } -} - -repositories { - mavenCentral() -} - -dependencies { - implementation 'org.springframework.boot:spring-boot-starter-actuator' - runtimeOnly 'io.micrometer:micrometer-registry-prometheus' - implementation 'org.springframework.boot:spring-boot-starter-cache' - 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-oauth2-client' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.flywaydb:flyway-core' - implementation 'org.flywaydb:flyway-mysql' - - // JWT - implementation 'io.jsonwebtoken:jjwt-api:0.12.6' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' - - // DB Drivers (4개 DBMS) - runtimeOnly 'com.mysql:mysql-connector-j' - runtimeOnly 'org.postgresql:postgresql' - runtimeOnly 'com.oracle.database.jdbc:ojdbc11:23.6.0.24.10' - runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11' - runtimeOnly 'org.flywaydb:flyway-sqlserver' - - // OpenAPI - // SpringDoc — Boot 3.5 호환 - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6' - - // Admin Console (Thymeleaf) - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - - compileOnly 'org.projectlombok:lombok' - developmentOnly 'org.springframework.boot:spring-boot-devtools' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - testRuntimeOnly 'com.h2database:h2' -} - -tasks.named('test') { - useJUnitPlatform() + apply plugin: 'java' + apply plugin: 'io.spring.dependency-management' + + java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + } + + repositories { + mavenCentral() + } + + dependencyManagement { + imports { + mavenBom "org.springframework.boot:spring-boot-dependencies:3.5.0" + } + } + + dependencies { + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + } + + tasks.named('test') { + useJUnitPlatform() + } } diff --git a/settings.gradle b/settings.gradle index 7d4e6ef..3a84400 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,9 @@ -rootProject.name = 'wbx-spring-core' +rootProject.name = 'wbx-spring' + +// 공유 프레임워크 +include 'wbx-spring-core' + +// === 고객 프로젝트 (추가 시 여기에 include) === +include 'wtm-api' // 한화오션 WTM (시수관리) +// include 'xxx-api' // 향후 프로젝트 B +// include 'yyy-api' // 향후 프로젝트 C diff --git a/src/main/java/kr/co/accura/wbx/spring/WbxSpringCoreApplication.java b/src/main/java/kr/co/accura/wbx/spring/WbxSpringCoreApplication.java deleted file mode 100644 index 1f893d3..0000000 --- a/src/main/java/kr/co/accura/wbx/spring/WbxSpringCoreApplication.java +++ /dev/null @@ -1,20 +0,0 @@ -package kr.co.accura.wbx.spring; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.annotation.EnableScheduling; - -@SpringBootApplication -@EnableJpaAuditing -@EnableAsync -@EnableScheduling -@EnableCaching -public class WbxSpringCoreApplication { - - public static void main(String[] args) { - SpringApplication.run(WbxSpringCoreApplication.class, args); - } -} diff --git a/.dockerignore b/wbx-spring-core/.dockerignore similarity index 100% rename from .dockerignore rename to wbx-spring-core/.dockerignore diff --git a/.editorconfig b/wbx-spring-core/.editorconfig similarity index 100% rename from .editorconfig rename to wbx-spring-core/.editorconfig diff --git a/.env b/wbx-spring-core/.env similarity index 100% rename from .env rename to wbx-spring-core/.env diff --git a/.gitattributes b/wbx-spring-core/.gitattributes similarity index 100% rename from .gitattributes rename to wbx-spring-core/.gitattributes diff --git a/.github/workflows/ci.yml b/wbx-spring-core/.github/workflows/ci.yml similarity index 100% rename from .github/workflows/ci.yml rename to wbx-spring-core/.github/workflows/ci.yml diff --git a/wbx-spring-core/.gitignore b/wbx-spring-core/.gitignore new file mode 100644 index 0000000..46c5936 --- /dev/null +++ b/wbx-spring-core/.gitignore @@ -0,0 +1,50 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Runtime ### +*.log +server.log +uploads/ +temp/ + +### Environment ### +.env +.env.local +*.pem +*.key +.claude/ diff --git a/wbx-spring-core/.gradle/8.14.4/checksums/checksums.lock b/wbx-spring-core/.gradle/8.14.4/checksums/checksums.lock new file mode 100644 index 0000000..50e7644 Binary files /dev/null and b/wbx-spring-core/.gradle/8.14.4/checksums/checksums.lock differ diff --git a/wbx-spring-core/.gradle/8.14.4/checksums/md5-checksums.bin b/wbx-spring-core/.gradle/8.14.4/checksums/md5-checksums.bin new file mode 100644 index 0000000..5211e80 Binary files /dev/null and b/wbx-spring-core/.gradle/8.14.4/checksums/md5-checksums.bin differ diff --git a/wbx-spring-core/.gradle/8.14.4/checksums/sha1-checksums.bin b/wbx-spring-core/.gradle/8.14.4/checksums/sha1-checksums.bin new file mode 100644 index 0000000..940caf4 Binary files /dev/null and b/wbx-spring-core/.gradle/8.14.4/checksums/sha1-checksums.bin differ diff --git a/wbx-spring-core/.gradle/8.14.4/executionHistory/executionHistory.bin b/wbx-spring-core/.gradle/8.14.4/executionHistory/executionHistory.bin new file mode 100644 index 0000000..542da4b Binary files /dev/null and b/wbx-spring-core/.gradle/8.14.4/executionHistory/executionHistory.bin differ diff --git a/wbx-spring-core/.gradle/8.14.4/executionHistory/executionHistory.lock b/wbx-spring-core/.gradle/8.14.4/executionHistory/executionHistory.lock new file mode 100644 index 0000000..8010d2e Binary files /dev/null and b/wbx-spring-core/.gradle/8.14.4/executionHistory/executionHistory.lock differ diff --git a/wbx-spring-core/.gradle/8.14.4/fileChanges/last-build.bin b/wbx-spring-core/.gradle/8.14.4/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/wbx-spring-core/.gradle/8.14.4/fileChanges/last-build.bin differ diff --git a/wbx-spring-core/.gradle/8.14.4/fileHashes/fileHashes.bin b/wbx-spring-core/.gradle/8.14.4/fileHashes/fileHashes.bin new file mode 100644 index 0000000..07767ea Binary files /dev/null and b/wbx-spring-core/.gradle/8.14.4/fileHashes/fileHashes.bin differ diff --git a/wbx-spring-core/.gradle/8.14.4/fileHashes/fileHashes.lock b/wbx-spring-core/.gradle/8.14.4/fileHashes/fileHashes.lock new file mode 100644 index 0000000..22565f8 Binary files /dev/null and b/wbx-spring-core/.gradle/8.14.4/fileHashes/fileHashes.lock differ diff --git a/wbx-spring-core/.gradle/8.14.4/fileHashes/resourceHashesCache.bin b/wbx-spring-core/.gradle/8.14.4/fileHashes/resourceHashesCache.bin new file mode 100644 index 0000000..dfe5469 Binary files /dev/null and b/wbx-spring-core/.gradle/8.14.4/fileHashes/resourceHashesCache.bin differ diff --git a/wbx-spring-core/.gradle/8.14.4/gc.properties b/wbx-spring-core/.gradle/8.14.4/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/wbx-spring-core/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/wbx-spring-core/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..580b924 Binary files /dev/null and b/wbx-spring-core/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/wbx-spring-core/.gradle/buildOutputCleanup/cache.properties b/wbx-spring-core/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..e4487bf --- /dev/null +++ b/wbx-spring-core/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Wed Mar 25 16:49:13 KST 2026 +gradle.version=8.14.4 diff --git a/wbx-spring-core/.gradle/buildOutputCleanup/outputFiles.bin b/wbx-spring-core/.gradle/buildOutputCleanup/outputFiles.bin new file mode 100644 index 0000000..65708c5 Binary files /dev/null and b/wbx-spring-core/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/wbx-spring-core/.gradle/vcs-1/gc.properties b/wbx-spring-core/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/wbx-spring-core/.omc/state/agent-replay-6666f085-e1ae-4dd4-86ee-7f7d5466a239.jsonl b/wbx-spring-core/.omc/state/agent-replay-6666f085-e1ae-4dd4-86ee-7f7d5466a239.jsonl new file mode 100644 index 0000000..38868f2 --- /dev/null +++ b/wbx-spring-core/.omc/state/agent-replay-6666f085-e1ae-4dd4-86ee-7f7d5466a239.jsonl @@ -0,0 +1,12 @@ +{"t":0,"agent":"a18f090","agent_type":"Explore","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"a18f090","agent_type":"Explore","event":"agent_stop","success":true,"duration_ms":44564} +{"t":0,"agent":"ad9b656","agent_type":"architect","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"ad9b656","agent_type":"architect","event":"agent_stop","success":true,"duration_ms":169049} +{"t":0,"agent":"ad7c101","agent_type":"architect","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"ad7c101","agent_type":"architect","event":"agent_stop","success":true,"duration_ms":202690} +{"t":0,"agent":"aeff378","agent_type":"critic","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"aeff378","agent_type":"critic","event":"agent_stop","success":true,"duration_ms":175651} +{"t":0,"agent":"a35d81f","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"a66b53d","agent_type":"general-purpose","event":"agent_start","parent_mode":"none"} +{"t":0,"agent":"a35d81f","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":105638} +{"t":0,"agent":"a66b53d","agent_type":"general-purpose","event":"agent_stop","success":true,"duration_ms":411042} diff --git a/wbx-spring-core/.omc/state/subagent-tracking.json b/wbx-spring-core/.omc/state/subagent-tracking.json new file mode 100644 index 0000000..0b209b2 --- /dev/null +++ b/wbx-spring-core/.omc/state/subagent-tracking.json @@ -0,0 +1,62 @@ +{ + "agents": [ + { + "agent_id": "a18f0906df8444921", + "agent_type": "Explore", + "started_at": "2026-03-25T09:57:59.104Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-03-25T09:58:43.668Z", + "duration_ms": 44564 + }, + { + "agent_id": "ad9b65686f679467a", + "agent_type": "oh-my-claudecode:architect", + "started_at": "2026-03-25T10:12:41.765Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-03-25T10:15:30.814Z", + "duration_ms": 169049 + }, + { + "agent_id": "ad7c101944473c52f", + "agent_type": "oh-my-claudecode:architect", + "started_at": "2026-03-25T10:20:25.037Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-03-25T10:23:47.727Z", + "duration_ms": 202690 + }, + { + "agent_id": "aeff378642946b837", + "agent_type": "oh-my-claudecode:critic", + "started_at": "2026-03-25T10:32:26.548Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-03-25T10:35:22.199Z", + "duration_ms": 175651 + }, + { + "agent_id": "a35d81fe5c1491345", + "agent_type": "general-purpose", + "started_at": "2026-03-25T10:54:19.348Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-03-25T10:56:04.986Z", + "duration_ms": 105638 + }, + { + "agent_id": "a66b53d0c9bd248ef", + "agent_type": "general-purpose", + "started_at": "2026-03-25T10:54:45.602Z", + "parent_mode": "none", + "status": "completed", + "completed_at": "2026-03-25T11:01:36.644Z", + "duration_ms": 411042 + } + ], + "total_spawned": 6, + "total_completed": 6, + "total_failed": 0, + "last_updated": "2026-03-25T11:01:36.764Z" +} \ No newline at end of file diff --git a/Dockerfile b/wbx-spring-core/Dockerfile similarity index 100% rename from Dockerfile rename to wbx-spring-core/Dockerfile diff --git a/HanwhaOCN/wtmgr/00-overview.md b/wbx-spring-core/HanwhaOCN/wtmgr/00-overview.md similarity index 100% rename from HanwhaOCN/wtmgr/00-overview.md rename to wbx-spring-core/HanwhaOCN/wtmgr/00-overview.md diff --git a/HanwhaOCN/wtmgr/01-architecture.md b/wbx-spring-core/HanwhaOCN/wtmgr/01-architecture.md similarity index 100% rename from HanwhaOCN/wtmgr/01-architecture.md rename to wbx-spring-core/HanwhaOCN/wtmgr/01-architecture.md diff --git a/HanwhaOCN/wtmgr/02-database-schema.md b/wbx-spring-core/HanwhaOCN/wtmgr/02-database-schema.md similarity index 100% rename from HanwhaOCN/wtmgr/02-database-schema.md rename to wbx-spring-core/HanwhaOCN/wtmgr/02-database-schema.md diff --git a/HanwhaOCN/wtmgr/03-timesheet-module.md b/wbx-spring-core/HanwhaOCN/wtmgr/03-timesheet-module.md similarity index 100% rename from HanwhaOCN/wtmgr/03-timesheet-module.md rename to wbx-spring-core/HanwhaOCN/wtmgr/03-timesheet-module.md diff --git a/HanwhaOCN/wtmgr/04-wbs-teal-module.md b/wbx-spring-core/HanwhaOCN/wtmgr/04-wbs-teal-module.md similarity index 100% rename from HanwhaOCN/wtmgr/04-wbs-teal-module.md rename to wbx-spring-core/HanwhaOCN/wtmgr/04-wbs-teal-module.md diff --git a/HanwhaOCN/wtmgr/05-approval-handlers.md b/wbx-spring-core/HanwhaOCN/wtmgr/05-approval-handlers.md similarity index 100% rename from HanwhaOCN/wtmgr/05-approval-handlers.md rename to wbx-spring-core/HanwhaOCN/wtmgr/05-approval-handlers.md diff --git a/HanwhaOCN/wtmgr/06-reporting-module.md b/wbx-spring-core/HanwhaOCN/wtmgr/06-reporting-module.md similarity index 100% rename from HanwhaOCN/wtmgr/06-reporting-module.md rename to wbx-spring-core/HanwhaOCN/wtmgr/06-reporting-module.md diff --git a/HanwhaOCN/wtmgr/07-api-spec.md b/wbx-spring-core/HanwhaOCN/wtmgr/07-api-spec.md similarity index 100% rename from HanwhaOCN/wtmgr/07-api-spec.md rename to wbx-spring-core/HanwhaOCN/wtmgr/07-api-spec.md diff --git a/HanwhaOCN/wtmgr/08-sap-btp-integration.md b/wbx-spring-core/HanwhaOCN/wtmgr/08-sap-btp-integration.md similarity index 100% rename from HanwhaOCN/wtmgr/08-sap-btp-integration.md rename to wbx-spring-core/HanwhaOCN/wtmgr/08-sap-btp-integration.md diff --git a/HanwhaOCN/wtmgr/09-devops-infra.md b/wbx-spring-core/HanwhaOCN/wtmgr/09-devops-infra.md similarity index 100% rename from HanwhaOCN/wtmgr/09-devops-infra.md rename to wbx-spring-core/HanwhaOCN/wtmgr/09-devops-infra.md diff --git a/HanwhaOCN/wtmgr/10-schedule-milestones.md b/wbx-spring-core/HanwhaOCN/wtmgr/10-schedule-milestones.md similarity index 100% rename from HanwhaOCN/wtmgr/10-schedule-milestones.md rename to wbx-spring-core/HanwhaOCN/wtmgr/10-schedule-milestones.md diff --git a/HanwhaOCN/wtmgr/11-requirements-traceability.md b/wbx-spring-core/HanwhaOCN/wtmgr/11-requirements-traceability.md similarity index 100% rename from HanwhaOCN/wtmgr/11-requirements-traceability.md rename to wbx-spring-core/HanwhaOCN/wtmgr/11-requirements-traceability.md diff --git a/HanwhaOCN/wtmgr/12-project-setup-plan.md b/wbx-spring-core/HanwhaOCN/wtmgr/12-project-setup-plan.md similarity index 100% rename from HanwhaOCN/wtmgr/12-project-setup-plan.md rename to wbx-spring-core/HanwhaOCN/wtmgr/12-project-setup-plan.md diff --git a/HanwhaOCN/wtmgr/13-frontend-setup-plan.md b/wbx-spring-core/HanwhaOCN/wtmgr/13-frontend-setup-plan.md similarity index 100% rename from HanwhaOCN/wtmgr/13-frontend-setup-plan.md rename to wbx-spring-core/HanwhaOCN/wtmgr/13-frontend-setup-plan.md diff --git a/HanwhaOCN/wtmgr/14-layout-standard.md b/wbx-spring-core/HanwhaOCN/wtmgr/14-layout-standard.md similarity index 100% rename from HanwhaOCN/wtmgr/14-layout-standard.md rename to wbx-spring-core/HanwhaOCN/wtmgr/14-layout-standard.md diff --git a/wbx-spring-core/build.gradle b/wbx-spring-core/build.gradle new file mode 100644 index 0000000..d126c33 --- /dev/null +++ b/wbx-spring-core/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'java-library' +} + +dependencies { + // Spring Boot Starters (api로 노출 — 소비 모듈이 사용) + api 'org.springframework.boot:spring-boot-starter-web' + api 'org.springframework.boot:spring-boot-starter-data-jpa' + api 'org.springframework.boot:spring-boot-starter-security' + api 'org.springframework.boot:spring-boot-starter-validation' + api 'org.springframework.boot:spring-boot-starter-data-redis' + api 'org.springframework.boot:spring-boot-starter-cache' + api 'org.springframework.boot:spring-boot-starter-actuator' + api 'org.springframework.boot:spring-boot-starter-oauth2-client' + api 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + + // JWT + api 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + // OpenAPI + api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6' + + // Admin Console (조건부) + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + + // Flyway (소비 모듈이 DBMS별 추가) + api 'org.flywaydb:flyway-core' + + // DB 드라이버 — compileOnly (소비 모듈이 runtimeOnly로 선택) + compileOnly 'com.mysql:mysql-connector-j' + compileOnly 'org.postgresql:postgresql' + compileOnly 'com.oracle.database.jdbc:ojdbc11:23.6.0.24.10' + compileOnly 'com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11' + + // Micrometer + runtimeOnly 'io.micrometer:micrometer-registry-prometheus' + + // Test + testRuntimeOnly 'com.h2database:h2' +} diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/HealthController.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/HealthController.class new file mode 100644 index 0000000..ac55be2 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/HealthController.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/AdminAutoConfiguration.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/AdminAutoConfiguration.class new file mode 100644 index 0000000..0cb35b0 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/AdminAutoConfiguration.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/AdminController.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/AdminController.class new file mode 100644 index 0000000..aebe36a Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/AdminController.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/AdminLoginController.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/AdminLoginController.class new file mode 100644 index 0000000..e886a36 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/AdminLoginController.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/AdminUserDetailsService.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/AdminUserDetailsService.class new file mode 100644 index 0000000..4c764a9 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/AdminUserDetailsService.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/AdminViewController.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/AdminViewController.class new file mode 100644 index 0000000..e81823a Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/AdminViewController.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/WbxRoleRepository.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/WbxRoleRepository.class new file mode 100644 index 0000000..77f5067 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/WbxRoleRepository.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/WbxSystemConfigRepository.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/WbxSystemConfigRepository.class new file mode 100644 index 0000000..45bf35a Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/admin/WbxSystemConfigRepository.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ActionRequest.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ActionRequest.class new file mode 100644 index 0000000..06edb17 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ActionRequest.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalCompletedEvent.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalCompletedEvent.class new file mode 100644 index 0000000..9fbad4a Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalCompletedEvent.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalHandler.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalHandler.class new file mode 100644 index 0000000..31d25df Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalHandler.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalHandlerRegistry.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalHandlerRegistry.class new file mode 100644 index 0000000..69ae338 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalHandlerRegistry.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalHistoryDto.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalHistoryDto.class new file mode 100644 index 0000000..c2eff08 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalHistoryDto.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalLineDto.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalLineDto.class new file mode 100644 index 0000000..0e27348 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalLineDto.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalPendingDto.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalPendingDto.class new file mode 100644 index 0000000..4ef2736 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalPendingDto.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalResult.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalResult.class new file mode 100644 index 0000000..4e41a71 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/ApprovalResult.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/UnifiedApprovalController.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/UnifiedApprovalController.class new file mode 100644 index 0000000..54b96fb Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/approval/UnifiedApprovalController.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/audit/AuditLogRepository.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/audit/AuditLogRepository.class new file mode 100644 index 0000000..7ae8354 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/audit/AuditLogRepository.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/audit/AuditLogService.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/audit/AuditLogService.class new file mode 100644 index 0000000..4e44c3f Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/audit/AuditLogService.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/audit/WbxAuditLog$WbxAuditLogBuilder.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/audit/WbxAuditLog$WbxAuditLogBuilder.class new file mode 100644 index 0000000..164a299 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/audit/WbxAuditLog$WbxAuditLogBuilder.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/audit/WbxAuditLog.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/audit/WbxAuditLog.class new file mode 100644 index 0000000..4fc5fdd Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/audit/WbxAuditLog.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/ApiKeyFilter.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/ApiKeyFilter.class new file mode 100644 index 0000000..4abe556 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/ApiKeyFilter.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/AuthController$LoginRequest.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/AuthController$LoginRequest.class new file mode 100644 index 0000000..7339718 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/AuthController$LoginRequest.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/AuthController$MfaVerifyRequest.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/AuthController$MfaVerifyRequest.class new file mode 100644 index 0000000..6d47f45 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/AuthController$MfaVerifyRequest.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/AuthController$PasswordChangeRequest.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/AuthController$PasswordChangeRequest.class new file mode 100644 index 0000000..da67bcc Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/AuthController$PasswordChangeRequest.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/AuthController$RegisterRequest.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/AuthController$RegisterRequest.class new file mode 100644 index 0000000..086df86 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/AuthController$RegisterRequest.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/AuthController.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/AuthController.class new file mode 100644 index 0000000..d148a57 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/AuthController.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/JwtFilter.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/JwtFilter.class new file mode 100644 index 0000000..ed7a3eb Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/JwtFilter.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/JwtProvider.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/JwtProvider.class new file mode 100644 index 0000000..6be1382 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/JwtProvider.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/LoginHistoryRepository.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/LoginHistoryRepository.class new file mode 100644 index 0000000..cf9cd6a Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/LoginHistoryRepository.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/MfaController$CodeRequest.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/MfaController$CodeRequest.class new file mode 100644 index 0000000..6275958 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/MfaController$CodeRequest.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/MfaController.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/MfaController.class new file mode 100644 index 0000000..3d632e9 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/MfaController.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/MfaService$1.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/MfaService$1.class new file mode 100644 index 0000000..f652bcc Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/MfaService$1.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/MfaService.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/MfaService.class new file mode 100644 index 0000000..720fc53 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/MfaService.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/PasswordPolicy.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/PasswordPolicy.class new file mode 100644 index 0000000..3b38a5f Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/PasswordPolicy.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/RefreshTokenRepository.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/RefreshTokenRepository.class new file mode 100644 index 0000000..d1cb455 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/RefreshTokenRepository.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/RefreshTokenService.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/RefreshTokenService.class new file mode 100644 index 0000000..b54608e Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/RefreshTokenService.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/SsoSuccessHandler.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/SsoSuccessHandler.class new file mode 100644 index 0000000..8b97f24 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/SsoSuccessHandler.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/TotpSecretRepository.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/TotpSecretRepository.class new file mode 100644 index 0000000..dce4e17 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/TotpSecretRepository.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxLoginHistory$WbxLoginHistoryBuilder.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxLoginHistory$WbxLoginHistoryBuilder.class new file mode 100644 index 0000000..88fe22b Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxLoginHistory$WbxLoginHistoryBuilder.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxLoginHistory.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxLoginHistory.class new file mode 100644 index 0000000..b8f512d Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxLoginHistory.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxRefreshToken$WbxRefreshTokenBuilder.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxRefreshToken$WbxRefreshTokenBuilder.class new file mode 100644 index 0000000..e10cbd9 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxRefreshToken$WbxRefreshTokenBuilder.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxRefreshToken.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxRefreshToken.class new file mode 100644 index 0000000..c03aafa Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxRefreshToken.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxTotpSecret$WbxTotpSecretBuilder.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxTotpSecret$WbxTotpSecretBuilder.class new file mode 100644 index 0000000..b7ffc81 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxTotpSecret$WbxTotpSecretBuilder.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxTotpSecret.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxTotpSecret.class new file mode 100644 index 0000000..e94c0e0 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxTotpSecret.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxUser$WbxUserBuilder.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxUser$WbxUserBuilder.class new file mode 100644 index 0000000..fd9d19b Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxUser$WbxUserBuilder.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxUser.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxUser.class new file mode 100644 index 0000000..8e0f1d9 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxUser.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxUserDetails$WbxUserDetailsBuilder.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxUserDetails$WbxUserDetailsBuilder.class new file mode 100644 index 0000000..36dd93c Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxUserDetails$WbxUserDetailsBuilder.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxUserDetails.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxUserDetails.class new file mode 100644 index 0000000..be0007c Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxUserDetails.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxUserRepository.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxUserRepository.class new file mode 100644 index 0000000..5f5d6dc Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/auth/WbxUserRepository.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/common/BaseEntity.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/common/BaseEntity.class new file mode 100644 index 0000000..6aaf52c Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/common/BaseEntity.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/common/BusinessException.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/common/BusinessException.class new file mode 100644 index 0000000..269ea25 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/common/BusinessException.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/common/NotFoundException.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/common/NotFoundException.class new file mode 100644 index 0000000..2cff4be Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/common/NotFoundException.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/common/SecurityUtils.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/common/SecurityUtils.class new file mode 100644 index 0000000..66a29fd Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/common/SecurityUtils.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/compat/WbxErrorHandler.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/compat/WbxErrorHandler.class new file mode 100644 index 0000000..1e210b0 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/compat/WbxErrorHandler.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/compat/WbxPaginationConfig$WbxPaginationResolver.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/compat/WbxPaginationConfig$WbxPaginationResolver.class new file mode 100644 index 0000000..9e0d4da Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/compat/WbxPaginationConfig$WbxPaginationResolver.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/compat/WbxPaginationConfig.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/compat/WbxPaginationConfig.class new file mode 100644 index 0000000..68fd183 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/compat/WbxPaginationConfig.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/CorsAutoConfig.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/CorsAutoConfig.class new file mode 100644 index 0000000..b4a6a5e Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/CorsAutoConfig.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/OpenApiConfig.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/OpenApiConfig.class new file mode 100644 index 0000000..9ec0b62 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/OpenApiConfig.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/SecurityAutoConfig.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/SecurityAutoConfig.class new file mode 100644 index 0000000..30fa45a Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/SecurityAutoConfig.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxAutoConfiguration.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxAutoConfiguration.class new file mode 100644 index 0000000..e38c002 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxAutoConfiguration.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$AdminUi.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$AdminUi.class new file mode 100644 index 0000000..f5a870c Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$AdminUi.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Approval.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Approval.class new file mode 100644 index 0000000..1832224 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Approval.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Compat.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Compat.class new file mode 100644 index 0000000..f0e6c3e Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Compat.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Cors.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Cors.class new file mode 100644 index 0000000..e0358af Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Cors.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$DataSourceConfig.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$DataSourceConfig.class new file mode 100644 index 0000000..e28fcef Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$DataSourceConfig.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$FileConfig$AwsConfig.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$FileConfig$AwsConfig.class new file mode 100644 index 0000000..390a085 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$FileConfig$AwsConfig.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$FileConfig$AzureConfig.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$FileConfig$AzureConfig.class new file mode 100644 index 0000000..cbdf17e Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$FileConfig$AzureConfig.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$FileConfig$GcpConfig.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$FileConfig$GcpConfig.class new file mode 100644 index 0000000..9d44ea0 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$FileConfig$GcpConfig.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$FileConfig.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$FileConfig.class new file mode 100644 index 0000000..69710f6 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$FileConfig.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Jwt.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Jwt.class new file mode 100644 index 0000000..9ac1699 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Jwt.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Mfa.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Mfa.class new file mode 100644 index 0000000..1bf2c21 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Mfa.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Notification.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Notification.class new file mode 100644 index 0000000..253f3ba Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Notification.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Password.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Password.class new file mode 100644 index 0000000..bc01964 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties$Password.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties.class new file mode 100644 index 0000000..36cacf5 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSpringProperties.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSystemConfig$WbxSystemConfigBuilder.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSystemConfig$WbxSystemConfigBuilder.class new file mode 100644 index 0000000..7eca708 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSystemConfig$WbxSystemConfigBuilder.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSystemConfig.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSystemConfig.class new file mode 100644 index 0000000..d89f41a Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/config/WbxSystemConfig.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/datasource/DataSource.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/datasource/DataSource.class new file mode 100644 index 0000000..d812f0a Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/datasource/DataSource.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/datasource/DataSourceAspect.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/datasource/DataSourceAspect.class new file mode 100644 index 0000000..1e86b62 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/datasource/DataSourceAspect.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/datasource/MultiDataSourceConfig.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/datasource/MultiDataSourceConfig.class new file mode 100644 index 0000000..a23f04d Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/datasource/MultiDataSourceConfig.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/datasource/WbxRoutingDataSource.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/datasource/WbxRoutingDataSource.class new file mode 100644 index 0000000..1bd6d56 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/datasource/WbxRoutingDataSource.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/AwsS3StorageService.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/AwsS3StorageService.class new file mode 100644 index 0000000..4d37ed6 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/AwsS3StorageService.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/AzureBlobStorageService.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/AzureBlobStorageService.class new file mode 100644 index 0000000..32641cf Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/AzureBlobStorageService.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/FileStorageService.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/FileStorageService.class new file mode 100644 index 0000000..42793e0 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/FileStorageService.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/GcpStorageService.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/GcpStorageService.class new file mode 100644 index 0000000..3aa4d94 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/GcpStorageService.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/LocalFileStorageService.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/LocalFileStorageService.class new file mode 100644 index 0000000..2f4a332 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/LocalFileStorageService.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/WbxFileUpload$WbxFileUploadBuilder.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/WbxFileUpload$WbxFileUploadBuilder.class new file mode 100644 index 0000000..66cd857 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/WbxFileUpload$WbxFileUploadBuilder.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/WbxFileUpload.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/WbxFileUpload.class new file mode 100644 index 0000000..2cca5dc Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/file/WbxFileUpload.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/Notification$NotificationBuilder.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/Notification$NotificationBuilder.class new file mode 100644 index 0000000..2d4e5fb Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/Notification$NotificationBuilder.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/Notification.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/Notification.class new file mode 100644 index 0000000..501e040 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/Notification.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/NotificationController.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/NotificationController.class new file mode 100644 index 0000000..69af3eb Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/NotificationController.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/NotificationDto$NotificationDtoBuilder.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/NotificationDto$NotificationDtoBuilder.class new file mode 100644 index 0000000..58334a8 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/NotificationDto$NotificationDtoBuilder.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/NotificationDto.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/NotificationDto.class new file mode 100644 index 0000000..7eb99f3 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/NotificationDto.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/SseNotificationService.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/SseNotificationService.class new file mode 100644 index 0000000..add7517 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/notification/SseNotificationService.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/DeptScope.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/DeptScope.class new file mode 100644 index 0000000..239ddf4 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/DeptScope.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/PermissionEvaluator.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/PermissionEvaluator.class new file mode 100644 index 0000000..322153d Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/PermissionEvaluator.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/RolePermission$RolePermissionBuilder.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/RolePermission$RolePermissionBuilder.class new file mode 100644 index 0000000..bf6e649 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/RolePermission$RolePermissionBuilder.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/RolePermission.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/RolePermission.class new file mode 100644 index 0000000..8f9fd59 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/RolePermission.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/RolePermissionRepository.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/RolePermissionRepository.class new file mode 100644 index 0000000..85810cf Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/RolePermissionRepository.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/WbxRole$WbxRoleBuilder.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/WbxRole$WbxRoleBuilder.class new file mode 100644 index 0000000..263cd62 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/WbxRole$WbxRoleBuilder.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/WbxRole.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/WbxRole.class new file mode 100644 index 0000000..8a31383 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/WbxRole.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/WbxUserRole$WbxUserRoleBuilder.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/WbxUserRole$WbxUserRoleBuilder.class new file mode 100644 index 0000000..c3db060 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/WbxUserRole$WbxUserRoleBuilder.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/WbxUserRole.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/WbxUserRole.class new file mode 100644 index 0000000..7d3c158 Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/WbxUserRole.class differ diff --git a/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/WbxUserRoleRepository.class b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/WbxUserRoleRepository.class new file mode 100644 index 0000000..80369fa Binary files /dev/null and b/wbx-spring-core/build/classes/java/main/kr/co/accura/wbx/spring/rbac/WbxUserRoleRepository.class differ diff --git a/wbx-spring-core/build/libs/wbx-spring-core-0.0.1-SNAPSHOT-plain.jar b/wbx-spring-core/build/libs/wbx-spring-core-0.0.1-SNAPSHOT-plain.jar new file mode 100644 index 0000000..e25ef7b Binary files /dev/null and b/wbx-spring-core/build/libs/wbx-spring-core-0.0.1-SNAPSHOT-plain.jar differ diff --git a/wbx-spring-core/build/libs/wbx-spring-core-0.0.1-SNAPSHOT.jar b/wbx-spring-core/build/libs/wbx-spring-core-0.0.1-SNAPSHOT.jar new file mode 100644 index 0000000..b8e9d34 Binary files /dev/null and b/wbx-spring-core/build/libs/wbx-spring-core-0.0.1-SNAPSHOT.jar differ diff --git a/wbx-spring-core/build/libs/wbx-spring-core-1.0.0-SNAPSHOT.jar b/wbx-spring-core/build/libs/wbx-spring-core-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000..7ace0ac Binary files /dev/null and b/wbx-spring-core/build/libs/wbx-spring-core-1.0.0-SNAPSHOT.jar differ diff --git a/wbx-spring-core/build/reports/problems/problems-report.html b/wbx-spring-core/build/reports/problems/problems-report.html new file mode 100644 index 0000000..37c5c8f --- /dev/null +++ b/wbx-spring-core/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/wbx-spring-core/build/resolvedMainClassName b/wbx-spring-core/build/resolvedMainClassName new file mode 100644 index 0000000..680b816 --- /dev/null +++ b/wbx-spring-core/build/resolvedMainClassName @@ -0,0 +1 @@ +kr.co.accura.wbx.spring.WbxSpringCoreApplication \ No newline at end of file diff --git a/wbx-spring-core/build/resources/main/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/wbx-spring-core/build/resources/main/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..c0db889 --- /dev/null +++ b/wbx-spring-core/build/resources/main/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +kr.co.accura.wbx.spring.config.WbxAutoConfiguration +kr.co.accura.wbx.spring.admin.AdminAutoConfiguration diff --git a/src/main/resources/application-aws.yml b/wbx-spring-core/build/resources/main/application-aws.yml similarity index 100% rename from src/main/resources/application-aws.yml rename to wbx-spring-core/build/resources/main/application-aws.yml diff --git a/src/main/resources/application-azure.yml b/wbx-spring-core/build/resources/main/application-azure.yml similarity index 100% rename from src/main/resources/application-azure.yml rename to wbx-spring-core/build/resources/main/application-azure.yml diff --git a/src/main/resources/application.yml b/wbx-spring-core/build/resources/main/application-example.yml similarity index 100% rename from src/main/resources/application.yml rename to wbx-spring-core/build/resources/main/application-example.yml diff --git a/src/main/resources/application-mssql.yml b/wbx-spring-core/build/resources/main/application-mssql.yml similarity index 100% rename from src/main/resources/application-mssql.yml rename to wbx-spring-core/build/resources/main/application-mssql.yml diff --git a/src/main/resources/application-mysql.yml b/wbx-spring-core/build/resources/main/application-mysql.yml similarity index 100% rename from src/main/resources/application-mysql.yml rename to wbx-spring-core/build/resources/main/application-mysql.yml diff --git a/src/main/resources/application-oracle.yml b/wbx-spring-core/build/resources/main/application-oracle.yml similarity index 100% rename from src/main/resources/application-oracle.yml rename to wbx-spring-core/build/resources/main/application-oracle.yml diff --git a/src/main/resources/application-postgresql.yml b/wbx-spring-core/build/resources/main/application-postgresql.yml similarity index 100% rename from src/main/resources/application-postgresql.yml rename to wbx-spring-core/build/resources/main/application-postgresql.yml diff --git a/src/main/resources/application-prod.yml b/wbx-spring-core/build/resources/main/application-prod.yml similarity index 100% rename from src/main/resources/application-prod.yml rename to wbx-spring-core/build/resources/main/application-prod.yml diff --git a/src/main/resources/application-test.yml b/wbx-spring-core/build/resources/main/application-test.yml similarity index 100% rename from src/main/resources/application-test.yml rename to wbx-spring-core/build/resources/main/application-test.yml diff --git a/src/main/resources/db/migration/common/V001__seed_roles.sql b/wbx-spring-core/build/resources/main/db/migration/common/V001__seed_roles.sql similarity index 100% rename from src/main/resources/db/migration/common/V001__seed_roles.sql rename to wbx-spring-core/build/resources/main/db/migration/common/V001__seed_roles.sql diff --git a/src/main/resources/db/migration/common/V002__seed_system_config.sql b/wbx-spring-core/build/resources/main/db/migration/common/V002__seed_system_config.sql similarity index 100% rename from src/main/resources/db/migration/common/V002__seed_system_config.sql rename to wbx-spring-core/build/resources/main/db/migration/common/V002__seed_system_config.sql diff --git a/src/main/resources/db/migration/mssql/V001__create_tables.sql b/wbx-spring-core/build/resources/main/db/migration/mssql/V001__create_tables.sql similarity index 100% rename from src/main/resources/db/migration/mssql/V001__create_tables.sql rename to wbx-spring-core/build/resources/main/db/migration/mssql/V001__create_tables.sql diff --git a/src/main/resources/db/migration/mysql/V001__create_tables.sql b/wbx-spring-core/build/resources/main/db/migration/mysql/V001__create_tables.sql similarity index 100% rename from src/main/resources/db/migration/mysql/V001__create_tables.sql rename to wbx-spring-core/build/resources/main/db/migration/mysql/V001__create_tables.sql diff --git a/src/main/resources/db/migration/oracle/V001__create_tables.sql b/wbx-spring-core/build/resources/main/db/migration/oracle/V001__create_tables.sql similarity index 100% rename from src/main/resources/db/migration/oracle/V001__create_tables.sql rename to wbx-spring-core/build/resources/main/db/migration/oracle/V001__create_tables.sql diff --git a/src/main/resources/db/migration/postgresql/V001__create_tables.sql b/wbx-spring-core/build/resources/main/db/migration/postgresql/V001__create_tables.sql similarity index 100% rename from src/main/resources/db/migration/postgresql/V001__create_tables.sql rename to wbx-spring-core/build/resources/main/db/migration/postgresql/V001__create_tables.sql diff --git a/src/main/resources/static/admin/css/admin.css b/wbx-spring-core/build/resources/main/static/admin/css/admin.css similarity index 100% rename from src/main/resources/static/admin/css/admin.css rename to wbx-spring-core/build/resources/main/static/admin/css/admin.css diff --git a/src/main/resources/templates/admin/audit-logs.html b/wbx-spring-core/build/resources/main/templates/admin/audit-logs.html similarity index 100% rename from src/main/resources/templates/admin/audit-logs.html rename to wbx-spring-core/build/resources/main/templates/admin/audit-logs.html diff --git a/src/main/resources/templates/admin/config.html b/wbx-spring-core/build/resources/main/templates/admin/config.html similarity index 100% rename from src/main/resources/templates/admin/config.html rename to wbx-spring-core/build/resources/main/templates/admin/config.html diff --git a/src/main/resources/templates/admin/dashboard.html b/wbx-spring-core/build/resources/main/templates/admin/dashboard.html similarity index 100% rename from src/main/resources/templates/admin/dashboard.html rename to wbx-spring-core/build/resources/main/templates/admin/dashboard.html diff --git a/src/main/resources/templates/admin/fragments.html b/wbx-spring-core/build/resources/main/templates/admin/fragments.html similarity index 100% rename from src/main/resources/templates/admin/fragments.html rename to wbx-spring-core/build/resources/main/templates/admin/fragments.html diff --git a/src/main/resources/templates/admin/login-history.html b/wbx-spring-core/build/resources/main/templates/admin/login-history.html similarity index 100% rename from src/main/resources/templates/admin/login-history.html rename to wbx-spring-core/build/resources/main/templates/admin/login-history.html diff --git a/src/main/resources/templates/admin/login.html b/wbx-spring-core/build/resources/main/templates/admin/login.html similarity index 100% rename from src/main/resources/templates/admin/login.html rename to wbx-spring-core/build/resources/main/templates/admin/login.html diff --git a/src/main/resources/templates/admin/permissions.html b/wbx-spring-core/build/resources/main/templates/admin/permissions.html similarity index 100% rename from src/main/resources/templates/admin/permissions.html rename to wbx-spring-core/build/resources/main/templates/admin/permissions.html diff --git a/src/main/resources/templates/admin/role-detail.html b/wbx-spring-core/build/resources/main/templates/admin/role-detail.html similarity index 100% rename from src/main/resources/templates/admin/role-detail.html rename to wbx-spring-core/build/resources/main/templates/admin/role-detail.html diff --git a/src/main/resources/templates/admin/roles.html b/wbx-spring-core/build/resources/main/templates/admin/roles.html similarity index 100% rename from src/main/resources/templates/admin/roles.html rename to wbx-spring-core/build/resources/main/templates/admin/roles.html diff --git a/src/main/resources/templates/admin/system-health.html b/wbx-spring-core/build/resources/main/templates/admin/system-health.html similarity index 100% rename from src/main/resources/templates/admin/system-health.html rename to wbx-spring-core/build/resources/main/templates/admin/system-health.html diff --git a/src/main/resources/templates/admin/user-detail.html b/wbx-spring-core/build/resources/main/templates/admin/user-detail.html similarity index 100% rename from src/main/resources/templates/admin/user-detail.html rename to wbx-spring-core/build/resources/main/templates/admin/user-detail.html diff --git a/src/main/resources/templates/admin/users.html b/wbx-spring-core/build/resources/main/templates/admin/users.html similarity index 100% rename from src/main/resources/templates/admin/users.html rename to wbx-spring-core/build/resources/main/templates/admin/users.html diff --git a/wbx-spring-core/build/tmp/bootJar/MANIFEST.MF b/wbx-spring-core/build/tmp/bootJar/MANIFEST.MF new file mode 100644 index 0000000..59499bc --- /dev/null +++ b/wbx-spring-core/build/tmp/bootJar/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 + diff --git a/wbx-spring-core/build/tmp/compileJava/compileTransaction/stash-dir/AdminController.class.uniqueId1 b/wbx-spring-core/build/tmp/compileJava/compileTransaction/stash-dir/AdminController.class.uniqueId1 new file mode 100644 index 0000000..20ede33 Binary files /dev/null and b/wbx-spring-core/build/tmp/compileJava/compileTransaction/stash-dir/AdminController.class.uniqueId1 differ diff --git a/wbx-spring-core/build/tmp/compileJava/compileTransaction/stash-dir/AdminLoginController.class.uniqueId3 b/wbx-spring-core/build/tmp/compileJava/compileTransaction/stash-dir/AdminLoginController.class.uniqueId3 new file mode 100644 index 0000000..6be8634 Binary files /dev/null and b/wbx-spring-core/build/tmp/compileJava/compileTransaction/stash-dir/AdminLoginController.class.uniqueId3 differ diff --git a/wbx-spring-core/build/tmp/compileJava/compileTransaction/stash-dir/AdminUserDetailsService.class.uniqueId4 b/wbx-spring-core/build/tmp/compileJava/compileTransaction/stash-dir/AdminUserDetailsService.class.uniqueId4 new file mode 100644 index 0000000..f148e83 Binary files /dev/null and b/wbx-spring-core/build/tmp/compileJava/compileTransaction/stash-dir/AdminUserDetailsService.class.uniqueId4 differ diff --git a/wbx-spring-core/build/tmp/compileJava/compileTransaction/stash-dir/AdminViewController.class.uniqueId2 b/wbx-spring-core/build/tmp/compileJava/compileTransaction/stash-dir/AdminViewController.class.uniqueId2 new file mode 100644 index 0000000..2c10c4a Binary files /dev/null and b/wbx-spring-core/build/tmp/compileJava/compileTransaction/stash-dir/AdminViewController.class.uniqueId2 differ diff --git a/wbx-spring-core/build/tmp/compileJava/compileTransaction/stash-dir/SecurityAutoConfig.class.uniqueId0 b/wbx-spring-core/build/tmp/compileJava/compileTransaction/stash-dir/SecurityAutoConfig.class.uniqueId0 new file mode 100644 index 0000000..9091e81 Binary files /dev/null and b/wbx-spring-core/build/tmp/compileJava/compileTransaction/stash-dir/SecurityAutoConfig.class.uniqueId0 differ diff --git a/wbx-spring-core/build/tmp/compileJava/previous-compilation-data.bin b/wbx-spring-core/build/tmp/compileJava/previous-compilation-data.bin new file mode 100644 index 0000000..0699c34 Binary files /dev/null and b/wbx-spring-core/build/tmp/compileJava/previous-compilation-data.bin differ diff --git a/wbx-spring-core/build/tmp/jar/MANIFEST.MF b/wbx-spring-core/build/tmp/jar/MANIFEST.MF new file mode 100644 index 0000000..59499bc --- /dev/null +++ b/wbx-spring-core/build/tmp/jar/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 + diff --git a/docker-compose-dev.yml b/wbx-spring-core/docker-compose-dev.yml similarity index 100% rename from docker-compose-dev.yml rename to wbx-spring-core/docker-compose-dev.yml diff --git a/docker-compose.yml b/wbx-spring-core/docker-compose.yml similarity index 100% rename from docker-compose.yml rename to wbx-spring-core/docker-compose.yml diff --git a/docs/WBX_Spring_Framework_개발자가이드.pdf b/wbx-spring-core/docs/WBX_Spring_Framework_개발자가이드.pdf similarity index 100% rename from docs/WBX_Spring_Framework_개발자가이드.pdf rename to wbx-spring-core/docs/WBX_Spring_Framework_개발자가이드.pdf diff --git a/docs/WBX_Spring_Framework_설치가이드_Cloud.pdf b/wbx-spring-core/docs/WBX_Spring_Framework_설치가이드_Cloud.pdf similarity index 100% rename from docs/WBX_Spring_Framework_설치가이드_Cloud.pdf rename to wbx-spring-core/docs/WBX_Spring_Framework_설치가이드_Cloud.pdf diff --git a/docs/WBX_Spring_Framework_설치가이드_OnPremise.pdf b/wbx-spring-core/docs/WBX_Spring_Framework_설치가이드_OnPremise.pdf similarity index 100% rename from docs/WBX_Spring_Framework_설치가이드_OnPremise.pdf rename to wbx-spring-core/docs/WBX_Spring_Framework_설치가이드_OnPremise.pdf diff --git a/docs/generate_system_design.py b/wbx-spring-core/docs/generate_system_design.py similarity index 100% rename from docs/generate_system_design.py rename to wbx-spring-core/docs/generate_system_design.py diff --git a/docs/개발환경_사전설치_가이드.txt b/wbx-spring-core/docs/개발환경_사전설치_가이드.txt similarity index 100% rename from docs/개발환경_사전설치_가이드.txt rename to wbx-spring-core/docs/개발환경_사전설치_가이드.txt diff --git a/gradle/wrapper/gradle-wrapper.jar b/wbx-spring-core/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from gradle/wrapper/gradle-wrapper.jar rename to wbx-spring-core/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/wbx-spring-core/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from gradle/wrapper/gradle-wrapper.properties rename to wbx-spring-core/gradle/wrapper/gradle-wrapper.properties diff --git a/gradlew b/wbx-spring-core/gradlew similarity index 100% rename from gradlew rename to wbx-spring-core/gradlew diff --git a/gradlew.bat b/wbx-spring-core/gradlew.bat similarity index 100% rename from gradlew.bat rename to wbx-spring-core/gradlew.bat diff --git a/scripts/deploy-prod.sh b/wbx-spring-core/scripts/deploy-prod.sh similarity index 100% rename from scripts/deploy-prod.sh rename to wbx-spring-core/scripts/deploy-prod.sh diff --git a/scripts/gitea-bulk-create.sh b/wbx-spring-core/scripts/gitea-bulk-create.sh similarity index 100% rename from scripts/gitea-bulk-create.sh rename to wbx-spring-core/scripts/gitea-bulk-create.sh diff --git a/scripts/gitea-create-user.sh b/wbx-spring-core/scripts/gitea-create-user.sh similarity index 100% rename from scripts/gitea-create-user.sh rename to wbx-spring-core/scripts/gitea-create-user.sh diff --git a/scripts/install.bat b/wbx-spring-core/scripts/install.bat similarity index 100% rename from scripts/install.bat rename to wbx-spring-core/scripts/install.bat diff --git a/scripts/install.sh b/wbx-spring-core/scripts/install.sh similarity index 100% rename from scripts/install.sh rename to wbx-spring-core/scripts/install.sh diff --git a/scripts/run-install.bat b/wbx-spring-core/scripts/run-install.bat similarity index 100% rename from scripts/run-install.bat rename to wbx-spring-core/scripts/run-install.bat diff --git a/wbx-spring-core/settings.gradle.bak b/wbx-spring-core/settings.gradle.bak new file mode 100644 index 0000000..7d4e6ef --- /dev/null +++ b/wbx-spring-core/settings.gradle.bak @@ -0,0 +1 @@ +rootProject.name = 'wbx-spring-core' diff --git a/src/main/java/kr/co/accura/wbx/spring/HealthController.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/HealthController.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/HealthController.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/HealthController.java diff --git a/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/AdminAutoConfiguration.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/AdminAutoConfiguration.java new file mode 100644 index 0000000..78a9d05 --- /dev/null +++ b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/AdminAutoConfiguration.java @@ -0,0 +1,47 @@ +package kr.co.accura.wbx.spring.admin; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +/** + * Conditionally activates the Admin Console UI. + * Enabled by default (matchIfMissing = true); set wbx.spring.admin-ui.enabled=false to disable. + * Individual admin beans (AdminController, AdminLoginController, AdminUserDetailsService, + * AdminViewController) also carry the same @ConditionalOnProperty so they are excluded + * from component scanning when the property is false. + */ +@Configuration +@ConditionalOnProperty(name = "wbx.spring.admin-ui.enabled", havingValue = "true", matchIfMissing = true) +public class AdminAutoConfiguration { + + /** + * Admin Console — session-based form login (moved from SecurityAutoConfig). + */ + @Bean + @Order(1) + public SecurityFilterChain adminFilterChain(HttpSecurity http) throws Exception { + return http + .securityMatcher("/admin/**", "/admin") + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/admin/css/**", "/admin/js/**").permitAll() + .requestMatchers("/admin/login").permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form + .loginPage("/admin/login") + .defaultSuccessUrl("/admin", true) + .permitAll() + ) + .logout(logout -> logout + .logoutUrl("/admin/logout") + .logoutSuccessUrl("/admin/login?logout") + ) + .build(); + } +} diff --git a/src/main/java/kr/co/accura/wbx/spring/admin/AdminController.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/AdminController.java similarity index 96% rename from src/main/java/kr/co/accura/wbx/spring/admin/AdminController.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/AdminController.java index 6a06b3b..a31de13 100644 --- a/src/main/java/kr/co/accura/wbx/spring/admin/AdminController.java +++ b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/AdminController.java @@ -13,9 +13,12 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + import java.util.List; import java.util.Map; +@ConditionalOnProperty(name = "wbx.spring.admin-ui.enabled", havingValue = "true", matchIfMissing = true) @RestController @RequestMapping("${wbx.spring.api-prefix:/api}/admin") @PreAuthorize("hasRole('SA')") diff --git a/src/main/java/kr/co/accura/wbx/spring/admin/AdminLoginController.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/AdminLoginController.java similarity index 61% rename from src/main/java/kr/co/accura/wbx/spring/admin/AdminLoginController.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/AdminLoginController.java index fc50097..c547fd7 100644 --- a/src/main/java/kr/co/accura/wbx/spring/admin/AdminLoginController.java +++ b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/AdminLoginController.java @@ -1,8 +1,10 @@ package kr.co.accura.wbx.spring.admin; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +@ConditionalOnProperty(name = "wbx.spring.admin-ui.enabled", havingValue = "true", matchIfMissing = true) @Controller public class AdminLoginController { diff --git a/src/main/java/kr/co/accura/wbx/spring/admin/AdminUserDetailsService.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/AdminUserDetailsService.java similarity index 87% rename from src/main/java/kr/co/accura/wbx/spring/admin/AdminUserDetailsService.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/AdminUserDetailsService.java index f825a97..d4fc879 100644 --- a/src/main/java/kr/co/accura/wbx/spring/admin/AdminUserDetailsService.java +++ b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/AdminUserDetailsService.java @@ -7,10 +7,12 @@ import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.List; +@ConditionalOnProperty(name = "wbx.spring.admin-ui.enabled", havingValue = "true", matchIfMissing = true) @Service @RequiredArgsConstructor public class AdminUserDetailsService implements UserDetailsService { diff --git a/src/main/java/kr/co/accura/wbx/spring/admin/AdminViewController.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/AdminViewController.java similarity index 98% rename from src/main/java/kr/co/accura/wbx/spring/admin/AdminViewController.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/AdminViewController.java index 57c2d83..02c595c 100644 --- a/src/main/java/kr/co/accura/wbx/spring/admin/AdminViewController.java +++ b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/AdminViewController.java @@ -17,10 +17,13 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +@ConditionalOnProperty(name = "wbx.spring.admin-ui.enabled", havingValue = "true", matchIfMissing = true) @Controller @RequestMapping("/admin") @RequiredArgsConstructor diff --git a/src/main/java/kr/co/accura/wbx/spring/admin/WbxRoleRepository.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/WbxRoleRepository.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/admin/WbxRoleRepository.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/WbxRoleRepository.java diff --git a/src/main/java/kr/co/accura/wbx/spring/admin/WbxSystemConfigRepository.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/WbxSystemConfigRepository.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/admin/WbxSystemConfigRepository.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/admin/WbxSystemConfigRepository.java diff --git a/src/main/java/kr/co/accura/wbx/spring/approval/ActionRequest.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ActionRequest.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/approval/ActionRequest.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ActionRequest.java diff --git a/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalCompletedEvent.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalCompletedEvent.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/approval/ApprovalCompletedEvent.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalCompletedEvent.java diff --git a/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalHandler.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalHandler.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/approval/ApprovalHandler.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalHandler.java diff --git a/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalHandlerRegistry.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalHandlerRegistry.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/approval/ApprovalHandlerRegistry.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalHandlerRegistry.java diff --git a/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalHistoryDto.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalHistoryDto.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/approval/ApprovalHistoryDto.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalHistoryDto.java diff --git a/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalLineDto.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalLineDto.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/approval/ApprovalLineDto.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalLineDto.java diff --git a/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalPendingDto.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalPendingDto.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/approval/ApprovalPendingDto.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalPendingDto.java diff --git a/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalResult.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalResult.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/approval/ApprovalResult.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/ApprovalResult.java diff --git a/src/main/java/kr/co/accura/wbx/spring/approval/UnifiedApprovalController.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/UnifiedApprovalController.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/approval/UnifiedApprovalController.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/approval/UnifiedApprovalController.java diff --git a/src/main/java/kr/co/accura/wbx/spring/audit/AuditLogRepository.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/audit/AuditLogRepository.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/audit/AuditLogRepository.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/audit/AuditLogRepository.java diff --git a/src/main/java/kr/co/accura/wbx/spring/audit/AuditLogService.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/audit/AuditLogService.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/audit/AuditLogService.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/audit/AuditLogService.java diff --git a/src/main/java/kr/co/accura/wbx/spring/audit/WbxAuditLog.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/audit/WbxAuditLog.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/audit/WbxAuditLog.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/audit/WbxAuditLog.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/ApiKeyFilter.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/ApiKeyFilter.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/ApiKeyFilter.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/ApiKeyFilter.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/AuthController.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/AuthController.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/AuthController.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/AuthController.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/JwtFilter.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/JwtFilter.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/JwtFilter.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/JwtFilter.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/JwtProvider.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/JwtProvider.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/JwtProvider.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/JwtProvider.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/LoginHistoryRepository.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/LoginHistoryRepository.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/LoginHistoryRepository.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/LoginHistoryRepository.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/MfaController.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/MfaController.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/MfaController.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/MfaController.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/MfaService.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/MfaService.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/MfaService.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/MfaService.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/PasswordPolicy.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/PasswordPolicy.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/PasswordPolicy.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/PasswordPolicy.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/RefreshTokenRepository.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/RefreshTokenRepository.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/RefreshTokenRepository.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/RefreshTokenRepository.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/RefreshTokenService.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/RefreshTokenService.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/RefreshTokenService.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/RefreshTokenService.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/SsoSuccessHandler.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/SsoSuccessHandler.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/SsoSuccessHandler.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/SsoSuccessHandler.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/TotpSecretRepository.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/TotpSecretRepository.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/TotpSecretRepository.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/TotpSecretRepository.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/WbxLoginHistory.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/WbxLoginHistory.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/WbxLoginHistory.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/WbxLoginHistory.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/WbxRefreshToken.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/WbxRefreshToken.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/WbxRefreshToken.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/WbxRefreshToken.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/WbxTotpSecret.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/WbxTotpSecret.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/WbxTotpSecret.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/WbxTotpSecret.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/WbxUser.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/WbxUser.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/WbxUser.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/WbxUser.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/WbxUserDetails.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/WbxUserDetails.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/WbxUserDetails.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/WbxUserDetails.java diff --git a/src/main/java/kr/co/accura/wbx/spring/auth/WbxUserRepository.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/WbxUserRepository.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/auth/WbxUserRepository.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/auth/WbxUserRepository.java diff --git a/src/main/java/kr/co/accura/wbx/spring/common/BaseEntity.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/common/BaseEntity.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/common/BaseEntity.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/common/BaseEntity.java diff --git a/src/main/java/kr/co/accura/wbx/spring/common/BusinessException.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/common/BusinessException.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/common/BusinessException.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/common/BusinessException.java diff --git a/src/main/java/kr/co/accura/wbx/spring/common/NotFoundException.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/common/NotFoundException.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/common/NotFoundException.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/common/NotFoundException.java diff --git a/src/main/java/kr/co/accura/wbx/spring/common/SecurityUtils.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/common/SecurityUtils.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/common/SecurityUtils.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/common/SecurityUtils.java diff --git a/src/main/java/kr/co/accura/wbx/spring/compat/WbxErrorHandler.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/compat/WbxErrorHandler.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/compat/WbxErrorHandler.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/compat/WbxErrorHandler.java diff --git a/src/main/java/kr/co/accura/wbx/spring/compat/WbxPaginationConfig.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/compat/WbxPaginationConfig.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/compat/WbxPaginationConfig.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/compat/WbxPaginationConfig.java diff --git a/src/main/java/kr/co/accura/wbx/spring/config/CorsAutoConfig.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/config/CorsAutoConfig.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/config/CorsAutoConfig.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/config/CorsAutoConfig.java diff --git a/src/main/java/kr/co/accura/wbx/spring/config/OpenApiConfig.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/config/OpenApiConfig.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/config/OpenApiConfig.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/config/OpenApiConfig.java diff --git a/src/main/java/kr/co/accura/wbx/spring/config/SecurityAutoConfig.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/config/SecurityAutoConfig.java similarity index 73% rename from src/main/java/kr/co/accura/wbx/spring/config/SecurityAutoConfig.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/config/SecurityAutoConfig.java index cb53c65..6fb30e1 100644 --- a/src/main/java/kr/co/accura/wbx/spring/config/SecurityAutoConfig.java +++ b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/config/SecurityAutoConfig.java @@ -30,32 +30,6 @@ public class SecurityAutoConfig { @Autowired(required = false) private SsoSuccessHandler ssoSuccessHandler; - /** - * Admin Console — 세션 기반 폼 로그인 - */ - @Bean - @Order(1) - public SecurityFilterChain adminFilterChain(HttpSecurity http) throws Exception { - return http - .securityMatcher("/admin/**", "/admin") - .csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(auth -> auth - .requestMatchers("/admin/css/**", "/admin/js/**").permitAll() - .requestMatchers("/admin/login").permitAll() - .anyRequest().authenticated() - ) - .formLogin(form -> form - .loginPage("/admin/login") - .defaultSuccessUrl("/admin", true) - .permitAll() - ) - .logout(logout -> logout - .logoutUrl("/admin/logout") - .logoutSuccessUrl("/admin/login?logout") - ) - .build(); - } - /** * REST API — JWT Stateless */ diff --git a/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/config/WbxAutoConfiguration.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/config/WbxAutoConfiguration.java new file mode 100644 index 0000000..a48a2da --- /dev/null +++ b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/config/WbxAutoConfiguration.java @@ -0,0 +1,21 @@ +package kr.co.accura.wbx.spring.config; + +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * wbx-spring-core Auto-Configuration. + * Replaces the former WbxSpringCoreApplication annotations. + * Consuming Spring Boot applications pick this up automatically + * via META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. + */ +@Configuration +@EnableJpaAuditing +@EnableAsync +@EnableScheduling +@EnableCaching +public class WbxAutoConfiguration { +} diff --git a/src/main/java/kr/co/accura/wbx/spring/config/WbxSpringProperties.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/config/WbxSpringProperties.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/config/WbxSpringProperties.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/config/WbxSpringProperties.java diff --git a/src/main/java/kr/co/accura/wbx/spring/config/WbxSystemConfig.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/config/WbxSystemConfig.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/config/WbxSystemConfig.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/config/WbxSystemConfig.java diff --git a/src/main/java/kr/co/accura/wbx/spring/datasource/DataSource.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/datasource/DataSource.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/datasource/DataSource.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/datasource/DataSource.java diff --git a/src/main/java/kr/co/accura/wbx/spring/datasource/DataSourceAspect.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/datasource/DataSourceAspect.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/datasource/DataSourceAspect.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/datasource/DataSourceAspect.java diff --git a/src/main/java/kr/co/accura/wbx/spring/datasource/MultiDataSourceConfig.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/datasource/MultiDataSourceConfig.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/datasource/MultiDataSourceConfig.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/datasource/MultiDataSourceConfig.java diff --git a/src/main/java/kr/co/accura/wbx/spring/datasource/WbxRoutingDataSource.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/datasource/WbxRoutingDataSource.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/datasource/WbxRoutingDataSource.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/datasource/WbxRoutingDataSource.java diff --git a/src/main/java/kr/co/accura/wbx/spring/file/AwsS3StorageService.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/file/AwsS3StorageService.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/file/AwsS3StorageService.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/file/AwsS3StorageService.java diff --git a/src/main/java/kr/co/accura/wbx/spring/file/AzureBlobStorageService.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/file/AzureBlobStorageService.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/file/AzureBlobStorageService.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/file/AzureBlobStorageService.java diff --git a/src/main/java/kr/co/accura/wbx/spring/file/FileStorageService.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/file/FileStorageService.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/file/FileStorageService.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/file/FileStorageService.java diff --git a/src/main/java/kr/co/accura/wbx/spring/file/GcpStorageService.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/file/GcpStorageService.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/file/GcpStorageService.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/file/GcpStorageService.java diff --git a/src/main/java/kr/co/accura/wbx/spring/file/LocalFileStorageService.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/file/LocalFileStorageService.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/file/LocalFileStorageService.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/file/LocalFileStorageService.java diff --git a/src/main/java/kr/co/accura/wbx/spring/file/WbxFileUpload.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/file/WbxFileUpload.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/file/WbxFileUpload.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/file/WbxFileUpload.java diff --git a/src/main/java/kr/co/accura/wbx/spring/notification/Notification.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/notification/Notification.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/notification/Notification.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/notification/Notification.java diff --git a/src/main/java/kr/co/accura/wbx/spring/notification/NotificationController.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/notification/NotificationController.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/notification/NotificationController.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/notification/NotificationController.java diff --git a/src/main/java/kr/co/accura/wbx/spring/notification/NotificationDto.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/notification/NotificationDto.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/notification/NotificationDto.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/notification/NotificationDto.java diff --git a/src/main/java/kr/co/accura/wbx/spring/notification/SseNotificationService.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/notification/SseNotificationService.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/notification/SseNotificationService.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/notification/SseNotificationService.java diff --git a/src/main/java/kr/co/accura/wbx/spring/rbac/DeptScope.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/rbac/DeptScope.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/rbac/DeptScope.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/rbac/DeptScope.java diff --git a/src/main/java/kr/co/accura/wbx/spring/rbac/PermissionEvaluator.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/rbac/PermissionEvaluator.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/rbac/PermissionEvaluator.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/rbac/PermissionEvaluator.java diff --git a/src/main/java/kr/co/accura/wbx/spring/rbac/RolePermission.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/rbac/RolePermission.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/rbac/RolePermission.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/rbac/RolePermission.java diff --git a/src/main/java/kr/co/accura/wbx/spring/rbac/RolePermissionRepository.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/rbac/RolePermissionRepository.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/rbac/RolePermissionRepository.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/rbac/RolePermissionRepository.java diff --git a/src/main/java/kr/co/accura/wbx/spring/rbac/WbxRole.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/rbac/WbxRole.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/rbac/WbxRole.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/rbac/WbxRole.java diff --git a/src/main/java/kr/co/accura/wbx/spring/rbac/WbxUserRole.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/rbac/WbxUserRole.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/rbac/WbxUserRole.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/rbac/WbxUserRole.java diff --git a/src/main/java/kr/co/accura/wbx/spring/rbac/WbxUserRoleRepository.java b/wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/rbac/WbxUserRoleRepository.java similarity index 100% rename from src/main/java/kr/co/accura/wbx/spring/rbac/WbxUserRoleRepository.java rename to wbx-spring-core/src/main/java/kr/co/accura/wbx/spring/rbac/WbxUserRoleRepository.java diff --git a/wbx-spring-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/wbx-spring-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..c0db889 --- /dev/null +++ b/wbx-spring-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +kr.co.accura.wbx.spring.config.WbxAutoConfiguration +kr.co.accura.wbx.spring.admin.AdminAutoConfiguration diff --git a/wbx-spring-core/src/main/resources/application-aws.yml b/wbx-spring-core/src/main/resources/application-aws.yml new file mode 100644 index 0000000..789ab78 --- /dev/null +++ b/wbx-spring-core/src/main/resources/application-aws.yml @@ -0,0 +1,29 @@ +# ===== AWS 클라우드 프로필 ===== +# 사용법: --spring.profiles.active=prod,postgresql,aws +# AWS Cognito SSO + S3 연동 + +spring: + security: + oauth2: + client: + registration: + cognito: + client-id: ${AWS_COGNITO_CLIENT_ID} + client-secret: ${AWS_COGNITO_CLIENT_SECRET} + scope: openid,profile,email + provider: + cognito: + issuer-uri: https://cognito-idp.${AWS_REGION:ap-northeast-2}.amazonaws.com/${AWS_USER_POOL_ID} + +wbx: + spring: + mfa: + enabled: true + force-for-external: true + file: + storage-type: aws-s3 + aws: + bucket: ${AWS_S3_BUCKET} + region: ${AWS_REGION:ap-northeast-2} + access-key: ${AWS_ACCESS_KEY} + secret-key: ${AWS_SECRET_KEY} diff --git a/wbx-spring-core/src/main/resources/application-azure.yml b/wbx-spring-core/src/main/resources/application-azure.yml new file mode 100644 index 0000000..771f662 --- /dev/null +++ b/wbx-spring-core/src/main/resources/application-azure.yml @@ -0,0 +1,28 @@ +# ===== Azure 클라우드 프로필 ===== +# 사용법: --spring.profiles.active=prod,mssql,azure +# Azure Entra SSO + Blob Storage + Key Vault 연동 + +spring: + security: + oauth2: + client: + registration: + azure: + client-id: ${AZURE_CLIENT_ID} + client-secret: ${AZURE_CLIENT_SECRET} + scope: openid,profile,email + provider: + azure: + issuer-uri: https://login.microsoftonline.com/${AZURE_TENANT_ID}/v2.0 + +wbx: + spring: + mfa: + enabled: false # Azure Entra Conditional Access가 MFA 처리 + force-for-external: true + file: + storage-type: azure-blob + azure: + account-name: ${AZURE_STORAGE_ACCOUNT} + account-key: ${AZURE_STORAGE_KEY} + container-name: ${AZURE_CONTAINER:uploads} diff --git a/wbx-spring-core/src/main/resources/application-example.yml b/wbx-spring-core/src/main/resources/application-example.yml new file mode 100644 index 0000000..d3f6f67 --- /dev/null +++ b/wbx-spring-core/src/main/resources/application-example.yml @@ -0,0 +1,69 @@ +spring: + application: + name: wbx-spring-core + + jpa: + hibernate: + ddl-auto: update + open-in-view: false + database-platform: org.hibernate.dialect.MySQLDialect + properties: + hibernate: + default_batch_fetch_size: 100 + format_sql: true + dialect: org.hibernate.dialect.MySQLDialect + + flyway: + enabled: false # 개발 시 hibernate ddl-auto 사용, 프로덕션 시 true + + datasource: + url: jdbc:mysql://${DB_HOST:ws.ubuilder.co.kr}:${DB_PORT:3306}/${DB_NAME:mos}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul + username: ${DB_USER:jsh} + password: ${DB_PASS:jsh@} + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + maximum-pool-size: 20 + minimum-idle: 5 + connection-timeout: 30000 + + data: + redis: + host: localhost + port: 6379 + +server: + port: 8080 + forward-headers-strategy: native + servlet: + context-path: ${SERVER_CONTEXT_PATH:/} + +# WBX Spring Framework +wbx: + spring: + api-prefix: /api + jwt: + secret: ${JWT_SECRET:wbx-spring-dev-secret-key-change-in-production-minimum-256-bits-long} + expiration: 28800 + cors: + allowed-origins: ${CORS_ORIGINS:http://localhost:5173,http://localhost:3000,http://localhost:8080} + notification: + sse-enabled: true + heartbeat-seconds: 30 + +management: + endpoints: + web: + exposure: + include: health,info,metrics + endpoint: + health: + show-details: when-authorized + +springdoc: + api-docs: + path: /api-docs + swagger-ui: + path: /swagger-ui + packages-to-scan: kr.co.accura.wbx.spring + +spring.mvc.problemdetail.enabled: false diff --git a/wbx-spring-core/src/main/resources/application-mssql.yml b/wbx-spring-core/src/main/resources/application-mssql.yml new file mode 100644 index 0000000..9d10acc --- /dev/null +++ b/wbx-spring-core/src/main/resources/application-mssql.yml @@ -0,0 +1,22 @@ +# ===== MSSQL 프로필 ===== +# 사용법: --spring.profiles.active=prod,mssql + +spring: + datasource: + url: jdbc:sqlserver://${DB_HOST:localhost}:${DB_PORT:1433};databaseName=${DB_NAME:wbx_spring};encrypt=true;trustServerCertificate=true + username: ${DB_USER:sa} + password: ${DB_PASS:password} + driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver + hikari: + maximum-pool-size: ${DB_POOL_SIZE:20} + minimum-idle: 5 + connection-timeout: 30000 + + jpa: + database-platform: org.hibernate.dialect.SQLServerDialect + properties: + hibernate: + dialect: org.hibernate.dialect.SQLServerDialect + + flyway: + locations: classpath:db/migration/common,classpath:db/migration/mssql diff --git a/wbx-spring-core/src/main/resources/application-mysql.yml b/wbx-spring-core/src/main/resources/application-mysql.yml new file mode 100644 index 0000000..28a7742 --- /dev/null +++ b/wbx-spring-core/src/main/resources/application-mysql.yml @@ -0,0 +1,22 @@ +# ===== MySQL 프로필 ===== +# 사용법: --spring.profiles.active=prod,mysql + +spring: + datasource: + url: jdbc:mysql://${DB_HOST:ws.ubuilder.co.kr}:${DB_PORT:3306}/${DB_NAME:mos}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul + username: ${DB_USER:jsh} + password: ${DB_PASS:jsh@} + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + maximum-pool-size: ${DB_POOL_SIZE:20} + minimum-idle: 5 + connection-timeout: 30000 + + jpa: + database-platform: org.hibernate.dialect.MySQLDialect + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect + + flyway: + locations: classpath:db/migration/common,classpath:db/migration/mysql diff --git a/wbx-spring-core/src/main/resources/application-oracle.yml b/wbx-spring-core/src/main/resources/application-oracle.yml new file mode 100644 index 0000000..0552737 --- /dev/null +++ b/wbx-spring-core/src/main/resources/application-oracle.yml @@ -0,0 +1,22 @@ +# ===== Oracle 프로필 ===== +# 사용법: --spring.profiles.active=prod,oracle + +spring: + datasource: + url: jdbc:oracle:thin:@${DB_HOST:localhost}:${DB_PORT:1521}:${DB_SID:ORCL} + username: ${DB_USER:wbxapp} + password: ${DB_PASS:password} + driver-class-name: oracle.jdbc.OracleDriver + hikari: + maximum-pool-size: ${DB_POOL_SIZE:20} + minimum-idle: 5 + connection-timeout: 30000 + + jpa: + database-platform: org.hibernate.dialect.OracleDialect + properties: + hibernate: + dialect: org.hibernate.dialect.OracleDialect + + flyway: + locations: classpath:db/migration/common,classpath:db/migration/oracle diff --git a/wbx-spring-core/src/main/resources/application-postgresql.yml b/wbx-spring-core/src/main/resources/application-postgresql.yml new file mode 100644 index 0000000..9478d24 --- /dev/null +++ b/wbx-spring-core/src/main/resources/application-postgresql.yml @@ -0,0 +1,22 @@ +# ===== PostgreSQL 프로필 ===== +# 사용법: --spring.profiles.active=prod,postgresql + +spring: + datasource: + url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:wbx_spring} + username: ${DB_USER:wbxapp} + password: ${DB_PASS:password} + driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: ${DB_POOL_SIZE:20} + minimum-idle: 5 + connection-timeout: 30000 + + jpa: + database-platform: org.hibernate.dialect.PostgreSQLDialect + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + + flyway: + locations: classpath:db/migration/common,classpath:db/migration/postgresql diff --git a/wbx-spring-core/src/main/resources/application-prod.yml b/wbx-spring-core/src/main/resources/application-prod.yml new file mode 100644 index 0000000..e5046a7 --- /dev/null +++ b/wbx-spring-core/src/main/resources/application-prod.yml @@ -0,0 +1,52 @@ +# ===== WBX Spring Framework — 프로덕션 프로필 ===== +# 사용법: java -jar app.jar --spring.profiles.active=prod,mysql +# java -jar app.jar --spring.profiles.active=prod,postgresql + +server: + port: 8080 + forward-headers-strategy: native + servlet: + context-path: ${SERVER_CONTEXT_PATH:/} + +spring: + jpa: + hibernate: + ddl-auto: validate # 프로덕션: Flyway 사용, DDL 검증만 + open-in-view: false + properties: + hibernate: + default_batch_fetch_size: 100 + + flyway: + enabled: true + +wbx: + spring: + jwt: + secret: ${JWT_SECRET} + expiration: 28800 + cors: + allowed-origins: ${CORS_ORIGINS:https://app.company.com} + notification: + sse-enabled: true + heartbeat-seconds: 30 + +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + endpoint: + health: + show-details: when-authorized + +springdoc: + swagger-ui: + enabled: false # 프로덕션 Swagger 비활성화 + +logging: + level: + root: WARN + kr.co.accura.wbx.spring: INFO + file: + name: ${LOG_PATH:/opt/wbx-app/logs/app.log} diff --git a/wbx-spring-core/src/main/resources/application-test.yml b/wbx-spring-core/src/main/resources/application-test.yml new file mode 100644 index 0000000..18cdd23 --- /dev/null +++ b/wbx-spring-core/src/main/resources/application-test.yml @@ -0,0 +1,38 @@ +# ===== 테스트 프로필 ===== +# 사용법: ./gradlew test (자동 적용) + +spring: + datasource: + url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1 + username: sa + password: + driver-class-name: org.h2.Driver + + jpa: + hibernate: + ddl-auto: create-drop + database-platform: org.hibernate.dialect.H2Dialect + + flyway: + enabled: false + + data: + redis: + host: localhost + port: 6379 + +wbx: + spring: + jwt: + secret: test-secret-key-minimum-256-bits-for-hmac-sha-algorithm + expiration: 3600 + mfa: + enabled: false + file: + storage-type: local + upload-dir: ./build/test-uploads + +logging: + level: + root: WARN + kr.co.accura.wbx.spring: DEBUG diff --git a/wbx-spring-core/src/main/resources/db/migration/common/V001__seed_roles.sql b/wbx-spring-core/src/main/resources/db/migration/common/V001__seed_roles.sql new file mode 100644 index 0000000..fc58623 --- /dev/null +++ b/wbx-spring-core/src/main/resources/db/migration/common/V001__seed_roles.sql @@ -0,0 +1,8 @@ +-- WBX Spring Framework — 초기 역할 데이터 +INSERT IGNORE INTO wbx_roles (code, name, description, is_system) VALUES + ('SA', 'System Administrator', '전체 시스템 관리', true), + ('PM', 'Project Manager', '프로젝트 관리, 최종 결재', true), + ('PCM', 'Project Control Mgr', 'WBS/TEAL 관리', true), + ('PTK', 'Project Timekeeper', '시수 관리', true), + ('DL', 'Discipline Lead', '1차 결재, Discipline 관리', true), + ('USER', 'General User', '일반 사용자', true); diff --git a/wbx-spring-core/src/main/resources/db/migration/common/V002__seed_system_config.sql b/wbx-spring-core/src/main/resources/db/migration/common/V002__seed_system_config.sql new file mode 100644 index 0000000..16137c2 --- /dev/null +++ b/wbx-spring-core/src/main/resources/db/migration/common/V002__seed_system_config.sql @@ -0,0 +1,10 @@ +-- WBX Spring Framework — 초기 시스템 설정 +INSERT IGNORE INTO wbx_system_config (config_key, config_value, value_type, description) VALUES + ('auth.max_failed_attempts', '5', 'INT', '최대 로그인 실패 횟수'), + ('auth.lockout_minutes', '15', 'INT', '계정 잠금 시간(분)'), + ('auth.password_expiry_days', '90', 'INT', '비밀번호 만료 기간(일)'), + ('auth.session_timeout_minutes', '480', 'INT', '세션 타임아웃(분)'), + ('notification.sse_heartbeat_seconds', '30', 'INT', 'SSE 하트비트 주기(초)'), + ('file.max_upload_size_mb', '50', 'INT', '최대 업로드 크기(MB)'), + ('app.timezone', 'Asia/Seoul', 'STRING', '시스템 타임존'), + ('app.date_format', 'yyyy-MM-dd', 'STRING', '날짜 표시 형식'); diff --git a/wbx-spring-core/src/main/resources/db/migration/mssql/V001__create_tables.sql b/wbx-spring-core/src/main/resources/db/migration/mssql/V001__create_tables.sql new file mode 100644 index 0000000..9d6642e --- /dev/null +++ b/wbx-spring-core/src/main/resources/db/migration/mssql/V001__create_tables.sql @@ -0,0 +1,134 @@ +-- WBX Spring Framework — MSSQL 스키마 + +CREATE TABLE wbx_users ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + email NVARCHAR(255) NOT NULL UNIQUE, + username NVARCHAR(100) NOT NULL UNIQUE, + hashed_password NVARCHAR(500), + full_name NVARCHAR(255), + phone NVARCHAR(50), + department_id BIGINT, + position_title NVARCHAR(100), + employee_number NVARCHAR(50) UNIQUE, + is_active BIT DEFAULT 1, + is_admin BIT DEFAULT 0, + mfa_enabled BIT DEFAULT 0, + azure_oid NVARCHAR(255), + sso_provider NVARCHAR(50), + failed_login_attempts INT DEFAULT 0, + last_failed_login DATETIME2, + locked_until DATETIME2, + password_changed_at DATETIME2, + must_change_password BIT DEFAULT 0, + last_login_at DATETIME2, + created_at DATETIME2 DEFAULT GETDATE(), + updated_at DATETIME2 DEFAULT GETDATE(), + created_by BIGINT, + updated_by BIGINT +); + +CREATE TABLE wbx_roles ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + code NVARCHAR(50) NOT NULL UNIQUE, + name NVARCHAR(100) NOT NULL, + description NVARCHAR(500), + is_system BIT DEFAULT 0, + created_at DATETIME2 DEFAULT GETDATE(), + updated_at DATETIME2 DEFAULT GETDATE() +); + +CREATE TABLE wbx_user_roles ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + user_id BIGINT NOT NULL FOREIGN KEY REFERENCES wbx_users(id), + role_id BIGINT NOT NULL FOREIGN KEY REFERENCES wbx_roles(id), + CONSTRAINT uk_wbx_user_role UNIQUE (user_id, role_id) +); + +CREATE TABLE wbx_role_permissions ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + role_id BIGINT NOT NULL FOREIGN KEY REFERENCES wbx_roles(id), + module NVARCHAR(100) NOT NULL, + action NVARCHAR(50) NOT NULL, + dept_scope NVARCHAR(20) DEFAULT 'OWN', + CONSTRAINT uk_wbx_role_perm UNIQUE (role_id, module, action) +); + +CREATE TABLE wbx_refresh_tokens ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + user_id BIGINT NOT NULL FOREIGN KEY REFERENCES wbx_users(id), + token_hash NVARCHAR(500) NOT NULL, + device_info NVARCHAR(500), + ip_address NVARCHAR(50), + expires_at DATETIME2 NOT NULL, + created_at DATETIME2 DEFAULT GETDATE() +); + +CREATE TABLE wbx_totp_secrets ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + user_id BIGINT NOT NULL UNIQUE FOREIGN KEY REFERENCES wbx_users(id), + encrypted_secret NVARCHAR(500) NOT NULL, + verified BIT DEFAULT 0, + backup_codes NVARCHAR(2000), + created_at DATETIME2 DEFAULT GETDATE(), + updated_at DATETIME2 DEFAULT GETDATE(), + created_by BIGINT, + updated_by BIGINT +); + +CREATE TABLE wbx_login_history ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + user_id BIGINT, + email NVARCHAR(255), + action NVARCHAR(50), + auth_method NVARCHAR(50), + ip_address NVARCHAR(50), + user_agent NVARCHAR(500), + failure_reason NVARCHAR(500), + created_at DATETIME2 DEFAULT GETDATE() +); + +CREATE TABLE wbx_notifications ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + user_id BIGINT NOT NULL FOREIGN KEY REFERENCES wbx_users(id), + title NVARCHAR(500), + message NVARCHAR(MAX), + type NVARCHAR(50), + is_read BIT DEFAULT 0, + link NVARCHAR(1000), + created_at DATETIME2 DEFAULT GETDATE() +); + +CREATE TABLE wbx_audit_logs ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + user_id BIGINT, + username NVARCHAR(255), + action NVARCHAR(50) NOT NULL, + resource NVARCHAR(100) NOT NULL, + resource_id BIGINT, + detail NVARCHAR(4000), + ip_address NVARCHAR(50), + created_at DATETIME2 DEFAULT GETDATE() +); + +CREATE TABLE wbx_file_uploads ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + user_id BIGINT, + original_name NVARCHAR(500), + stored_name NVARCHAR(500), + file_key NVARCHAR(500), + content_type NVARCHAR(200), + file_size BIGINT, + category NVARCHAR(100), + created_at DATETIME2 DEFAULT GETDATE() +); + +CREATE TABLE wbx_system_config ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + config_key NVARCHAR(100) NOT NULL UNIQUE, + config_value NVARCHAR(4000), + value_type NVARCHAR(20) DEFAULT 'STRING', + description NVARCHAR(500), + is_editable BIT DEFAULT 1, + updated_at DATETIME2 DEFAULT GETDATE(), + updated_by BIGINT +); diff --git a/wbx-spring-core/src/main/resources/db/migration/mysql/V001__create_tables.sql b/wbx-spring-core/src/main/resources/db/migration/mysql/V001__create_tables.sql new file mode 100644 index 0000000..d502025 --- /dev/null +++ b/wbx-spring-core/src/main/resources/db/migration/mysql/V001__create_tables.sql @@ -0,0 +1,141 @@ +-- WBX Spring Framework — MySQL 스키마 (utf8mb4) +-- Hibernate ddl-auto와 병행 사용 가능 (Flyway 활성화 시 이 파일 사용) + +CREATE TABLE IF NOT EXISTS wbx_users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + username VARCHAR(100) NOT NULL UNIQUE, + hashed_password VARCHAR(500), + full_name VARCHAR(255), + phone VARCHAR(50), + department_id BIGINT, + position_title VARCHAR(100), + employee_number VARCHAR(50) UNIQUE, + is_active TINYINT(1) DEFAULT 1, + is_admin TINYINT(1) DEFAULT 0, + mfa_enabled TINYINT(1) DEFAULT 0, + azure_oid VARCHAR(255), + sso_provider VARCHAR(50), + failed_login_attempts INT DEFAULT 0, + last_failed_login DATETIME, + locked_until DATETIME, + password_changed_at DATETIME, + must_change_password TINYINT(1) DEFAULT 0, + last_login_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + created_by BIGINT, + updated_by BIGINT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS wbx_roles ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + code VARCHAR(50) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + description VARCHAR(500), + is_system TINYINT(1) DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS wbx_user_roles ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + role_id BIGINT NOT NULL, + UNIQUE KEY uk_user_role (user_id, role_id), + FOREIGN KEY (user_id) REFERENCES wbx_users(id), + FOREIGN KEY (role_id) REFERENCES wbx_roles(id) +) ENGINE=InnoDB; + +CREATE TABLE IF NOT EXISTS wbx_role_permissions ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + role_id BIGINT NOT NULL, + module VARCHAR(100) NOT NULL, + action VARCHAR(50) NOT NULL, + dept_scope VARCHAR(20) DEFAULT 'OWN', + UNIQUE KEY uk_role_perm (role_id, module, action), + FOREIGN KEY (role_id) REFERENCES wbx_roles(id) +) ENGINE=InnoDB; + +CREATE TABLE IF NOT EXISTS wbx_refresh_tokens ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + token_hash VARCHAR(500) NOT NULL, + device_info VARCHAR(500), + ip_address VARCHAR(50), + expires_at DATETIME NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES wbx_users(id) +) ENGINE=InnoDB; + +CREATE TABLE IF NOT EXISTS wbx_totp_secrets ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL UNIQUE, + encrypted_secret VARCHAR(500) NOT NULL, + verified TINYINT(1) DEFAULT 0, + backup_codes VARCHAR(2000), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + created_by BIGINT, + updated_by BIGINT, + FOREIGN KEY (user_id) REFERENCES wbx_users(id) +) ENGINE=InnoDB; + +CREATE TABLE IF NOT EXISTS wbx_login_history ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT, + email VARCHAR(255), + action VARCHAR(50), + auth_method VARCHAR(50), + ip_address VARCHAR(50), + user_agent VARCHAR(500), + failure_reason VARCHAR(500), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB; + +CREATE TABLE IF NOT EXISTS wbx_notifications ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + title VARCHAR(500), + message TEXT, + type VARCHAR(50), + is_read TINYINT(1) DEFAULT 0, + link VARCHAR(1000), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES wbx_users(id) +) ENGINE=InnoDB; + +CREATE TABLE IF NOT EXISTS wbx_audit_logs ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT, + username VARCHAR(255), + action VARCHAR(50) NOT NULL, + resource VARCHAR(100) NOT NULL, + resource_id BIGINT, + detail VARCHAR(4000), + ip_address VARCHAR(50), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB; + +CREATE TABLE IF NOT EXISTS wbx_file_uploads ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT, + original_name VARCHAR(500), + stored_name VARCHAR(500), + file_key VARCHAR(500), + content_type VARCHAR(200), + file_size BIGINT, + category VARCHAR(100), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB; + +CREATE TABLE IF NOT EXISTS wbx_system_config ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + config_key VARCHAR(100) NOT NULL UNIQUE, + config_value VARCHAR(4000), + value_type VARCHAR(20) DEFAULT 'STRING', + description VARCHAR(500), + is_editable TINYINT(1) DEFAULT 1, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + updated_by BIGINT +) ENGINE=InnoDB; diff --git a/wbx-spring-core/src/main/resources/db/migration/oracle/V001__create_tables.sql b/wbx-spring-core/src/main/resources/db/migration/oracle/V001__create_tables.sql new file mode 100644 index 0000000..ca75ced --- /dev/null +++ b/wbx-spring-core/src/main/resources/db/migration/oracle/V001__create_tables.sql @@ -0,0 +1,134 @@ +-- WBX Spring Framework — Oracle 스키마 + +CREATE TABLE wbx_users ( + id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + email VARCHAR2(255) NOT NULL UNIQUE, + username VARCHAR2(100) NOT NULL UNIQUE, + hashed_password VARCHAR2(500), + full_name VARCHAR2(255), + phone VARCHAR2(50), + department_id NUMBER(19), + position_title VARCHAR2(100), + employee_number VARCHAR2(50) UNIQUE, + is_active NUMBER(1) DEFAULT 1, + is_admin NUMBER(1) DEFAULT 0, + mfa_enabled NUMBER(1) DEFAULT 0, + azure_oid VARCHAR2(255), + sso_provider VARCHAR2(50), + failed_login_attempts NUMBER(10) DEFAULT 0, + last_failed_login TIMESTAMP, + locked_until TIMESTAMP, + password_changed_at TIMESTAMP, + must_change_password NUMBER(1) DEFAULT 0, + last_login_at TIMESTAMP, + created_at TIMESTAMP DEFAULT SYSTIMESTAMP, + updated_at TIMESTAMP DEFAULT SYSTIMESTAMP, + created_by NUMBER(19), + updated_by NUMBER(19) +); + +CREATE TABLE wbx_roles ( + id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + code VARCHAR2(50) NOT NULL UNIQUE, + name VARCHAR2(100) NOT NULL, + description VARCHAR2(500), + is_system NUMBER(1) DEFAULT 0, + created_at TIMESTAMP DEFAULT SYSTIMESTAMP, + updated_at TIMESTAMP DEFAULT SYSTIMESTAMP +); + +CREATE TABLE wbx_user_roles ( + id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id NUMBER(19) NOT NULL REFERENCES wbx_users(id), + role_id NUMBER(19) NOT NULL REFERENCES wbx_roles(id), + CONSTRAINT uk_wbx_user_role UNIQUE (user_id, role_id) +); + +CREATE TABLE wbx_role_permissions ( + id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + role_id NUMBER(19) NOT NULL REFERENCES wbx_roles(id), + module VARCHAR2(100) NOT NULL, + action VARCHAR2(50) NOT NULL, + dept_scope VARCHAR2(20) DEFAULT 'OWN', + CONSTRAINT uk_wbx_role_perm UNIQUE (role_id, module, action) +); + +CREATE TABLE wbx_refresh_tokens ( + id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id NUMBER(19) NOT NULL REFERENCES wbx_users(id), + token_hash VARCHAR2(500) NOT NULL, + device_info VARCHAR2(500), + ip_address VARCHAR2(50), + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT SYSTIMESTAMP +); + +CREATE TABLE wbx_totp_secrets ( + id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id NUMBER(19) NOT NULL UNIQUE REFERENCES wbx_users(id), + encrypted_secret VARCHAR2(500) NOT NULL, + verified NUMBER(1) DEFAULT 0, + backup_codes VARCHAR2(2000), + created_at TIMESTAMP DEFAULT SYSTIMESTAMP, + updated_at TIMESTAMP DEFAULT SYSTIMESTAMP, + created_by NUMBER(19), + updated_by NUMBER(19) +); + +CREATE TABLE wbx_login_history ( + id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id NUMBER(19), + email VARCHAR2(255), + action VARCHAR2(50), + auth_method VARCHAR2(50), + ip_address VARCHAR2(50), + user_agent VARCHAR2(500), + failure_reason VARCHAR2(500), + created_at TIMESTAMP DEFAULT SYSTIMESTAMP +); + +CREATE TABLE wbx_notifications ( + id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id NUMBER(19) NOT NULL REFERENCES wbx_users(id), + title VARCHAR2(500), + message CLOB, + type VARCHAR2(50), + is_read NUMBER(1) DEFAULT 0, + link VARCHAR2(1000), + created_at TIMESTAMP DEFAULT SYSTIMESTAMP +); + +CREATE TABLE wbx_audit_logs ( + id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id NUMBER(19), + username VARCHAR2(255), + action VARCHAR2(50) NOT NULL, + resource VARCHAR2(100) NOT NULL, + resource_id NUMBER(19), + detail VARCHAR2(4000), + ip_address VARCHAR2(50), + created_at TIMESTAMP DEFAULT SYSTIMESTAMP +); + +CREATE TABLE wbx_file_uploads ( + id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id NUMBER(19), + original_name VARCHAR2(500), + stored_name VARCHAR2(500), + file_key VARCHAR2(500), + content_type VARCHAR2(200), + file_size NUMBER(19), + category VARCHAR2(100), + created_at TIMESTAMP DEFAULT SYSTIMESTAMP +); + +CREATE TABLE wbx_system_config ( + id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + config_key VARCHAR2(100) NOT NULL UNIQUE, + config_value VARCHAR2(4000), + value_type VARCHAR2(20) DEFAULT 'STRING', + description VARCHAR2(500), + is_editable NUMBER(1) DEFAULT 1, + updated_at TIMESTAMP DEFAULT SYSTIMESTAMP, + updated_by NUMBER(19) +); diff --git a/wbx-spring-core/src/main/resources/db/migration/postgresql/V001__create_tables.sql b/wbx-spring-core/src/main/resources/db/migration/postgresql/V001__create_tables.sql new file mode 100644 index 0000000..256ec45 --- /dev/null +++ b/wbx-spring-core/src/main/resources/db/migration/postgresql/V001__create_tables.sql @@ -0,0 +1,134 @@ +-- WBX Spring Framework — PostgreSQL 스키마 + +CREATE TABLE IF NOT EXISTS wbx_users ( + id BIGSERIAL PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + username VARCHAR(100) NOT NULL UNIQUE, + hashed_password VARCHAR(500), + full_name VARCHAR(255), + phone VARCHAR(50), + department_id BIGINT, + position_title VARCHAR(100), + employee_number VARCHAR(50) UNIQUE, + is_active BOOLEAN DEFAULT TRUE, + is_admin BOOLEAN DEFAULT FALSE, + mfa_enabled BOOLEAN DEFAULT FALSE, + azure_oid VARCHAR(255), + sso_provider VARCHAR(50), + failed_login_attempts INT DEFAULT 0, + last_failed_login TIMESTAMP, + locked_until TIMESTAMP, + password_changed_at TIMESTAMP, + must_change_password BOOLEAN DEFAULT FALSE, + last_login_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + created_by BIGINT, + updated_by BIGINT +); + +CREATE TABLE IF NOT EXISTS wbx_roles ( + id BIGSERIAL PRIMARY KEY, + code VARCHAR(50) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + description VARCHAR(500), + is_system BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS wbx_user_roles ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES wbx_users(id), + role_id BIGINT NOT NULL REFERENCES wbx_roles(id), + UNIQUE (user_id, role_id) +); + +CREATE TABLE IF NOT EXISTS wbx_role_permissions ( + id BIGSERIAL PRIMARY KEY, + role_id BIGINT NOT NULL REFERENCES wbx_roles(id), + module VARCHAR(100) NOT NULL, + action VARCHAR(50) NOT NULL, + dept_scope VARCHAR(20) DEFAULT 'OWN', + UNIQUE (role_id, module, action) +); + +CREATE TABLE IF NOT EXISTS wbx_refresh_tokens ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES wbx_users(id), + token_hash VARCHAR(500) NOT NULL, + device_info VARCHAR(500), + ip_address VARCHAR(50), + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS wbx_totp_secrets ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL UNIQUE REFERENCES wbx_users(id), + encrypted_secret VARCHAR(500) NOT NULL, + verified BOOLEAN DEFAULT FALSE, + backup_codes VARCHAR(2000), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + created_by BIGINT, + updated_by BIGINT +); + +CREATE TABLE IF NOT EXISTS wbx_login_history ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT, + email VARCHAR(255), + action VARCHAR(50), + auth_method VARCHAR(50), + ip_address VARCHAR(50), + user_agent VARCHAR(500), + failure_reason VARCHAR(500), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS wbx_notifications ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES wbx_users(id), + title VARCHAR(500), + message TEXT, + type VARCHAR(50), + is_read BOOLEAN DEFAULT FALSE, + link VARCHAR(1000), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS wbx_audit_logs ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT, + username VARCHAR(255), + action VARCHAR(50) NOT NULL, + resource VARCHAR(100) NOT NULL, + resource_id BIGINT, + detail VARCHAR(4000), + ip_address VARCHAR(50), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS wbx_file_uploads ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT, + original_name VARCHAR(500), + stored_name VARCHAR(500), + file_key VARCHAR(500), + content_type VARCHAR(200), + file_size BIGINT, + category VARCHAR(100), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS wbx_system_config ( + id BIGSERIAL PRIMARY KEY, + config_key VARCHAR(100) NOT NULL UNIQUE, + config_value VARCHAR(4000), + value_type VARCHAR(20) DEFAULT 'STRING', + description VARCHAR(500), + is_editable BOOLEAN DEFAULT TRUE, + updated_at TIMESTAMP DEFAULT NOW(), + updated_by BIGINT +); diff --git a/wbx-spring-core/src/main/resources/static/admin/css/admin.css b/wbx-spring-core/src/main/resources/static/admin/css/admin.css new file mode 100644 index 0000000..c94cf89 --- /dev/null +++ b/wbx-spring-core/src/main/resources/static/admin/css/admin.css @@ -0,0 +1,64 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } +body { font-family: 'Malgun Gothic', -apple-system, sans-serif; background: #f0f2f5; color: #333; } + +/* Layout */ +.admin-layout { display: flex; min-height: 100vh; } +.admin-sidebar { width: 220px; background: #1e3c78; color: #fff; padding: 0; flex-shrink: 0; } +.admin-sidebar .logo { padding: 20px; font-size: 18px; font-weight: bold; border-bottom: 1px solid rgba(255,255,255,0.1); } +.admin-sidebar nav a { display: block; padding: 12px 20px; color: rgba(255,255,255,0.7); text-decoration: none; font-size: 14px; border-left: 3px solid transparent; } +.admin-sidebar nav a:hover { background: rgba(255,255,255,0.05); color: #fff; } +.admin-sidebar nav a.active { background: rgba(255,255,255,0.1); color: #fff; border-left-color: #4da6ff; } +.admin-content { flex: 1; padding: 24px; } + +/* Header */ +.page-header { margin-bottom: 24px; } +.page-header h1 { font-size: 22px; color: #1e3c78; } +.page-header p { color: #888; font-size: 13px; margin-top: 4px; } + +/* Cards */ +.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; } +.stat-card { background: #fff; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); } +.stat-card .label { font-size: 13px; color: #888; } +.stat-card .value { font-size: 28px; font-weight: bold; color: #1e3c78; margin-top: 4px; } +.stat-card .value.green { color: #2e7d32; } +.stat-card .value.orange { color: #e65100; } + +/* Table */ +.data-table { width: 100%; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); } +.data-table table { width: 100%; border-collapse: collapse; } +.data-table th { background: #1e3c78; color: #fff; padding: 10px 14px; text-align: left; font-size: 13px; font-weight: 500; } +.data-table td { padding: 10px 14px; border-bottom: 1px solid #f0f0f0; font-size: 13px; } +.data-table tr:hover td { background: #f8f9ff; } + +/* Badges */ +.badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 500; } +.badge-success { background: #e8f5e9; color: #2e7d32; } +.badge-danger { background: #ffebee; color: #c62828; } +.badge-warning { background: #fff8e1; color: #e65100; } +.badge-info { background: #e3f2fd; color: #1565c0; } + +/* Buttons */ +.btn { display: inline-block; padding: 6px 14px; border-radius: 4px; font-size: 13px; text-decoration: none; border: none; cursor: pointer; } +.btn-primary { background: #1e3c78; color: #fff; } +.btn-danger { background: #c62828; color: #fff; } +.btn-outline { background: #fff; border: 1px solid #ddd; color: #555; } +.btn:hover { opacity: 0.85; } + +/* Alert */ +.alert { padding: 12px 16px; border-radius: 6px; margin-bottom: 16px; font-size: 13px; } +.alert-success { background: #e8f5e9; color: #2e7d32; border: 1px solid #c8e6c9; } +.alert-info { background: #e3f2fd; color: #1565c0; } + +/* Detail */ +.detail-grid { display: grid; grid-template-columns: 140px 1fr; gap: 8px 16px; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); margin-bottom: 16px; } +.detail-grid .label { font-weight: 500; color: #888; font-size: 13px; } +.detail-grid .value { font-size: 14px; } + +/* Login */ +.login-container { display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #1e3c78; } +.login-box { background: #fff; padding: 40px; border-radius: 12px; width: 380px; box-shadow: 0 4px 20px rgba(0,0,0,0.2); } +.login-box h2 { text-align: center; color: #1e3c78; margin-bottom: 24px; } +.login-box input { width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 6px; margin-bottom: 12px; font-size: 14px; } +.login-box button { width: 100%; padding: 12px; background: #1e3c78; color: #fff; border: none; border-radius: 6px; font-size: 15px; cursor: pointer; } +.login-box button:hover { background: #15306a; } +.login-box .error { color: #c62828; font-size: 13px; text-align: center; margin-bottom: 12px; } diff --git a/wbx-spring-core/src/main/resources/templates/admin/audit-logs.html b/wbx-spring-core/src/main/resources/templates/admin/audit-logs.html new file mode 100644 index 0000000..77247d8 --- /dev/null +++ b/wbx-spring-core/src/main/resources/templates/admin/audit-logs.html @@ -0,0 +1,28 @@ + + +WBX Admin - Audit Logs + +
+
+
+ +
+ + + + + + + + + + + + + +
시간사용자ID액션리소스리소스IDIP상세
+
+
+
+ + diff --git a/wbx-spring-core/src/main/resources/templates/admin/config.html b/wbx-spring-core/src/main/resources/templates/admin/config.html new file mode 100644 index 0000000..f1deb52 --- /dev/null +++ b/wbx-spring-core/src/main/resources/templates/admin/config.html @@ -0,0 +1,39 @@ + + +WBX Admin - System Config + +
+
+
+ +
+ + +
+

설정 추가/수정

+
+
+
+
+ +
+
+ + +
+ + + + + + + + + + +
KeyValue설명수정일
+
+
+
+ + diff --git a/wbx-spring-core/src/main/resources/templates/admin/dashboard.html b/wbx-spring-core/src/main/resources/templates/admin/dashboard.html new file mode 100644 index 0000000..728d6fe --- /dev/null +++ b/wbx-spring-core/src/main/resources/templates/admin/dashboard.html @@ -0,0 +1,43 @@ + + + + + WBX Admin - Dashboard + + + +
+
+
+ + +
+
+
활성 사용자
+
0
+
+
+
전체 사용자
+
0
+
+
+
로그인 성공
+
0
+
+
+
등록 역할
+
0
+
+
+ + + 사용자 관리 + 역할 관리 + 로그인 이력 +
+
+ + diff --git a/wbx-spring-core/src/main/resources/templates/admin/fragments.html b/wbx-spring-core/src/main/resources/templates/admin/fragments.html new file mode 100644 index 0000000..09c43b2 --- /dev/null +++ b/wbx-spring-core/src/main/resources/templates/admin/fragments.html @@ -0,0 +1,19 @@ + + + +
+ + +
+ + diff --git a/wbx-spring-core/src/main/resources/templates/admin/login-history.html b/wbx-spring-core/src/main/resources/templates/admin/login-history.html new file mode 100644 index 0000000..06b7e79 --- /dev/null +++ b/wbx-spring-core/src/main/resources/templates/admin/login-history.html @@ -0,0 +1,31 @@ + + +WBX Admin - Login History + +
+
+
+ +
+ + + + + + + + + + + + +
시간이메일액션IP인증방법사유
+ 성공 + 실패 + 로그아웃 +
+
+
+
+ + diff --git a/wbx-spring-core/src/main/resources/templates/admin/login.html b/wbx-spring-core/src/main/resources/templates/admin/login.html new file mode 100644 index 0000000..380b4ff --- /dev/null +++ b/wbx-spring-core/src/main/resources/templates/admin/login.html @@ -0,0 +1,22 @@ + + + + + WBX Admin - Login + + + +
+
+

WBX Admin

+
이메일 또는 비밀번호가 올바르지 않습니다.
+
로그아웃되었습니다.
+
+ + + +
+
+
+ + diff --git a/wbx-spring-core/src/main/resources/templates/admin/permissions.html b/wbx-spring-core/src/main/resources/templates/admin/permissions.html new file mode 100644 index 0000000..d4222d2 --- /dev/null +++ b/wbx-spring-core/src/main/resources/templates/admin/permissions.html @@ -0,0 +1,41 @@ + + +WBX Admin - Permissions + +
+
+
+ +
+ + + + + + + + + + + + +
역할모듈액션범위관리
+ + + + + + +
+ +
+
등록된 권한이 없습니다. 역할 관리에서 권한을 추가하세요.
+
+
+
+ + diff --git a/wbx-spring-core/src/main/resources/templates/admin/role-detail.html b/wbx-spring-core/src/main/resources/templates/admin/role-detail.html new file mode 100644 index 0000000..e244fa8 --- /dev/null +++ b/wbx-spring-core/src/main/resources/templates/admin/role-detail.html @@ -0,0 +1,89 @@ + + +WBX Admin - Role Detail + +
+
+
+ +
+
+ + +
+

역할 정보

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+

권한 추가

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+ + + + + + + + + + + +
모듈액션범위관리
+ + +
+ +
+
설정된 권한이 없습니다. 위 폼에서 권한을 추가하세요.
+
+
+
+ + diff --git a/wbx-spring-core/src/main/resources/templates/admin/roles.html b/wbx-spring-core/src/main/resources/templates/admin/roles.html new file mode 100644 index 0000000..b50e9ae --- /dev/null +++ b/wbx-spring-core/src/main/resources/templates/admin/roles.html @@ -0,0 +1,57 @@ + + +WBX Admin - Roles + +
+
+
+ +
+
+ + +
+

새 역할 추가

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+ + + + + + + + + + + + + +
ID코드이름설명시스템관리
+ 상세/권한 +
+ +
+
등록된 역할이 없습니다.
+
+
+
+ + diff --git a/wbx-spring-core/src/main/resources/templates/admin/system-health.html b/wbx-spring-core/src/main/resources/templates/admin/system-health.html new file mode 100644 index 0000000..7de134a --- /dev/null +++ b/wbx-spring-core/src/main/resources/templates/admin/system-health.html @@ -0,0 +1,23 @@ + + +WBX Admin - System Health + +
+
+
+ +
+ + + + + + + + +
항목
+
+
+
+ + diff --git a/wbx-spring-core/src/main/resources/templates/admin/user-detail.html b/wbx-spring-core/src/main/resources/templates/admin/user-detail.html new file mode 100644 index 0000000..00b0071 --- /dev/null +++ b/wbx-spring-core/src/main/resources/templates/admin/user-detail.html @@ -0,0 +1,154 @@ + + +WBX Admin - User Detail + +
+
+
+ +
+
+ + +
+

기본 정보

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+

계정 상태

+
+
상태
+
+ 활성 + 비활성 +
+
잠금
+
+ 잠금 (실패 회) + 정상 +
+
MFA
+
+
SSO
+
+
최종 로그인
+
+
생성일
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+

역할 할당

+
+
+ + +
+ +
+
+ + + + + + + + + + +
역할 코드역할 이름할당일관리
+ + + + +
+ +
+
+
+

할당된 역할이 없습니다.

+
+ + +
+

로그인 이력 (최근 10건)

+
+ + + + + + + + + + + +
시간액션IP인증방법
+ 성공 + 실패 + 로그아웃 +
이력이 없습니다.
+
+
+
+
+ + diff --git a/wbx-spring-core/src/main/resources/templates/admin/users.html b/wbx-spring-core/src/main/resources/templates/admin/users.html new file mode 100644 index 0000000..96743a2 --- /dev/null +++ b/wbx-spring-core/src/main/resources/templates/admin/users.html @@ -0,0 +1,73 @@ + + +WBX Admin - Users + +
+
+
+ +
+
+ + +
+

새 사용자 추가

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + +
ID이메일사용자명이름관리자상태잠금최종 로그인관리
SA + 활성 + 비활성 + 잠금 + 상세 +
+
+
+
+ + diff --git a/src/test/java/kr/co/accura/wbx/spring/WbxSpringCoreApplicationTests.java b/wbx-spring-core/src/test/java/kr/co/accura/wbx/spring/WbxSpringCoreApplicationTests.java similarity index 100% rename from src/test/java/kr/co/accura/wbx/spring/WbxSpringCoreApplicationTests.java rename to wbx-spring-core/src/test/java/kr/co/accura/wbx/spring/WbxSpringCoreApplicationTests.java diff --git a/wtm-api/build.gradle b/wtm-api/build.gradle new file mode 100644 index 0000000..adabfc7 --- /dev/null +++ b/wtm-api/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'org.springframework.boot' version '3.5.0' + id 'io.spring.dependency-management' version '1.1.7' +} + +dependencies { + // wbx-spring 프레임워크 + implementation project(':wbx-spring-core') + + // WTM 전용 + implementation 'org.apache.poi:poi-ooxml:5.3.0' // P6 WBS, Excel 업로드/다운로드 + + // QueryDSL — Phase 2에서 도입 예정 (현재 native query 사용) + // implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' + // annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' + + // MapStruct (DTO 매핑) + implementation 'org.mapstruct:mapstruct:1.6.3' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3' + + // Annotation processor ordering: Lombok → MapStruct → QueryDSL + annotationProcessor 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' + + // Flyway — MySQL (개발) + Azure SQL (운영) + implementation 'org.flywaydb:flyway-mysql' + implementation 'org.flywaydb:flyway-sqlserver' + + // DB Driver — 개발: MySQL, 운영: MSSQL (Azure SQL) + runtimeOnly 'com.mysql:mysql-connector-j' + runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11' + + // Test + testRuntimeOnly 'com.h2database:h2' +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/WtmApplication.java b/wtm-api/src/main/java/kr/co/accura/wtm/WtmApplication.java new file mode 100644 index 0000000..b33ea8b --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/WtmApplication.java @@ -0,0 +1,24 @@ +package kr.co.accura.wtm; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@SpringBootApplication(scanBasePackages = { + "kr.co.accura.wbx.spring", + "kr.co.accura.wtm" +}) +@EntityScan(basePackages = { + "kr.co.accura.wbx.spring", + "kr.co.accura.wtm" +}) +@EnableJpaRepositories(basePackages = { + "kr.co.accura.wbx.spring", + "kr.co.accura.wtm" +}) +public class WtmApplication { + public static void main(String[] args) { + SpringApplication.run(WtmApplication.class, args); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/api/ApprovalController.java b/wtm-api/src/main/java/kr/co/accura/wtm/api/ApprovalController.java new file mode 100644 index 0000000..5bffa04 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/api/ApprovalController.java @@ -0,0 +1,75 @@ +package kr.co.accura.wtm.api; + +import kr.co.accura.wbx.spring.common.SecurityUtils; +import kr.co.accura.wtm.domain.approval.dto.*; +import kr.co.accura.wtm.domain.approval.service.ApprovalService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/wtm/approvals") +@RequiredArgsConstructor +public class ApprovalController { + + private final ApprovalService approvalService; + + @GetMapping("/pending") + public Page pending(Pageable pageable) { + return approvalService.getPending(SecurityUtils.getCurrentUserId(), pageable); + } + + @PostMapping("/{approvalId}/approve") + public ResponseEntity> approve( + @PathVariable Long approvalId, + @RequestBody(required = false) ApprovalActionRequest request) { + approvalService.approve(approvalId, SecurityUtils.getCurrentUserId(), + request != null ? request.comment() : null); + return ResponseEntity.ok(Map.of("message", "승인 완료")); + } + + @PostMapping("/{approvalId}/reject") + public ResponseEntity> reject( + @PathVariable Long approvalId, + @RequestBody(required = false) ApprovalActionRequest request) { + approvalService.reject(approvalId, SecurityUtils.getCurrentUserId(), + request != null ? request.comment() : null); + return ResponseEntity.ok(Map.of("message", "반려 완료")); + } + + @PostMapping("/batch-approve") + public ResponseEntity> batchApprove( + @RequestBody BatchApproveRequest request) { + approvalService.batchApprove(request.lineIds(), SecurityUtils.getCurrentUserId(), request.comment()); + return ResponseEntity.ok(Map.of("message", "일괄 승인 완료")); + } + + @PostMapping("/{approvalId}/comments") + public ResponseEntity> addComment( + @PathVariable Long approvalId, + @RequestBody ApprovalActionRequest request) { + approvalService.addComment(approvalId, SecurityUtils.getCurrentUserId(), request.comment()); + return ResponseEntity.ok(Map.of("message", "코멘트 등록 완료")); + } + + @GetMapping("/{approvalId}") + public ApprovalDto getDetail(@PathVariable Long approvalId) { + return approvalService.getApprovalDetail(approvalId); + } + + @GetMapping("/history") + public Page history(Pageable pageable) { + return approvalService.getHistory(SecurityUtils.getCurrentUserId(), pageable); + } + + @GetMapping("/overdue") + public Page overdue(Pageable pageable) { + // For now returns pending items — can be enhanced with date filter + return approvalService.getPending(SecurityUtils.getCurrentUserId(), pageable); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/api/HomeController.java b/wtm-api/src/main/java/kr/co/accura/wtm/api/HomeController.java new file mode 100644 index 0000000..6c75e68 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/api/HomeController.java @@ -0,0 +1,67 @@ +package kr.co.accura.wtm.api; + +import kr.co.accura.wbx.spring.common.SecurityUtils; +import kr.co.accura.wtm.domain.approval.repository.TtApprovalLineRepository; +import kr.co.accura.wtm.domain.timesheet.TimesheetStatus; +import kr.co.accura.wtm.domain.timesheet.repository.TimesheetRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/wtm/home") +@RequiredArgsConstructor +public class HomeController { + + private final TimesheetRepository timesheetRepository; + private final TtApprovalLineRepository approvalLineRepository; + + @GetMapping("/dashboard") + public Map dashboard() { + Long userId = SecurityUtils.getCurrentUserId(); + Map data = new HashMap<>(); + + // Current week timesheet status + var recentTimesheets = timesheetRepository.findByUserId(userId, PageRequest.of(0, 5)); + data.put("recentTimesheets", recentTimesheets.getContent().stream().map(ts -> + Map.of( + "id", ts.getId(), + "weekStartDate", ts.getWeekStartDate().toString(), + "status", ts.getStatus().name(), + "totalHours", ts.getTotalHours() + )).toList()); + + // Pending approvals count + var pendingApprovals = approvalLineRepository.findPendingByApproverId( + userId, PageRequest.of(0, 1)); + data.put("pendingApprovalCount", pendingApprovals.getTotalElements()); + + return data; + } + + @GetMapping("/notifications") + public Map notifications() { + Long userId = SecurityUtils.getCurrentUserId(); + Map data = new HashMap<>(); + + // Pending approval notifications + var pendingLines = approvalLineRepository.findPendingByApproverId( + userId, PageRequest.of(0, 10)); + data.put("items", pendingLines.getContent().stream().map(line -> + Map.of( + "type", "APPROVAL_REQUEST", + "title", "시수 결재 요청", + "message", "결재 대기 항목이 있습니다. (결재라인 #" + line.getId() + ")", + "createdAt", line.getCreatedAt() != null ? line.getCreatedAt().toString() : "" + )).toList()); + data.put("total", pendingLines.getTotalElements()); + + return data; + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/api/HrIntegrationController.java b/wtm-api/src/main/java/kr/co/accura/wtm/api/HrIntegrationController.java new file mode 100644 index 0000000..26323cf --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/api/HrIntegrationController.java @@ -0,0 +1,26 @@ +package kr.co.accura.wtm.api; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Map; + +@RestController +@RequestMapping("/api/wtm/integration/hr") +@RequiredArgsConstructor +public class HrIntegrationController { + + @PostMapping("/upload") + public ResponseEntity> upload(@RequestParam("file") MultipartFile file) { + // TODO: Implement HR data upload from Excel/CSV + return ResponseEntity.ok(Map.of("message", "HR sync not yet implemented")); + } + + @PostMapping("/sync") + public ResponseEntity> sync(@RequestBody Map request) { + // TODO: Implement HR system sync (SAP, etc.) + return ResponseEntity.ok(Map.of("message", "HR sync not yet implemented")); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/api/OverheadTypeController.java b/wtm-api/src/main/java/kr/co/accura/wtm/api/OverheadTypeController.java new file mode 100644 index 0000000..66e7055 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/api/OverheadTypeController.java @@ -0,0 +1,31 @@ +package kr.co.accura.wtm.api; + +import kr.co.accura.wtm.domain.config.dto.OverheadTypeDto; +import kr.co.accura.wtm.domain.config.service.OverheadTypeService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/wtm/overhead-types") +@RequiredArgsConstructor +public class OverheadTypeController { + + private final OverheadTypeService overheadTypeService; + + @GetMapping + public List list() { + return overheadTypeService.getAll(); + } + + @PostMapping + public OverheadTypeDto create(@RequestBody OverheadTypeDto dto) { + return overheadTypeService.create(dto); + } + + @PutMapping("/{id}") + public OverheadTypeDto update(@PathVariable Long id, @RequestBody OverheadTypeDto dto) { + return overheadTypeService.update(id, dto); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/api/ProjectController.java b/wtm-api/src/main/java/kr/co/accura/wtm/api/ProjectController.java new file mode 100644 index 0000000..bf376ec --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/api/ProjectController.java @@ -0,0 +1,62 @@ +package kr.co.accura.wtm.api; + +import kr.co.accura.wtm.domain.project.dto.*; +import kr.co.accura.wtm.domain.project.service.ProjectService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/wtm/projects") +@RequiredArgsConstructor +public class ProjectController { + + private final ProjectService projectService; + + @GetMapping + public ResponseEntity> list(Pageable pageable) { + Page page = projectService.findAll(pageable); + return ResponseEntity.ok(Map.of( + "items", page.getContent(), + "total", page.getTotalElements() + )); + } + + @PostMapping + public ResponseEntity create(@RequestBody @Valid ProjectCreateRequest request) { + return ResponseEntity.ok(projectService.create(request)); + } + + @GetMapping("/{id}") + public ResponseEntity getById(@PathVariable Long id) { + return ResponseEntity.ok(projectService.findById(id)); + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable Long id, + @RequestBody @Valid ProjectCreateRequest request) { + return ResponseEntity.ok(projectService.update(id, request)); + } + + @GetMapping("/my") + public ResponseEntity> myProjects(@RequestParam Long userId) { + return ResponseEntity.ok(projectService.findMyProjects(userId)); + } + + @GetMapping("/{id}/members") + public ResponseEntity> getMembers(@PathVariable Long id) { + return ResponseEntity.ok(projectService.getAssignments(id)); + } + + @PostMapping("/{id}/members") + public ResponseEntity addMember(@PathVariable Long id, + @RequestBody @Valid AssignmentCreateRequest request) { + return ResponseEntity.ok(projectService.createAssignment(id, request)); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/api/ReportController.java b/wtm-api/src/main/java/kr/co/accura/wtm/api/ReportController.java new file mode 100644 index 0000000..83165f5 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/api/ReportController.java @@ -0,0 +1,81 @@ +package kr.co.accura.wtm.api; + +import kr.co.accura.wtm.domain.report.dto.ProjectHoursReport; +import kr.co.accura.wtm.domain.report.dto.ReportFilter; +import kr.co.accura.wtm.domain.report.dto.WbsHoursReport; +import kr.co.accura.wtm.domain.report.service.ReportService; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; + +@RestController +@RequestMapping("/api/wtm/reports") +@RequiredArgsConstructor +public class ReportController { + + private final ReportService reportService; + + @GetMapping("/project-hours") + public ProjectHoursReport projectHours( + @RequestParam(required = false) Long projectId, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to, + @RequestParam(required = false) String groupBy) { + return reportService.getProjectHoursReport( + new ReportFilter(projectId, from, to, groupBy, null, null)); + } + + @GetMapping("/project-hours/export") + public ProjectHoursReport projectHoursExport( + @RequestParam(required = false) Long projectId, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to, + @RequestParam(required = false) String groupBy) { + // TODO: Return Excel byte[] with proper content type + return reportService.getProjectHoursReport( + new ReportFilter(projectId, from, to, groupBy, null, null)); + } + + @GetMapping("/wbs-hours") + public WbsHoursReport wbsHours( + @RequestParam(required = false) Long projectId, + @RequestParam(required = false) Integer wbsLevel, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) { + return reportService.getWbsHoursReport( + new ReportFilter(projectId, from, to, null, wbsLevel, null)); + } + + @GetMapping("/wbs-hours/export") + public WbsHoursReport wbsHoursExport( + @RequestParam(required = false) Long projectId, + @RequestParam(required = false) Integer wbsLevel, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) { + // TODO: Return Excel byte[] with proper content type + return reportService.getWbsHoursReport( + new ReportFilter(projectId, from, to, null, wbsLevel, null)); + } + + @GetMapping("/phase-ratio") + public ProjectHoursReport phaseRatio( + @RequestParam(required = false) Long projectId, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) { + // TODO: Implement phase ratio logic + return reportService.getProjectHoursReport( + new ReportFilter(projectId, from, to, null, null, null)); + } + + @GetMapping("/np-ratio") + public ProjectHoursReport npRatio( + @RequestParam(required = false) String department, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) { + // TODO: Implement NP ratio logic + return reportService.getProjectHoursReport( + new ReportFilter(null, from, to, null, null, department)); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/api/ResourceAssignController.java b/wtm-api/src/main/java/kr/co/accura/wtm/api/ResourceAssignController.java new file mode 100644 index 0000000..b93b133 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/api/ResourceAssignController.java @@ -0,0 +1,59 @@ +package kr.co.accura.wtm.api; + +import kr.co.accura.wtm.domain.project.dto.*; +import kr.co.accura.wtm.domain.project.service.ProjectService; +import kr.co.accura.wtm.domain.user.dto.UserDto; +import kr.co.accura.wtm.domain.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/wtm/projects/{projectId}/assignments") +@RequiredArgsConstructor +public class ResourceAssignController { + + private final ProjectService projectService; + + @GetMapping + public ResponseEntity> list(@PathVariable Long projectId) { + return ResponseEntity.ok(projectService.getAssignments(projectId)); + } + + @PostMapping + public ResponseEntity create(@PathVariable Long projectId, + @RequestBody @Valid AssignmentCreateRequest request) { + return ResponseEntity.ok(projectService.createAssignment(projectId, request)); + } + + @PutMapping("/{assignId}") + public ResponseEntity update(@PathVariable Long projectId, + @PathVariable Long assignId, + @RequestBody @Valid AssignmentCreateRequest request) { + return ResponseEntity.ok(projectService.updateAssignment(projectId, assignId, request)); + } + + @DeleteMapping("/{assignId}") + public ResponseEntity delete(@PathVariable Long projectId, + @PathVariable Long assignId) { + projectService.deleteAssignment(projectId, assignId); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/available") + public ResponseEntity> getAvailable(@PathVariable Long projectId, + Pageable pageable) { + Page page = projectService.getAvailableUsers(projectId, pageable); + List users = page.getContent().stream().map(UserDto::from).toList(); + return ResponseEntity.ok(Map.of( + "items", users, + "total", page.getTotalElements() + )); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/api/TealController.java b/wtm-api/src/main/java/kr/co/accura/wtm/api/TealController.java new file mode 100644 index 0000000..7a575cb --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/api/TealController.java @@ -0,0 +1,46 @@ +package kr.co.accura.wtm.api; + +import kr.co.accura.wtm.domain.teal.dto.TealEntryDto; +import kr.co.accura.wtm.domain.teal.dto.TealVersionDto; +import kr.co.accura.wtm.domain.teal.service.TealService; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequestMapping("/api/wtm/projects/{projectId}/teal") +@RequiredArgsConstructor +public class TealController { + + private final TealService tealService; + + @PostMapping("/upload") + public ResponseEntity upload( + @PathVariable Long projectId, + @RequestParam("file") MultipartFile file, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate effectiveDate, + @RequestParam(required = false) String description) { + return ResponseEntity.ok(tealService.uploadTeal(projectId, file, effectiveDate, description)); + } + + @GetMapping("/versions") + public ResponseEntity> getVersions(@PathVariable Long projectId) { + return ResponseEntity.ok(tealService.getVersions(projectId)); + } + + @GetMapping("/active") + public ResponseEntity> getActiveEntries(@PathVariable Long projectId) { + return ResponseEntity.ok(tealService.getActiveTealEntries(projectId)); + } + + @GetMapping("/by-wbs/{wbsId}") + public ResponseEntity> getByWbs(@PathVariable Long projectId, + @PathVariable Long wbsId) { + return ResponseEntity.ok(tealService.getEntriesByWbs(projectId, wbsId)); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/api/TimesheetController.java b/wtm-api/src/main/java/kr/co/accura/wtm/api/TimesheetController.java new file mode 100644 index 0000000..6bef85f --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/api/TimesheetController.java @@ -0,0 +1,81 @@ +package kr.co.accura.wtm.api; + +import jakarta.validation.Valid; +import kr.co.accura.wbx.spring.common.SecurityUtils; +import kr.co.accura.wtm.domain.timesheet.dto.*; +import kr.co.accura.wtm.domain.timesheet.service.TimesheetService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequestMapping("/api/wtm/timesheets") +@RequiredArgsConstructor +public class TimesheetController { + + private final TimesheetService timesheetService; + + @GetMapping("/week") + public TimesheetDto getWeekly( + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate weekStart) { + return timesheetService.getOrCreateWeekly( + SecurityUtils.getCurrentUserId(), weekStart); + } + + @PostMapping("/{timesheetId}/entries") + public TimesheetEntryDto saveEntry( + @PathVariable Long timesheetId, + @Valid @RequestBody TimesheetEntryRequest request) { + return timesheetService.saveEntry(timesheetId, request); + } + + @PutMapping("/{timesheetId}/entries/batch") + public TimesheetDto saveBatch( + @PathVariable Long timesheetId, + @Valid @RequestBody List entries) { + return timesheetService.saveBatch(timesheetId, entries); + } + + @DeleteMapping("/{timesheetId}/entries/{entryId}") + public ResponseEntity deleteEntry( + @PathVariable Long timesheetId, + @PathVariable Long entryId) { + timesheetService.deleteEntry(timesheetId, entryId); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/{timesheetId}/submit") + public TimesheetDto submit(@PathVariable Long timesheetId) { + return timesheetService.submit(timesheetId); + } + + @PostMapping("/upload") + public UploadResultDto uploadExcel( + @RequestParam("file") MultipartFile file, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate weekStart) { + return timesheetService.uploadExcel( + SecurityUtils.getCurrentUserId(), file, weekStart); + } + + @GetMapping("/upload/template") + public ResponseEntity downloadTemplate() { + // TODO: Generate and return empty Excel template for timesheet upload + return ResponseEntity.status(501).build(); + } + + @GetMapping("/history") + public Page history( + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to, + Pageable pageable) { + return timesheetService.getHistory( + SecurityUtils.getCurrentUserId(), from, to, pageable); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/api/UserController.java b/wtm-api/src/main/java/kr/co/accura/wtm/api/UserController.java new file mode 100644 index 0000000..73c838c --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/api/UserController.java @@ -0,0 +1,66 @@ +package kr.co.accura.wtm.api; + +import kr.co.accura.wtm.domain.user.dto.*; +import kr.co.accura.wtm.domain.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.validation.Valid; +import java.util.Map; + +@RestController +@RequestMapping("/api/wtm/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @GetMapping + public ResponseEntity> list(Pageable pageable) { + Page page = userService.findAll(pageable); + return ResponseEntity.ok(Map.of( + "items", page.getContent(), + "total", page.getTotalElements() + )); + } + + @GetMapping("/{id}") + public ResponseEntity getById(@PathVariable Long id) { + return ResponseEntity.ok(userService.findById(id)); + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable Long id, + @RequestBody @Valid UserUpdateRequest request) { + return ResponseEntity.ok(userService.update(id, request)); + } + + @PutMapping("/{id}/roles") + public ResponseEntity assignRoles(@PathVariable Long id, + @RequestBody @Valid RoleAssignRequest request) { + userService.assignRoles(id, request); + return ResponseEntity.ok().build(); + } + + @PostMapping("/upload/internal") + public ResponseEntity> uploadInternal(@RequestParam("file") MultipartFile file) { + // TODO: Implement Excel upload for internal employees + return ResponseEntity.ok(Map.of("message", "Internal user upload not yet implemented")); + } + + @PostMapping("/upload/subcontractor") + public ResponseEntity> uploadSubcontractor(@RequestParam("file") MultipartFile file) { + // TODO: Implement Excel upload for subcontractors + return ResponseEntity.ok(Map.of("message", "Subcontractor upload not yet implemented")); + } + + @GetMapping("/upload/template") + public ResponseEntity downloadTemplate() { + // TODO: Implement template download + return ResponseEntity.noContent().build(); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/api/WbsController.java b/wtm-api/src/main/java/kr/co/accura/wtm/api/WbsController.java new file mode 100644 index 0000000..d66db7b --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/api/WbsController.java @@ -0,0 +1,75 @@ +package kr.co.accura.wtm.api; + +import kr.co.accura.wtm.domain.wbs.dto.*; +import kr.co.accura.wtm.domain.wbs.dto.WbsDisciplineAssignmentDto; +import kr.co.accura.wtm.domain.wbs.service.WbsService; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequestMapping("/api/wtm/projects/{projectId}") +@RequiredArgsConstructor +public class WbsController { + + private final WbsService wbsService; + + @PostMapping("/wbs/upload") + public ResponseEntity upload( + @PathVariable Long projectId, + @RequestParam("file") MultipartFile file, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate effectiveDate, + @RequestParam(required = false) String description) { + return ResponseEntity.ok(wbsService.uploadP6Wbs(projectId, file, effectiveDate, description)); + } + + @GetMapping("/wbs/versions") + public ResponseEntity> getVersions(@PathVariable Long projectId) { + return ResponseEntity.ok(wbsService.getVersions(projectId)); + } + + @GetMapping("/wbs/versions/{ver}") + public ResponseEntity> getVersionNodes(@PathVariable Long projectId, + @PathVariable("ver") Integer ver) { + return ResponseEntity.ok(wbsService.getVersionNodes(projectId, ver)); + } + + @PostMapping("/wbs/versions/{ver}/activate") + public ResponseEntity activateVersion(@PathVariable Long projectId, + @PathVariable("ver") Long ver) { + wbsService.activateVersion(ver); + return ResponseEntity.ok().build(); + } + + @GetMapping("/canonical-wbs") + public ResponseEntity> getCanonicalWbs(@PathVariable Long projectId) { + return ResponseEntity.ok(wbsService.getCanonicalWbs(projectId)); + } + + @GetMapping("/wbs/compare") + public ResponseEntity compare(@PathVariable Long projectId, + @RequestParam("a") int versionA, + @RequestParam("b") int versionB) { + return ResponseEntity.ok(wbsService.compareVersions(projectId, versionA, versionB)); + } + + /* ── WBS-Discipline assignments ── */ + + @GetMapping("/wbs-disciplines") + public ResponseEntity> getWbsDisciplines( + @PathVariable Long projectId) { + return ResponseEntity.ok(wbsService.getWbsDisciplines(projectId)); + } + + @PutMapping("/wbs-disciplines") + public ResponseEntity> saveWbsDisciplines( + @PathVariable Long projectId, + @RequestBody List assignments) { + return ResponseEntity.ok(wbsService.saveWbsDisciplines(projectId, assignments)); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/api/WorkRuleController.java b/wtm-api/src/main/java/kr/co/accura/wtm/api/WorkRuleController.java new file mode 100644 index 0000000..1f314cb --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/api/WorkRuleController.java @@ -0,0 +1,26 @@ +package kr.co.accura.wtm.api; + +import kr.co.accura.wtm.domain.config.dto.WorkRuleDto; +import kr.co.accura.wtm.domain.config.service.WorkRuleService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/wtm/work-rules") +@RequiredArgsConstructor +public class WorkRuleController { + + private final WorkRuleService workRuleService; + + @GetMapping + public List list() { + return workRuleService.getAll(); + } + + @PutMapping + public List update(@RequestBody List dtos) { + return workRuleService.saveAll(dtos); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/api/WtmAuthController.java b/wtm-api/src/main/java/kr/co/accura/wtm/api/WtmAuthController.java new file mode 100644 index 0000000..efd1371 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/api/WtmAuthController.java @@ -0,0 +1,52 @@ +package kr.co.accura.wtm.api; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * WTM-specific auth endpoints that supplement wbx-spring-core's AuthController. + *

+ * wbx-spring-core already provides: /api/wtm/auth/login, /me, /refresh, /logout, /password/change. + * This controller adds only the MISSING endpoints: SSO and password-reset. + */ +@RestController +@RequiredArgsConstructor +public class WtmAuthController { + + /** + * SSO initiation — redirects to OAuth2 authorization endpoint. + * Requires Azure Entra ID configuration. + */ + @GetMapping("/api/wtm/auth/sso") + public ResponseEntity> ssoInitiate() { + // TODO: Redirect to OAuth2 authorization URI once Azure Entra ID is configured + return ResponseEntity.status(501) + .body(Map.of("message", "SSO not yet configured. Azure Entra ID integration pending.")); + } + + /** + * SSO callback — handled by Spring Security OAuth2 filter chain. + * This endpoint exists for documentation; actual handling is done by SsoSuccessHandler. + */ + @GetMapping("/api/wtm/auth/sso/callback") + public ResponseEntity> ssoCallback() { + return ResponseEntity.status(501) + .body(Map.of("message", "SSO callback handled by Spring Security OAuth2.")); + } + + /** + * Password reset request — sends reset link to email. + */ + @PostMapping("/api/wtm/auth/password/reset") + public ResponseEntity> resetPassword(@RequestBody Map request) { + // TODO: Implement password reset email flow + String email = request.get("email"); + return ResponseEntity.ok(Map.of( + "message", "비밀번호 재설정 링크가 이메일로 전송되었습니다.", + "email", email != null ? email : "" + )); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/config/WtmConfig.java b/wtm-api/src/main/java/kr/co/accura/wtm/config/WtmConfig.java new file mode 100644 index 0000000..8141254 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/config/WtmConfig.java @@ -0,0 +1,11 @@ +package kr.co.accura.wtm.config; + +import org.springframework.context.annotation.Configuration; + +/** + * WTM 전용 설정 클래스. + * WTM 프로젝트에 특화된 Bean 및 설정을 여기에 추가. + */ +@Configuration +public class WtmConfig { +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/dto/ApprovalActionRequest.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/dto/ApprovalActionRequest.java new file mode 100644 index 0000000..abbc08c --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/dto/ApprovalActionRequest.java @@ -0,0 +1,5 @@ +package kr.co.accura.wtm.domain.approval.dto; + +public record ApprovalActionRequest( + String comment +) {} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/dto/ApprovalCommentDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/dto/ApprovalCommentDto.java new file mode 100644 index 0000000..48518ac --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/dto/ApprovalCommentDto.java @@ -0,0 +1,11 @@ +package kr.co.accura.wtm.domain.approval.dto; + +import java.time.LocalDateTime; + +public record ApprovalCommentDto( + Long id, + Long userId, + String comment, + String action, + LocalDateTime createdAt +) {} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/dto/ApprovalDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/dto/ApprovalDto.java new file mode 100644 index 0000000..e95ba2e --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/dto/ApprovalDto.java @@ -0,0 +1,16 @@ +package kr.co.accura.wtm.domain.approval.dto; + +import java.time.LocalDateTime; +import java.util.List; + +public record ApprovalDto( + Long id, + Long timesheetId, + Long requesterId, + Long projectId, + String status, + LocalDateTime submittedAt, + LocalDateTime completedAt, + List lines, + List comments +) {} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/dto/ApprovalLineItemDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/dto/ApprovalLineItemDto.java new file mode 100644 index 0000000..e15952c --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/dto/ApprovalLineItemDto.java @@ -0,0 +1,12 @@ +package kr.co.accura.wtm.domain.approval.dto; + +import java.time.LocalDateTime; + +public record ApprovalLineItemDto( + Long id, + Long approverId, + int approvalOrder, + String roleCode, + String status, + LocalDateTime actedAt +) {} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/dto/BatchApproveRequest.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/dto/BatchApproveRequest.java new file mode 100644 index 0000000..b5afe55 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/dto/BatchApproveRequest.java @@ -0,0 +1,8 @@ +package kr.co.accura.wtm.domain.approval.dto; + +import java.util.List; + +public record BatchApproveRequest( + List lineIds, + String comment +) {} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/entity/TtApproval.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/entity/TtApproval.java new file mode 100644 index 0000000..eee536b --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/entity/TtApproval.java @@ -0,0 +1,61 @@ +package kr.co.accura.wtm.domain.approval.entity; + +import jakarta.persistence.*; +import kr.co.accura.wbx.spring.common.BaseEntity; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "approvals", uniqueConstraints = { + @UniqueConstraint(columnNames = {"timesheet_id"}) +}) +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TtApproval extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "timesheet_id", nullable = false) + private Long timesheetId; + + @Column(name = "requester_id", nullable = false) + private Long requesterId; + + @Column(name = "project_id") + private Long projectId; + + @Column(length = 20) + @Builder.Default + private String status = "PENDING"; + + @Column(name = "submitted_at") + private LocalDateTime submittedAt; + + @Column(name = "completed_at") + private LocalDateTime completedAt; + + @OneToMany(mappedBy = "approval", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List lines = new ArrayList<>(); + + @OneToMany(mappedBy = "approval", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List comments = new ArrayList<>(); + + public void complete() { + this.status = "APPROVED"; + this.completedAt = LocalDateTime.now(); + } + + public void reject() { + this.status = "REJECTED"; + this.completedAt = LocalDateTime.now(); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/entity/TtApprovalComment.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/entity/TtApprovalComment.java new file mode 100644 index 0000000..f74ed4e --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/entity/TtApprovalComment.java @@ -0,0 +1,33 @@ +package kr.co.accura.wtm.domain.approval.entity; + +import jakarta.persistence.*; +import kr.co.accura.wbx.spring.common.BaseEntity; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "approval_comments") +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TtApprovalComment extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "approval_id", nullable = false) + private TtApproval approval; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(length = 2000) + private String comment; + + @Column(length = 20) + private String action; +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/entity/TtApprovalLine.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/entity/TtApprovalLine.java new file mode 100644 index 0000000..68b9592 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/entity/TtApprovalLine.java @@ -0,0 +1,50 @@ +package kr.co.accura.wtm.domain.approval.entity; + +import jakarta.persistence.*; +import kr.co.accura.wbx.spring.common.BaseEntity; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "approval_lines") +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TtApprovalLine extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "approval_id", nullable = false) + private TtApproval approval; + + @Column(name = "approver_id", nullable = false) + private Long approverId; + + @Column(name = "approval_order", nullable = false) + private int approvalOrder; + + @Column(name = "role_code", length = 20) + private String roleCode; + + @Column(length = 20) + @Builder.Default + private String status = "PENDING"; + + @Column(name = "acted_at") + private LocalDateTime actedAt; + + public void approve() { + this.status = "APPROVED"; + this.actedAt = LocalDateTime.now(); + } + + public void reject() { + this.status = "REJECTED"; + this.actedAt = LocalDateTime.now(); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/handler/TimesheetApprovalHandler.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/handler/TimesheetApprovalHandler.java new file mode 100644 index 0000000..594e9dc --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/handler/TimesheetApprovalHandler.java @@ -0,0 +1,117 @@ +package kr.co.accura.wtm.domain.approval.handler; + +import kr.co.accura.wbx.spring.approval.*; +import kr.co.accura.wbx.spring.common.NotFoundException; +import kr.co.accura.wtm.domain.approval.entity.TtApproval; +import kr.co.accura.wtm.domain.approval.entity.TtApprovalLine; +import kr.co.accura.wtm.domain.approval.repository.TtApprovalLineRepository; +import kr.co.accura.wtm.domain.approval.repository.TtApprovalRepository; +import kr.co.accura.wtm.domain.timesheet.TimesheetStatus; +import kr.co.accura.wtm.domain.timesheet.entity.Timesheet; +import kr.co.accura.wtm.domain.timesheet.repository.TimesheetRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class TimesheetApprovalHandler implements ApprovalHandler { + + private final TtApprovalRepository approvalRepository; + private final TtApprovalLineRepository lineRepository; + private final TimesheetRepository timesheetRepository; + private final ApplicationEventPublisher eventPublisher; + + @Override + public String getTypeKey() { + return "timesheet"; + } + + @Override + public String getTypeDisplay() { + return "시수 결재"; + } + + @Override + @Transactional + public ApprovalResult approve(Long lineId, Long approverId, String comment) { + TtApprovalLine line = lineRepository.findById(lineId) + .orElseThrow(() -> new NotFoundException("결재 라인을 찾을 수 없습니다.")); + line.approve(); + lineRepository.save(line); + + TtApproval approval = line.getApproval(); + Timesheet ts = timesheetRepository.findById(approval.getTimesheetId()) + .orElseThrow(() -> new NotFoundException("Timesheet not found")); + + Optional next = lineRepository.findNextPending( + approval.getId(), line.getApprovalOrder()); + + if (next.isPresent()) { + ts.setStatus(TimesheetStatus.DL_APPROVED); + } else { + ts.setStatus(TimesheetStatus.APPROVED); + approval.complete(); + approvalRepository.save(approval); + eventPublisher.publishEvent(new ApprovalCompletedEvent( + "timesheet", ts.getId(), approverId, approval)); + } + timesheetRepository.save(ts); + + return ApprovalResult.success("승인 완료"); + } + + @Override + @Transactional + public ApprovalResult reject(Long lineId, Long approverId, String comment) { + TtApprovalLine line = lineRepository.findById(lineId) + .orElseThrow(() -> new NotFoundException("결재 라인을 찾을 수 없습니다.")); + line.reject(); + lineRepository.save(line); + + TtApproval approval = line.getApproval(); + approval.reject(); + approvalRepository.save(approval); + + Timesheet ts = timesheetRepository.findById(approval.getTimesheetId()) + .orElseThrow(() -> new NotFoundException("Timesheet not found")); + ts.setStatus(TimesheetStatus.REJECTED); + timesheetRepository.save(ts); + + return ApprovalResult.success("반려 완료"); + } + + @Override + @Transactional(readOnly = true) + public ApprovalHistoryDto getApprovalHistory(Long timesheetId) { + TtApproval approval = approvalRepository.findByTimesheetId(timesheetId) + .orElseThrow(() -> new NotFoundException("결재 정보를 찾을 수 없습니다.")); + + List lineDtos = approval.getLines().stream() + .map(l -> new ApprovalLineDto( + l.getId(), l.getApproverId(), null, + l.getApprovalOrder(), l.getRoleCode(), l.getStatus(), + null, l.getActedAt())) + .toList(); + + return new ApprovalHistoryDto( + timesheetId, "timesheet", approval.getStatus(), + null, approval.getSubmittedAt(), lineDtos); + } + + @Override + @Transactional(readOnly = true) + public Page getPending(Long approverId, Pageable pageable) { + return lineRepository.findPendingByApproverId(approverId, pageable) + .map(line -> new ApprovalPendingDto( + line.getId(), "timesheet", + "시수 결재 #" + line.getApproval().getTimesheetId(), + null, line.getApproval().getSubmittedAt())); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/repository/TtApprovalCommentRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/repository/TtApprovalCommentRepository.java new file mode 100644 index 0000000..b1f7357 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/repository/TtApprovalCommentRepository.java @@ -0,0 +1,11 @@ +package kr.co.accura.wtm.domain.approval.repository; + +import kr.co.accura.wtm.domain.approval.entity.TtApprovalComment; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TtApprovalCommentRepository extends JpaRepository { + + List findByApproval_IdOrderByCreatedAtAsc(Long approvalId); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/repository/TtApprovalLineRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/repository/TtApprovalLineRepository.java new file mode 100644 index 0000000..4979a4a --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/repository/TtApprovalLineRepository.java @@ -0,0 +1,29 @@ +package kr.co.accura.wtm.domain.approval.repository; + +import kr.co.accura.wtm.domain.approval.entity.TtApprovalLine; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface TtApprovalLineRepository extends JpaRepository { + + List findByApproval_IdOrderByApprovalOrder(Long approvalId); + + @Query("SELECT l FROM TtApprovalLine l WHERE l.approval.id = :approvalId " + + "AND l.approvalOrder > :currentOrder AND l.status = 'PENDING' " + + "ORDER BY l.approvalOrder ASC") + Optional findNextPending( + @Param("approvalId") Long approvalId, + @Param("currentOrder") int currentOrder); + + @Query("SELECT l FROM TtApprovalLine l WHERE l.approverId = :approverId AND l.status = 'PENDING'") + Page findPendingByApproverId(@Param("approverId") Long approverId, Pageable pageable); + + @Query("SELECT l FROM TtApprovalLine l WHERE l.approverId = :approverId AND l.status <> 'PENDING'") + Page findHistoryByApproverId(@Param("approverId") Long approverId, Pageable pageable); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/repository/TtApprovalRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/repository/TtApprovalRepository.java new file mode 100644 index 0000000..425ed3c --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/repository/TtApprovalRepository.java @@ -0,0 +1,11 @@ +package kr.co.accura.wtm.domain.approval.repository; + +import kr.co.accura.wtm.domain.approval.entity.TtApproval; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface TtApprovalRepository extends JpaRepository { + + Optional findByTimesheetId(Long timesheetId); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/service/ApprovalService.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/service/ApprovalService.java new file mode 100644 index 0000000..435f132 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/approval/service/ApprovalService.java @@ -0,0 +1,145 @@ +package kr.co.accura.wtm.domain.approval.service; + +import kr.co.accura.wbx.spring.common.NotFoundException; +import kr.co.accura.wtm.domain.approval.dto.*; +import kr.co.accura.wtm.domain.approval.entity.TtApproval; +import kr.co.accura.wtm.domain.approval.entity.TtApprovalComment; +import kr.co.accura.wtm.domain.approval.entity.TtApprovalLine; +import kr.co.accura.wtm.domain.approval.repository.TtApprovalCommentRepository; +import kr.co.accura.wtm.domain.approval.repository.TtApprovalLineRepository; +import kr.co.accura.wtm.domain.approval.repository.TtApprovalRepository; +import kr.co.accura.wtm.domain.timesheet.TimesheetStatus; +import kr.co.accura.wtm.domain.timesheet.entity.Timesheet; +import kr.co.accura.wtm.domain.timesheet.repository.TimesheetRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class ApprovalService { + + private final TtApprovalRepository approvalRepository; + private final TtApprovalLineRepository lineRepository; + private final TtApprovalCommentRepository commentRepository; + private final TimesheetRepository timesheetRepository; + + @Transactional(readOnly = true) + public Page getPending(Long approverId, Pageable pageable) { + return lineRepository.findPendingByApproverId(approverId, pageable) + .map(this::toLineDto); + } + + public void approve(Long lineId, Long approverId, String comment) { + TtApprovalLine line = lineRepository.findById(lineId) + .orElseThrow(() -> new NotFoundException("결재 라인을 찾을 수 없습니다.")); + line.approve(); + lineRepository.save(line); + + TtApproval approval = line.getApproval(); + Timesheet ts = timesheetRepository.findById(approval.getTimesheetId()) + .orElseThrow(() -> new NotFoundException("Timesheet not found")); + + // Check for next pending approver + var next = lineRepository.findNextPending(approval.getId(), line.getApprovalOrder()); + if (next.isPresent()) { + ts.setStatus(TimesheetStatus.DL_APPROVED); + } else { + ts.setStatus(TimesheetStatus.APPROVED); + approval.complete(); + approvalRepository.save(approval); + } + timesheetRepository.save(ts); + + if (comment != null && !comment.isBlank()) { + addComment(approval, approverId, comment, "APPROVE"); + } + } + + public void reject(Long lineId, Long approverId, String comment) { + TtApprovalLine line = lineRepository.findById(lineId) + .orElseThrow(() -> new NotFoundException("결재 라인을 찾을 수 없습니다.")); + line.reject(); + lineRepository.save(line); + + TtApproval approval = line.getApproval(); + approval.reject(); + approvalRepository.save(approval); + + Timesheet ts = timesheetRepository.findById(approval.getTimesheetId()) + .orElseThrow(() -> new NotFoundException("Timesheet not found")); + ts.setStatus(TimesheetStatus.REJECTED); + timesheetRepository.save(ts); + + if (comment != null && !comment.isBlank()) { + addComment(approval, approverId, comment, "REJECT"); + } + } + + public void batchApprove(List lineIds, Long approverId, String comment) { + for (Long lineId : lineIds) { + approve(lineId, approverId, comment); + } + } + + public void addComment(Long approvalId, Long userId, String comment) { + TtApproval approval = approvalRepository.findById(approvalId) + .orElseThrow(() -> new NotFoundException("결재를 찾을 수 없습니다.")); + addComment(approval, userId, comment, "COMMENT"); + } + + @Transactional(readOnly = true) + public ApprovalDto getApprovalDetail(Long approvalId) { + TtApproval approval = approvalRepository.findById(approvalId) + .orElseThrow(() -> new NotFoundException("결재를 찾을 수 없습니다.")); + return toDto(approval); + } + + @Transactional(readOnly = true) + public Page getHistory(Long approverId, Pageable pageable) { + return lineRepository.findHistoryByApproverId(approverId, pageable) + .map(this::toLineDto); + } + + @Transactional(readOnly = true) + public List getComments(Long approvalId) { + return commentRepository.findByApproval_IdOrderByCreatedAtAsc(approvalId).stream() + .map(this::toCommentDto) + .toList(); + } + + // --- Helper methods --- + + private void addComment(TtApproval approval, Long userId, String comment, String action) { + TtApprovalComment c = TtApprovalComment.builder() + .approval(approval) + .userId(userId) + .comment(comment) + .action(action) + .build(); + commentRepository.save(c); + } + + private ApprovalDto toDto(TtApproval a) { + List lines = a.getLines().stream().map(this::toLineDto).toList(); + List comments = a.getComments().stream().map(this::toCommentDto).toList(); + return new ApprovalDto(a.getId(), a.getTimesheetId(), a.getRequesterId(), + a.getProjectId(), a.getStatus(), a.getSubmittedAt(), a.getCompletedAt(), + lines, comments); + } + + private ApprovalLineItemDto toLineDto(TtApprovalLine l) { + return new ApprovalLineItemDto(l.getId(), l.getApproverId(), + l.getApprovalOrder(), l.getRoleCode(), l.getStatus(), l.getActedAt()); + } + + private ApprovalCommentDto toCommentDto(TtApprovalComment c) { + return new ApprovalCommentDto(c.getId(), c.getUserId(), + c.getComment(), c.getAction(), c.getCreatedAt()); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/audit/entity/SaAccessLog.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/audit/entity/SaAccessLog.java new file mode 100644 index 0000000..6875be6 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/audit/entity/SaAccessLog.java @@ -0,0 +1,39 @@ +package kr.co.accura.wtm.domain.audit.entity; + +import jakarta.persistence.*; +import kr.co.accura.wbx.spring.common.BaseEntity; +import lombok.*; + +@Entity +@Table(name = "sa_access_logs") +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SaAccessLog extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(nullable = false, length = 50) + private String action; + + @Column(length = 100) + private String resource; + + @Column(name = "resource_id") + private Long resourceId; + + @Column(name = "ip_address", length = 50) + private String ipAddress; + + @Column(name = "user_agent", length = 500) + private String userAgent; + + @Column(length = 2000) + private String detail; +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/audit/repository/SaAccessLogRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/audit/repository/SaAccessLogRepository.java new file mode 100644 index 0000000..b7c7880 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/audit/repository/SaAccessLogRepository.java @@ -0,0 +1,13 @@ +package kr.co.accura.wtm.domain.audit.repository; + +import kr.co.accura.wtm.domain.audit.entity.SaAccessLog; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SaAccessLogRepository extends JpaRepository { + + Page findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable); + + Page findAllByOrderByCreatedAtDesc(Pageable pageable); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/dto/OverheadTypeDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/dto/OverheadTypeDto.java new file mode 100644 index 0000000..07a9463 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/dto/OverheadTypeDto.java @@ -0,0 +1,10 @@ +package kr.co.accura.wtm.domain.config.dto; + +public record OverheadTypeDto( + Long id, + String code, + String name, + String category, + Boolean isActive, + Integer sortOrder +) {} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/dto/WorkRuleDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/dto/WorkRuleDto.java new file mode 100644 index 0000000..991c62b --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/dto/WorkRuleDto.java @@ -0,0 +1,12 @@ +package kr.co.accura.wtm.domain.config.dto; + +import java.math.BigDecimal; + +public record WorkRuleDto( + Long id, + String location, + BigDecimal minDailyHours, + BigDecimal maxDailyHours, + BigDecimal maxWeeklyHours, + Boolean isActive +) {} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/entity/OverheadType.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/entity/OverheadType.java new file mode 100644 index 0000000..0c1485b --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/entity/OverheadType.java @@ -0,0 +1,35 @@ +package kr.co.accura.wtm.domain.config.entity; + +import jakarta.persistence.*; +import kr.co.accura.wbx.spring.common.BaseEntity; +import lombok.*; + +@Entity +@Table(name = "overhead_types") +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OverheadType extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true, length = 50) + private String code; + + @Column(nullable = false, length = 200) + private String name; + + @Column(length = 50) + private String category; + + @Column(name = "is_active") + @Builder.Default + private Boolean isActive = true; + + @Column(name = "sort_order") + @Builder.Default + private Integer sortOrder = 0; +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/entity/WorkRule.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/entity/WorkRule.java new file mode 100644 index 0000000..827c8e8 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/entity/WorkRule.java @@ -0,0 +1,39 @@ +package kr.co.accura.wtm.domain.config.entity; + +import jakarta.persistence.*; +import kr.co.accura.wbx.spring.common.BaseEntity; +import lombok.*; + +import java.math.BigDecimal; + +@Entity +@Table(name = "work_rules") +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WorkRule extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 50) + private String location; + + @Column(name = "min_daily_hours", precision = 4, scale = 2) + @Builder.Default + private BigDecimal minDailyHours = new BigDecimal("8.00"); + + @Column(name = "max_daily_hours", precision = 4, scale = 2) + @Builder.Default + private BigDecimal maxDailyHours = new BigDecimal("12.00"); + + @Column(name = "max_weekly_hours", precision = 5, scale = 2) + @Builder.Default + private BigDecimal maxWeeklyHours = new BigDecimal("52.00"); + + @Column(name = "is_active") + @Builder.Default + private Boolean isActive = true; +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/repository/OverheadTypeRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/repository/OverheadTypeRepository.java new file mode 100644 index 0000000..44a903c --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/repository/OverheadTypeRepository.java @@ -0,0 +1,11 @@ +package kr.co.accura.wtm.domain.config.repository; + +import kr.co.accura.wtm.domain.config.entity.OverheadType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface OverheadTypeRepository extends JpaRepository { + + List findByIsActiveTrueOrderBySortOrderAsc(); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/repository/WorkRuleRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/repository/WorkRuleRepository.java new file mode 100644 index 0000000..8319d5e --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/repository/WorkRuleRepository.java @@ -0,0 +1,14 @@ +package kr.co.accura.wtm.domain.config.repository; + +import kr.co.accura.wtm.domain.config.entity.WorkRule; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface WorkRuleRepository extends JpaRepository { + + Optional findByLocation(String location); + + List findByIsActiveTrue(); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/service/OverheadTypeService.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/service/OverheadTypeService.java new file mode 100644 index 0000000..d662080 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/service/OverheadTypeService.java @@ -0,0 +1,53 @@ +package kr.co.accura.wtm.domain.config.service; + +import kr.co.accura.wbx.spring.common.NotFoundException; +import kr.co.accura.wtm.domain.config.dto.OverheadTypeDto; +import kr.co.accura.wtm.domain.config.entity.OverheadType; +import kr.co.accura.wtm.domain.config.repository.OverheadTypeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class OverheadTypeService { + + private final OverheadTypeRepository repository; + + @Transactional(readOnly = true) + public List getAll() { + return repository.findByIsActiveTrueOrderBySortOrderAsc().stream() + .map(this::toDto) + .toList(); + } + + public OverheadTypeDto create(OverheadTypeDto dto) { + OverheadType entity = OverheadType.builder() + .code(dto.code()) + .name(dto.name()) + .category(dto.category()) + .isActive(dto.isActive() != null ? dto.isActive() : true) + .sortOrder(dto.sortOrder() != null ? dto.sortOrder() : 0) + .build(); + return toDto(repository.save(entity)); + } + + public OverheadTypeDto update(Long id, OverheadTypeDto dto) { + OverheadType entity = repository.findById(id) + .orElseThrow(() -> new NotFoundException("Overhead type not found: " + id)); + entity.setCode(dto.code()); + entity.setName(dto.name()); + entity.setCategory(dto.category()); + if (dto.isActive() != null) entity.setIsActive(dto.isActive()); + if (dto.sortOrder() != null) entity.setSortOrder(dto.sortOrder()); + return toDto(repository.save(entity)); + } + + private OverheadTypeDto toDto(OverheadType e) { + return new OverheadTypeDto(e.getId(), e.getCode(), e.getName(), + e.getCategory(), e.getIsActive(), e.getSortOrder()); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/service/WorkRuleService.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/service/WorkRuleService.java new file mode 100644 index 0000000..18ae0c9 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/config/service/WorkRuleService.java @@ -0,0 +1,54 @@ +package kr.co.accura.wtm.domain.config.service; + +import kr.co.accura.wtm.domain.config.dto.WorkRuleDto; +import kr.co.accura.wtm.domain.config.entity.WorkRule; +import kr.co.accura.wtm.domain.config.repository.WorkRuleRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class WorkRuleService { + + private final WorkRuleRepository repository; + + @Transactional(readOnly = true) + public List getAll() { + return repository.findByIsActiveTrue().stream() + .map(this::toDto) + .toList(); + } + + public List saveAll(List dtos) { + List rules = dtos.stream().map(dto -> { + WorkRule rule; + if (dto.id() != null) { + rule = repository.findById(dto.id()).orElse(new WorkRule()); + } else if (dto.location() != null) { + rule = repository.findByLocation(dto.location()).orElse(new WorkRule()); + } else { + rule = new WorkRule(); + } + rule.setLocation(dto.location()); + if (dto.minDailyHours() != null) rule.setMinDailyHours(dto.minDailyHours()); + if (dto.maxDailyHours() != null) rule.setMaxDailyHours(dto.maxDailyHours()); + if (dto.maxWeeklyHours() != null) rule.setMaxWeeklyHours(dto.maxWeeklyHours()); + if (dto.isActive() != null) rule.setIsActive(dto.isActive()); + return rule; + }).toList(); + + return repository.saveAll(rules).stream() + .map(this::toDto) + .toList(); + } + + private WorkRuleDto toDto(WorkRule e) { + return new WorkRuleDto(e.getId(), e.getLocation(), + e.getMinDailyHours(), e.getMaxDailyHours(), + e.getMaxWeeklyHours(), e.getIsActive()); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/ProjectStatus.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/ProjectStatus.java new file mode 100644 index 0000000..267d0f3 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/ProjectStatus.java @@ -0,0 +1,7 @@ +package kr.co.accura.wtm.domain.project; + +public enum ProjectStatus { + ACTIVE, + CLOSED, + HOLD +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/ProjectType.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/ProjectType.java new file mode 100644 index 0000000..b4339a1 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/ProjectType.java @@ -0,0 +1,7 @@ +package kr.co.accura.wtm.domain.project; + +public enum ProjectType { + EPC, + NON_PROJECT, + OTHER +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/dto/AssignmentCreateRequest.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/dto/AssignmentCreateRequest.java new file mode 100644 index 0000000..d40ae2d --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/dto/AssignmentCreateRequest.java @@ -0,0 +1,9 @@ +package kr.co.accura.wtm.domain.project.dto; + +import jakarta.validation.constraints.NotNull; + +public record AssignmentCreateRequest( + @NotNull Long userId, + String role +) { +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/dto/ProjectAssignmentDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/dto/ProjectAssignmentDto.java new file mode 100644 index 0000000..bbd746c --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/dto/ProjectAssignmentDto.java @@ -0,0 +1,27 @@ +package kr.co.accura.wtm.domain.project.dto; + +import kr.co.accura.wtm.domain.project.entity.ProjectAssignment; + +import java.time.LocalDateTime; + +public record ProjectAssignmentDto( + Long id, + Long projectId, + Long userId, + String userName, + String userEmail, + String role, + LocalDateTime assignedAt +) { + public static ProjectAssignmentDto from(ProjectAssignment assignment) { + return new ProjectAssignmentDto( + assignment.getId(), + assignment.getProject().getId(), + assignment.getUser().getId(), + assignment.getUser().getFullName(), + assignment.getUser().getEmail(), + assignment.getRole(), + assignment.getAssignedAt() + ); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/dto/ProjectCreateRequest.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/dto/ProjectCreateRequest.java new file mode 100644 index 0000000..2196e42 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/dto/ProjectCreateRequest.java @@ -0,0 +1,16 @@ +package kr.co.accura.wtm.domain.project.dto; + +import jakarta.validation.constraints.NotBlank; + +import java.time.LocalDate; + +public record ProjectCreateRequest( + @NotBlank String projectCode, + @NotBlank String name, + String description, + @NotBlank String projectType, + LocalDate startDate, + LocalDate endDate, + Long pmUserId +) { +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/dto/ProjectDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/dto/ProjectDto.java new file mode 100644 index 0000000..06f59cb --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/dto/ProjectDto.java @@ -0,0 +1,33 @@ +package kr.co.accura.wtm.domain.project.dto; + +import kr.co.accura.wtm.domain.project.entity.Project; + +import java.time.LocalDate; + +public record ProjectDto( + Long id, + String projectCode, + String name, + String description, + String projectType, + String status, + LocalDate startDate, + LocalDate endDate, + Long pmUserId, + String pmUserName +) { + public static ProjectDto from(Project project) { + return new ProjectDto( + project.getId(), + project.getProjectCode(), + project.getName(), + project.getDescription(), + project.getProjectType() != null ? project.getProjectType().name() : null, + project.getStatus() != null ? project.getStatus().name() : null, + project.getStartDate(), + project.getEndDate(), + project.getPmUser() != null ? project.getPmUser().getId() : null, + project.getPmUser() != null ? project.getPmUser().getFullName() : null + ); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/entity/Project.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/entity/Project.java new file mode 100644 index 0000000..fdfcc88 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/entity/Project.java @@ -0,0 +1,52 @@ +package kr.co.accura.wtm.domain.project.entity; + +import jakarta.persistence.*; +import kr.co.accura.wbx.spring.common.BaseEntity; +import kr.co.accura.wtm.domain.project.ProjectStatus; +import kr.co.accura.wtm.domain.project.ProjectType; +import kr.co.accura.wtm.domain.user.entity.User; +import lombok.*; + +import java.time.LocalDate; + +@Entity +@Table(name = "projects") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Project extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "project_code", unique = true, nullable = false, length = 50) + private String projectCode; + + @Column(nullable = false, length = 255) + private String name; + + @Column(length = 1000) + private String description; + + @Enumerated(EnumType.STRING) + @Column(name = "project_type", nullable = false, length = 20) + private ProjectType projectType; + + @Enumerated(EnumType.STRING) + @Column(length = 20) + @Builder.Default + private ProjectStatus status = ProjectStatus.ACTIVE; + + @Column(name = "start_date") + private LocalDate startDate; + + @Column(name = "end_date") + private LocalDate endDate; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "pm_user_id") + private User pmUser; +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/entity/ProjectAssignment.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/entity/ProjectAssignment.java new file mode 100644 index 0000000..ddcd248 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/entity/ProjectAssignment.java @@ -0,0 +1,39 @@ +package kr.co.accura.wtm.domain.project.entity; + +import jakarta.persistence.*; +import kr.co.accura.wtm.domain.user.entity.User; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "project_assignments") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ProjectAssignment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "project_id", nullable = false) + private Project project; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(length = 50) + private String role; + + @Column(name = "assigned_at") + @Builder.Default + private LocalDateTime assignedAt = LocalDateTime.now(); + + @Column(name = "assigned_by") + private Long assignedBy; +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/entity/ProjectTypeConfig.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/entity/ProjectTypeConfig.java new file mode 100644 index 0000000..e870029 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/entity/ProjectTypeConfig.java @@ -0,0 +1,44 @@ +package kr.co.accura.wtm.domain.project.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "project_type_config", + uniqueConstraints = @UniqueConstraint(columnNames = {"project_type", "config_key"})) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ProjectTypeConfig { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "project_type", nullable = false, length = 20) + private String projectType; + + @Column(name = "config_key", nullable = false, length = 100) + private String configKey; + + @Column(name = "config_value", length = 500) + private String configValue; + + @Column(length = 500) + private String description; + + @Column(name = "is_active") + @Builder.Default + private Boolean isActive = true; + + @Column(name = "created_at") + @Builder.Default + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/repository/ProjectAssignmentRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/repository/ProjectAssignmentRepository.java new file mode 100644 index 0000000..bf44af4 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/repository/ProjectAssignmentRepository.java @@ -0,0 +1,17 @@ +package kr.co.accura.wtm.domain.project.repository; + +import kr.co.accura.wtm.domain.project.entity.ProjectAssignment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ProjectAssignmentRepository extends JpaRepository { + + List findByProject_Id(Long projectId); + + List findByUser_Id(Long userId); + + boolean existsByProject_IdAndUser_Id(Long projectId, Long userId); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/repository/ProjectRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/repository/ProjectRepository.java new file mode 100644 index 0000000..5e19674 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/repository/ProjectRepository.java @@ -0,0 +1,15 @@ +package kr.co.accura.wtm.domain.project.repository; + +import kr.co.accura.wtm.domain.project.entity.Project; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface ProjectRepository extends JpaRepository { + + Optional findByProjectCode(String projectCode); + + boolean existsByProjectCode(String projectCode); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/repository/ProjectTypeConfigRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/repository/ProjectTypeConfigRepository.java new file mode 100644 index 0000000..b5fb30e --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/repository/ProjectTypeConfigRepository.java @@ -0,0 +1,16 @@ +package kr.co.accura.wtm.domain.project.repository; + +import kr.co.accura.wtm.domain.project.entity.ProjectTypeConfig; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ProjectTypeConfigRepository extends JpaRepository { + + List findByProjectTypeAndIsActiveTrue(String projectType); + + Optional findByProjectTypeAndConfigKey(String projectType, String configKey); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/service/ProjectService.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/service/ProjectService.java new file mode 100644 index 0000000..8ffab98 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/project/service/ProjectService.java @@ -0,0 +1,144 @@ +package kr.co.accura.wtm.domain.project.service; + +import kr.co.accura.wbx.spring.common.BusinessException; +import kr.co.accura.wbx.spring.common.NotFoundException; +import kr.co.accura.wtm.domain.project.ProjectStatus; +import kr.co.accura.wtm.domain.project.ProjectType; +import kr.co.accura.wtm.domain.project.dto.*; +import kr.co.accura.wtm.domain.project.entity.Project; +import kr.co.accura.wtm.domain.project.entity.ProjectAssignment; +import kr.co.accura.wtm.domain.project.repository.ProjectAssignmentRepository; +import kr.co.accura.wtm.domain.project.repository.ProjectRepository; +import kr.co.accura.wtm.domain.user.entity.User; +import kr.co.accura.wtm.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class ProjectService { + + private final ProjectRepository projectRepository; + private final ProjectAssignmentRepository assignmentRepository; + private final UserRepository userRepository; + + @Transactional(readOnly = true) + public Page findAll(Pageable pageable) { + return projectRepository.findAll(pageable).map(ProjectDto::from); + } + + @Transactional(readOnly = true) + public ProjectDto findById(Long id) { + Project project = projectRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Project not found: " + id)); + return ProjectDto.from(project); + } + + public ProjectDto create(ProjectCreateRequest request) { + if (projectRepository.existsByProjectCode(request.projectCode())) { + throw new BusinessException("Project code already exists: " + request.projectCode()); + } + + Project project = Project.builder() + .projectCode(request.projectCode()) + .name(request.name()) + .description(request.description()) + .projectType(ProjectType.valueOf(request.projectType())) + .startDate(request.startDate()) + .endDate(request.endDate()) + .build(); + + if (request.pmUserId() != null) { + User pmUser = userRepository.findById(request.pmUserId()) + .orElseThrow(() -> new NotFoundException("PM user not found: " + request.pmUserId())); + project.setPmUser(pmUser); + } + + projectRepository.save(project); + return ProjectDto.from(project); + } + + public ProjectDto update(Long id, ProjectCreateRequest request) { + Project project = projectRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Project not found: " + id)); + + if (request.name() != null) project.setName(request.name()); + if (request.description() != null) project.setDescription(request.description()); + if (request.projectType() != null) project.setProjectType(ProjectType.valueOf(request.projectType())); + if (request.startDate() != null) project.setStartDate(request.startDate()); + if (request.endDate() != null) project.setEndDate(request.endDate()); + if (request.pmUserId() != null) { + User pmUser = userRepository.findById(request.pmUserId()) + .orElseThrow(() -> new NotFoundException("PM user not found: " + request.pmUserId())); + project.setPmUser(pmUser); + } + + projectRepository.save(project); + return ProjectDto.from(project); + } + + @Transactional(readOnly = true) + public List findMyProjects(Long userId) { + List assignments = assignmentRepository.findByUser_Id(userId); + return assignments.stream() + .map(a -> ProjectDto.from(a.getProject())) + .toList(); + } + + // --- Assignment methods --- + + @Transactional(readOnly = true) + public List getAssignments(Long projectId) { + return assignmentRepository.findByProject_Id(projectId).stream() + .map(ProjectAssignmentDto::from) + .toList(); + } + + public ProjectAssignmentDto createAssignment(Long projectId, AssignmentCreateRequest request) { + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> new NotFoundException("Project not found: " + projectId)); + User user = userRepository.findById(request.userId()) + .orElseThrow(() -> new NotFoundException("User not found: " + request.userId())); + + if (assignmentRepository.existsByProject_IdAndUser_Id(projectId, request.userId())) { + throw new BusinessException("User already assigned to this project"); + } + + ProjectAssignment assignment = ProjectAssignment.builder() + .project(project) + .user(user) + .role(request.role()) + .build(); + + assignmentRepository.save(assignment); + return ProjectAssignmentDto.from(assignment); + } + + public ProjectAssignmentDto updateAssignment(Long projectId, Long assignId, AssignmentCreateRequest request) { + ProjectAssignment assignment = assignmentRepository.findById(assignId) + .orElseThrow(() -> new NotFoundException("Assignment not found: " + assignId)); + + if (request.role() != null) assignment.setRole(request.role()); + + assignmentRepository.save(assignment); + return ProjectAssignmentDto.from(assignment); + } + + public void deleteAssignment(Long projectId, Long assignId) { + ProjectAssignment assignment = assignmentRepository.findById(assignId) + .orElseThrow(() -> new NotFoundException("Assignment not found: " + assignId)); + assignmentRepository.delete(assignment); + } + + @Transactional(readOnly = true) + public Page getAvailableUsers(Long projectId, Pageable pageable) { + // Returns all active users; filtering out already-assigned users can be refined later + return userRepository.findAll(pageable); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/report/dto/ProjectHoursReport.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/report/dto/ProjectHoursReport.java new file mode 100644 index 0000000..68a99f4 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/report/dto/ProjectHoursReport.java @@ -0,0 +1,22 @@ +package kr.co.accura.wtm.domain.report.dto; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +public record ProjectHoursReport( + ReportFilter filter, + List rows, + BigDecimal totalHours, + LocalDateTime generatedAt +) { + public record ProjectHoursRow( + Long projectId, + String projectCode, + String projectName, + String discipline, + LocalDate entryDate, + BigDecimal hours + ) {} +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/report/dto/ReportFilter.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/report/dto/ReportFilter.java new file mode 100644 index 0000000..734938a --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/report/dto/ReportFilter.java @@ -0,0 +1,12 @@ +package kr.co.accura.wtm.domain.report.dto; + +import java.time.LocalDate; + +public record ReportFilter( + Long projectId, + LocalDate from, + LocalDate to, + String groupBy, + Integer wbsLevel, + String department +) {} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/report/dto/WbsHoursReport.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/report/dto/WbsHoursReport.java new file mode 100644 index 0000000..0434600 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/report/dto/WbsHoursReport.java @@ -0,0 +1,20 @@ +package kr.co.accura.wtm.domain.report.dto; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +public record WbsHoursReport( + ReportFilter filter, + List rows, + LocalDateTime generatedAt +) { + public record WbsHoursRow( + String wbsCode, + String wbsName, + int wbsLevel, + String discipline, + BigDecimal totalHours, + long userCount + ) {} +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/report/service/ReportService.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/report/service/ReportService.java new file mode 100644 index 0000000..e45acad --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/report/service/ReportService.java @@ -0,0 +1,119 @@ +package kr.co.accura.wtm.domain.report.service; + +import kr.co.accura.wtm.domain.report.dto.ProjectHoursReport; +import kr.co.accura.wtm.domain.report.dto.ReportFilter; +import kr.co.accura.wtm.domain.report.dto.WbsHoursReport; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ReportService { + + private final EntityManager em; + + public ProjectHoursReport getProjectHoursReport(ReportFilter filter) { + StringBuilder sql = new StringBuilder( + "SELECT te.epc_project_id, p.project_code, p.name, '' as discipline, " + + "te.entry_date, SUM(te.hours) " + + "FROM timesheet_entries te " + + "JOIN timesheets ts ON te.timesheet_id = ts.id " + + "LEFT JOIN projects p ON te.epc_project_id = p.id " + + "WHERE ts.status = 'APPROVED' "); + + if (filter.projectId() != null) { + sql.append("AND te.epc_project_id = :projectId "); + } + if (filter.from() != null) { + sql.append("AND te.entry_date >= :fromDate "); + } + if (filter.to() != null) { + sql.append("AND te.entry_date <= :toDate "); + } + sql.append("GROUP BY te.epc_project_id, p.project_code, p.name, te.entry_date "); + sql.append("ORDER BY te.entry_date"); + + Query query = em.createNativeQuery(sql.toString()); + if (filter.projectId() != null) query.setParameter("projectId", filter.projectId()); + if (filter.from() != null) query.setParameter("fromDate", filter.from()); + if (filter.to() != null) query.setParameter("toDate", filter.to()); + + @SuppressWarnings("unchecked") + List results = query.getResultList(); + + List rows = new ArrayList<>(); + BigDecimal totalHours = BigDecimal.ZERO; + + for (Object[] row : results) { + BigDecimal hours = row[5] != null ? new BigDecimal(row[5].toString()) : BigDecimal.ZERO; + rows.add(new ProjectHoursReport.ProjectHoursRow( + row[0] != null ? ((Number) row[0]).longValue() : null, + row[1] != null ? row[1].toString() : null, + row[2] != null ? row[2].toString() : null, + row[3] != null ? row[3].toString() : null, + row[4] != null ? java.time.LocalDate.parse(row[4].toString()) : null, + hours + )); + totalHours = totalHours.add(hours); + } + + return new ProjectHoursReport(filter, rows, totalHours, LocalDateTime.now()); + } + + public WbsHoursReport getWbsHoursReport(ReportFilter filter) { + StringBuilder sql = new StringBuilder( + "SELECT cw.wbs_code, cw.name, cw.level, cw.discipline, " + + "SUM(te.hours), COUNT(DISTINCT ts.user_id) " + + "FROM timesheet_entries te " + + "JOIN timesheets ts ON te.timesheet_id = ts.id " + + "JOIN canonical_wbs cw ON te.canonical_wbs_id = cw.id " + + "WHERE ts.status = 'APPROVED' "); + + if (filter.projectId() != null) { + sql.append("AND cw.project_id = :projectId "); + } + if (filter.wbsLevel() != null) { + sql.append("AND cw.level = :wbsLevel "); + } + if (filter.from() != null) { + sql.append("AND te.entry_date >= :fromDate "); + } + if (filter.to() != null) { + sql.append("AND te.entry_date <= :toDate "); + } + sql.append("GROUP BY cw.wbs_code, cw.name, cw.level, cw.discipline "); + sql.append("ORDER BY cw.wbs_code"); + + Query query = em.createNativeQuery(sql.toString()); + if (filter.projectId() != null) query.setParameter("projectId", filter.projectId()); + if (filter.wbsLevel() != null) query.setParameter("wbsLevel", filter.wbsLevel()); + if (filter.from() != null) query.setParameter("fromDate", filter.from()); + if (filter.to() != null) query.setParameter("toDate", filter.to()); + + @SuppressWarnings("unchecked") + List results = query.getResultList(); + + List rows = new ArrayList<>(); + for (Object[] row : results) { + rows.add(new WbsHoursReport.WbsHoursRow( + row[0] != null ? row[0].toString() : null, + row[1] != null ? row[1].toString() : null, + row[2] != null ? ((Number) row[2]).intValue() : 0, + row[3] != null ? row[3].toString() : null, + row[4] != null ? new BigDecimal(row[4].toString()) : BigDecimal.ZERO, + row[5] != null ? ((Number) row[5]).longValue() : 0 + )); + } + + return new WbsHoursReport(filter, rows, LocalDateTime.now()); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/dto/TealEntryDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/dto/TealEntryDto.java new file mode 100644 index 0000000..fa08472 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/dto/TealEntryDto.java @@ -0,0 +1,32 @@ +package kr.co.accura.wtm.domain.teal.dto; + +import kr.co.accura.wtm.domain.teal.entity.TealEntry; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TealEntryDto { + + private Long id; + private String wbsCode; + private Long canonicalWbsId; + private String activityCode; + private String activityName; + private String discipline; + private Boolean isActive; + + public static TealEntryDto from(TealEntry entry) { + return TealEntryDto.builder() + .id(entry.getId()) + .wbsCode(entry.getCanonicalWbs() != null ? entry.getCanonicalWbs().getWbsCode() : null) + .canonicalWbsId(entry.getCanonicalWbs() != null ? entry.getCanonicalWbs().getId() : null) + .activityCode(entry.getActivityCode()) + .activityName(entry.getActivityName()) + .discipline(entry.getDiscipline()) + .isActive(entry.getIsActive()) + .build(); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/dto/TealVersionDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/dto/TealVersionDto.java new file mode 100644 index 0000000..4993d29 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/dto/TealVersionDto.java @@ -0,0 +1,32 @@ +package kr.co.accura.wtm.domain.teal.dto; + +import kr.co.accura.wtm.domain.teal.entity.TealVersion; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +public record TealVersionDto( + Long id, + Long projectId, + Integer versionNumber, + LocalDate effectiveDate, + String description, + String status, + Long uploadedBy, + LocalDateTime createdAt, + int entryCount +) { + public static TealVersionDto from(TealVersion version, int entryCount) { + return new TealVersionDto( + version.getId(), + version.getProjectId(), + version.getVersionNumber(), + version.getEffectiveDate(), + version.getDescription(), + version.getStatus(), + version.getUploadedBy(), + version.getCreatedAt(), + entryCount + ); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/entity/TealEntry.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/entity/TealEntry.java new file mode 100644 index 0000000..701aca1 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/entity/TealEntry.java @@ -0,0 +1,40 @@ +package kr.co.accura.wtm.domain.teal.entity; + +import jakarta.persistence.*; +import kr.co.accura.wtm.domain.wbs.entity.CanonicalWbs; +import lombok.*; + +@Entity +@Table(name = "teal_entries") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TealEntry { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "teal_version_id", nullable = false) + private TealVersion tealVersion; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "canonical_wbs_id") + private CanonicalWbs canonicalWbs; + + @Column(name = "activity_code", nullable = false, length = 100) + private String activityCode; + + @Column(name = "activity_name", length = 500) + private String activityName; + + @Column(length = 50) + private String discipline; + + @Column(name = "is_active") + @Builder.Default + private Boolean isActive = true; +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/entity/TealVersion.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/entity/TealVersion.java new file mode 100644 index 0000000..569c5ec --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/entity/TealVersion.java @@ -0,0 +1,64 @@ +package kr.co.accura.wtm.domain.teal.entity; + +import jakarta.persistence.*; +import kr.co.accura.wtm.domain.project.entity.Project; +import lombok.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Entity +@Table(name = "teal_versions", + uniqueConstraints = @UniqueConstraint(columnNames = {"project_id", "version_number"})) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TealVersion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "project_id", nullable = false) + private Project project; + + @Column(name = "version_number", nullable = false) + private Integer versionNumber; + + @Column(name = "effective_date", nullable = false) + private LocalDate effectiveDate; + + @Column(length = 500) + private String description; + + @Column(length = 20) + @Builder.Default + private String status = "DRAFT"; + + @Column(name = "uploaded_by") + private Long uploadedBy; + + @Column(name = "created_at") + @Builder.Default + private LocalDateTime createdAt = LocalDateTime.now(); + + public void activate() { + this.status = "ACTIVE"; + } + + public Long getProjectId() { + return project != null ? project.getId() : null; + } + + public static TealVersion create(Project project, LocalDate effectiveDate, int versionNumber) { + return TealVersion.builder() + .project(project) + .versionNumber(versionNumber) + .effectiveDate(effectiveDate) + .status("DRAFT") + .build(); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/repository/TealEntryRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/repository/TealEntryRepository.java new file mode 100644 index 0000000..c320053 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/repository/TealEntryRepository.java @@ -0,0 +1,17 @@ +package kr.co.accura.wtm.domain.teal.repository; + +import kr.co.accura.wtm.domain.teal.entity.TealEntry; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface TealEntryRepository extends JpaRepository { + + List findByTealVersion_Id(Long tealVersionId); + + List findByTealVersion_IdAndIsActiveTrue(Long tealVersionId); + + List findByCanonicalWbs_Id(Long canonicalWbsId); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/repository/TealVersionRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/repository/TealVersionRepository.java new file mode 100644 index 0000000..e4df2eb --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/repository/TealVersionRepository.java @@ -0,0 +1,21 @@ +package kr.co.accura.wtm.domain.teal.repository; + +import kr.co.accura.wtm.domain.teal.entity.TealVersion; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface TealVersionRepository extends JpaRepository { + + List findByProject_IdOrderByVersionNumberDesc(Long projectId); + + @Query("SELECT MAX(v.versionNumber) FROM TealVersion v WHERE v.project.id = :projectId") + Optional findMaxVersionByProjectId(@Param("projectId") Long projectId); + + Optional findByProject_IdAndStatus(Long projectId, String status); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/service/TealService.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/service/TealService.java new file mode 100644 index 0000000..96864c6 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/teal/service/TealService.java @@ -0,0 +1,94 @@ +package kr.co.accura.wtm.domain.teal.service; + +import kr.co.accura.wbx.spring.common.BusinessException; +import kr.co.accura.wbx.spring.common.NotFoundException; +import kr.co.accura.wtm.domain.project.entity.Project; +import kr.co.accura.wtm.domain.project.repository.ProjectRepository; +import kr.co.accura.wtm.domain.teal.dto.TealEntryDto; +import kr.co.accura.wtm.domain.teal.dto.TealVersionDto; +import kr.co.accura.wtm.domain.teal.entity.TealEntry; +import kr.co.accura.wtm.domain.teal.entity.TealVersion; +import kr.co.accura.wtm.domain.teal.repository.TealEntryRepository; +import kr.co.accura.wtm.domain.teal.repository.TealVersionRepository; +import kr.co.accura.wtm.domain.wbs.repository.CanonicalWbsRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDate; +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class TealService { + + private final TealVersionRepository tealVersionRepository; + private final TealEntryRepository tealEntryRepository; + private final CanonicalWbsRepository canonicalWbsRepository; + private final ProjectRepository projectRepository; + + /** + * Upload TEAL file - Activity list linked to Canonical WBS. + */ + public TealVersionDto uploadTeal(Long projectId, MultipartFile file, + LocalDate effectiveDate, String description) { + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> new NotFoundException("Project not found: " + projectId)); + + // TODO: Parse TEAL file (WBS Code | Activity Code | Activity Name | Discipline) + // For now, create an empty version as a stub + + int nextVersion = tealVersionRepository + .findMaxVersionByProjectId(projectId) + .map(v -> v + 1).orElse(1); + + TealVersion version = TealVersion.builder() + .project(project) + .versionNumber(nextVersion) + .effectiveDate(effectiveDate) + .description(description) + .status("DRAFT") + .build(); + tealVersionRepository.save(version); + + return TealVersionDto.from(version, 0); + } + + /** + * Get TEAL versions for a project. + */ + @Transactional(readOnly = true) + public List getVersions(Long projectId) { + return tealVersionRepository.findByProject_IdOrderByVersionNumberDesc(projectId).stream() + .map(v -> { + int count = tealEntryRepository.findByTealVersion_Id(v.getId()).size(); + return TealVersionDto.from(v, count); + }) + .toList(); + } + + /** + * Get active TEAL entries for timesheet input. + */ + @Transactional(readOnly = true) + public List getActiveTealEntries(Long projectId) { + TealVersion activeVersion = tealVersionRepository.findByProject_IdAndStatus(projectId, "ACTIVE") + .orElseThrow(() -> new NotFoundException("No active TEAL version found for project: " + projectId)); + + return tealEntryRepository.findByTealVersion_IdAndIsActiveTrue(activeVersion.getId()).stream() + .map(TealEntryDto::from) + .toList(); + } + + /** + * Get TEAL entries by canonical WBS ID. + */ + @Transactional(readOnly = true) + public List getEntriesByWbs(Long projectId, Long wbsId) { + return tealEntryRepository.findByCanonicalWbs_Id(wbsId).stream() + .map(TealEntryDto::from) + .toList(); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/NonProjectCategory.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/NonProjectCategory.java new file mode 100644 index 0000000..2245d6d --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/NonProjectCategory.java @@ -0,0 +1,17 @@ +package kr.co.accura.wtm.domain.timesheet; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NonProjectCategory { + ANNUAL_LEAVE("연차"), + SICK_LEAVE("병가"), + TRAINING("교육"), + ADMIN("행정"), + PUBLIC_HOLIDAY("공휴일"), + OTHER("기타"); + + private final String displayName; +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/TimesheetEntryType.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/TimesheetEntryType.java new file mode 100644 index 0000000..e64df8e --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/TimesheetEntryType.java @@ -0,0 +1,7 @@ +package kr.co.accura.wtm.domain.timesheet; + +public enum TimesheetEntryType { + NON_PROJECT, + OTHER_PROJECT, + EPC +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/TimesheetStatus.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/TimesheetStatus.java new file mode 100644 index 0000000..6a34602 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/TimesheetStatus.java @@ -0,0 +1,9 @@ +package kr.co.accura.wtm.domain.timesheet; + +public enum TimesheetStatus { + DRAFT, + SUBMITTED, + DL_APPROVED, + APPROVED, + REJECTED +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/dto/TimesheetDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/dto/TimesheetDto.java new file mode 100644 index 0000000..6d8fb6a --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/dto/TimesheetDto.java @@ -0,0 +1,17 @@ +package kr.co.accura.wtm.domain.timesheet.dto; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +public record TimesheetDto( + Long id, + Long userId, + LocalDate weekStartDate, + LocalDate weekEndDate, + String status, + BigDecimal totalHours, + LocalDateTime submittedAt, + List entries +) {} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/dto/TimesheetEntryDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/dto/TimesheetEntryDto.java new file mode 100644 index 0000000..154aff5 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/dto/TimesheetEntryDto.java @@ -0,0 +1,19 @@ +package kr.co.accura.wtm.domain.timesheet.dto; + +import java.math.BigDecimal; +import java.time.LocalDate; + +public record TimesheetEntryDto( + Long id, + String entryType, + LocalDate entryDate, + BigDecimal hours, + String npCategory, + Long otherProjectId, + String otherCategory, + Long epcProjectId, + Long canonicalWbsId, + Long tealEntryId, + Integer revisionNumber, + String remark +) {} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/dto/TimesheetEntryRequest.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/dto/TimesheetEntryRequest.java new file mode 100644 index 0000000..86cf9f7 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/dto/TimesheetEntryRequest.java @@ -0,0 +1,22 @@ +package kr.co.accura.wtm.domain.timesheet.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; + +import java.math.BigDecimal; +import java.time.LocalDate; + +public record TimesheetEntryRequest( + Long id, + @NotNull String entryType, + @NotNull LocalDate entryDate, + @NotNull @PositiveOrZero BigDecimal hours, + String npCategory, + Long otherProjectId, + String otherCategory, + Long epcProjectId, + Long canonicalWbsId, + Long tealEntryId, + Integer revisionNumber, + String remark +) {} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/dto/UploadResultDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/dto/UploadResultDto.java new file mode 100644 index 0000000..d511d44 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/dto/UploadResultDto.java @@ -0,0 +1,10 @@ +package kr.co.accura.wtm.domain.timesheet.dto; + +public record UploadResultDto( + Long uploadId, + int totalRows, + int successRows, + int errorRows, + String status, + String errorLog +) {} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/entity/Timesheet.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/entity/Timesheet.java new file mode 100644 index 0000000..d52fdbc --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/entity/Timesheet.java @@ -0,0 +1,75 @@ +package kr.co.accura.wtm.domain.timesheet.entity; + +import jakarta.persistence.*; +import kr.co.accura.wbx.spring.common.BaseEntity; +import kr.co.accura.wbx.spring.common.BusinessException; +import kr.co.accura.wtm.domain.timesheet.TimesheetStatus; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "timesheets", uniqueConstraints = { + @UniqueConstraint(columnNames = {"user_id", "week_start_date"}) +}) +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Timesheet extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "week_start_date", nullable = false) + private LocalDate weekStartDate; + + @Column(name = "week_end_date", nullable = false) + private LocalDate weekEndDate; + + @Enumerated(EnumType.STRING) + @Column(length = 20) + @Builder.Default + private TimesheetStatus status = TimesheetStatus.DRAFT; + + @Column(name = "total_hours", precision = 10, scale = 2) + @Builder.Default + private BigDecimal totalHours = BigDecimal.ZERO; + + @Column(name = "submitted_at") + private LocalDateTime submittedAt; + + @OneToMany(mappedBy = "timesheet", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List entries = new ArrayList<>(); + + // Business methods + + public void addEntry(TimesheetEntry entry) { + entries.add(entry); + entry.setTimesheet(this); + recalculateTotal(); + } + + public void submit() { + if (status != TimesheetStatus.DRAFT && status != TimesheetStatus.REJECTED) { + throw new BusinessException("DRAFT 또는 REJECTED 상태에서만 제출 가능합니다."); + } + this.status = TimesheetStatus.SUBMITTED; + this.submittedAt = LocalDateTime.now(); + } + + public void recalculateTotal() { + this.totalHours = entries.stream() + .map(TimesheetEntry::getHours) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/entity/TimesheetEntry.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/entity/TimesheetEntry.java new file mode 100644 index 0000000..968976b --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/entity/TimesheetEntry.java @@ -0,0 +1,66 @@ +package kr.co.accura.wtm.domain.timesheet.entity; + +import jakarta.persistence.*; +import kr.co.accura.wbx.spring.common.BaseEntity; +import kr.co.accura.wtm.domain.timesheet.NonProjectCategory; +import kr.co.accura.wtm.domain.timesheet.TimesheetEntryType; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Entity +@Table(name = "timesheet_entries") +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TimesheetEntry extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "timesheet_id", nullable = false) + private Timesheet timesheet; + + @Enumerated(EnumType.STRING) + @Column(name = "entry_type", nullable = false, length = 20) + private TimesheetEntryType entryType; + + @Column(name = "entry_date", nullable = false) + private LocalDate entryDate; + + @Column(nullable = false, precision = 5, scale = 2) + @Builder.Default + private BigDecimal hours = BigDecimal.ZERO; + + // Non-Project fields + @Column(name = "np_category", length = 100) + private String npCategory; + + // Other Project fields + @Column(name = "other_project_id") + private Long otherProjectId; + + @Column(name = "other_category", length = 100) + private String otherCategory; + + // EPC Project fields + @Column(name = "epc_project_id") + private Long epcProjectId; + + @Column(name = "canonical_wbs_id") + private Long canonicalWbsId; + + @Column(name = "teal_entry_id") + private Long tealEntryId; + + @Column(name = "revision_number") + @Builder.Default + private Integer revisionNumber = 1; + + @Column(length = 500) + private String remark; +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/entity/TimesheetUpload.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/entity/TimesheetUpload.java new file mode 100644 index 0000000..41de1e0 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/entity/TimesheetUpload.java @@ -0,0 +1,42 @@ +package kr.co.accura.wtm.domain.timesheet.entity; + +import jakarta.persistence.*; +import kr.co.accura.wbx.spring.common.BaseEntity; +import lombok.*; + +@Entity +@Table(name = "timesheet_uploads") +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TimesheetUpload extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(length = 500) + private String filename; + + @Column(name = "file_path", length = 1000) + private String filePath; + + @Column(name = "total_rows") + private Integer totalRows; + + @Column(name = "success_rows") + private Integer successRows; + + @Column(name = "error_rows") + private Integer errorRows; + + @Column(name = "error_log", columnDefinition = "TEXT") + private String errorLog; + + @Column(length = 20) + private String status; +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/repository/TimesheetEntryRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/repository/TimesheetEntryRepository.java new file mode 100644 index 0000000..26f63ba --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/repository/TimesheetEntryRepository.java @@ -0,0 +1,11 @@ +package kr.co.accura.wtm.domain.timesheet.repository; + +import kr.co.accura.wtm.domain.timesheet.entity.TimesheetEntry; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TimesheetEntryRepository extends JpaRepository { + + List findByTimesheet_Id(Long timesheetId); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/repository/TimesheetRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/repository/TimesheetRepository.java new file mode 100644 index 0000000..2eda242 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/repository/TimesheetRepository.java @@ -0,0 +1,28 @@ +package kr.co.accura.wtm.domain.timesheet.repository; + +import kr.co.accura.wtm.domain.timesheet.entity.Timesheet; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDate; +import java.util.Optional; + +public interface TimesheetRepository extends JpaRepository { + + Optional findByUserIdAndWeekStartDate(Long userId, LocalDate weekStartDate); + + Page findByUserId(Long userId, Pageable pageable); + + @Query("SELECT t FROM Timesheet t WHERE t.userId = :userId " + + "AND (:from IS NULL OR t.weekStartDate >= :from) " + + "AND (:to IS NULL OR t.weekStartDate <= :to) " + + "ORDER BY t.weekStartDate DESC") + Page findByUserIdAndDateRange( + @Param("userId") Long userId, + @Param("from") LocalDate from, + @Param("to") LocalDate to, + Pageable pageable); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/repository/TimesheetUploadRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/repository/TimesheetUploadRepository.java new file mode 100644 index 0000000..e4a1e61 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/repository/TimesheetUploadRepository.java @@ -0,0 +1,7 @@ +package kr.co.accura.wtm.domain.timesheet.repository; + +import kr.co.accura.wtm.domain.timesheet.entity.TimesheetUpload; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TimesheetUploadRepository extends JpaRepository { +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/rule/TimesheetRuleEngine.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/rule/TimesheetRuleEngine.java new file mode 100644 index 0000000..6480735 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/rule/TimesheetRuleEngine.java @@ -0,0 +1,73 @@ +package kr.co.accura.wtm.domain.timesheet.rule; + +import kr.co.accura.wtm.domain.timesheet.TimesheetEntryType; +import kr.co.accura.wtm.domain.timesheet.entity.Timesheet; +import kr.co.accura.wtm.domain.timesheet.entity.TimesheetEntry; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Map; +import java.util.stream.Collectors; + +@Component +public class TimesheetRuleEngine { + + private static final BigDecimal MAX_DAILY_HOURS = new BigDecimal("8"); + private static final BigDecimal MAX_WEEKLY_HOURS = new BigDecimal("52"); + private static final BigDecimal WARN_DAILY_HOURS = new BigDecimal("10"); + private static final BigDecimal HARD_DAILY_LIMIT = new BigDecimal("24"); + + /** + * Validate timesheet entries against business rules. + */ + public ValidationResult validate(Timesheet timesheet) { + var result = new ValidationResult(); + + // 1. Daily hour limits + Map dailyTotals = timesheet.getEntries().stream() + .collect(Collectors.groupingBy( + TimesheetEntry::getEntryDate, + Collectors.reducing(BigDecimal.ZERO, TimesheetEntry::getHours, BigDecimal::add) + )); + + for (var entry : dailyTotals.entrySet()) { + LocalDate date = entry.getKey(); + BigDecimal total = entry.getValue(); + + if (total.compareTo(HARD_DAILY_LIMIT) > 0) { + result.addError(date, "일 최대 24시간을 초과할 수 없습니다."); + } else if (total.compareTo(WARN_DAILY_HOURS) > 0) { + result.addWarning(date, + String.format("일 %s시간 입력 — 기준(%sh) 초과", total, MAX_DAILY_HOURS)); + } + } + + // 2. Weekly total limit (52h) + if (timesheet.getTotalHours().compareTo(MAX_WEEKLY_HOURS) > 0) { + result.addError(null, + String.format("주간 합계 %s시간 — 최대 %sh 초과", + timesheet.getTotalHours(), MAX_WEEKLY_HOURS)); + } + + // 3. EPC entries require project and WBS + timesheet.getEntries().stream() + .filter(e -> e.getEntryType() == TimesheetEntryType.EPC) + .forEach(e -> { + if (e.getEpcProjectId() == null) { + result.addError(e.getEntryDate(), "EPC 시수 — 프로젝트 필수"); + } + if (e.getCanonicalWbsId() == null) { + result.addError(e.getEntryDate(), "EPC 시수 — WBS 선택 필수"); + } + }); + + // 4. No future dates + LocalDate today = LocalDate.now(); + timesheet.getEntries().stream() + .filter(e -> e.getEntryDate().isAfter(today)) + .forEach(e -> result.addError(e.getEntryDate(), "미래 날짜에 시수를 입력할 수 없습니다.")); + + return result; + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/rule/ValidationResult.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/rule/ValidationResult.java new file mode 100644 index 0000000..125f4b5 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/rule/ValidationResult.java @@ -0,0 +1,32 @@ +package kr.co.accura.wtm.domain.timesheet.rule; + +import lombok.Getter; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@Getter +public class ValidationResult { + + private final List errors = new ArrayList<>(); + private final List warnings = new ArrayList<>(); + + public void addError(LocalDate date, String message) { + errors.add(new ValidationMessage(date, message)); + } + + public void addWarning(LocalDate date, String message) { + warnings.add(new ValidationMessage(date, message)); + } + + public boolean hasErrors() { + return !errors.isEmpty(); + } + + public boolean hasWarnings() { + return !warnings.isEmpty(); + } + + public record ValidationMessage(LocalDate date, String message) {} +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/service/TimesheetService.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/service/TimesheetService.java new file mode 100644 index 0000000..891ea16 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/timesheet/service/TimesheetService.java @@ -0,0 +1,209 @@ +package kr.co.accura.wtm.domain.timesheet.service; + +import kr.co.accura.wbx.spring.common.BusinessException; +import kr.co.accura.wbx.spring.common.NotFoundException; +import kr.co.accura.wtm.domain.timesheet.TimesheetEntryType; +import kr.co.accura.wtm.domain.timesheet.TimesheetStatus; +import kr.co.accura.wtm.domain.timesheet.dto.*; +import kr.co.accura.wtm.domain.timesheet.entity.Timesheet; +import kr.co.accura.wtm.domain.timesheet.entity.TimesheetEntry; +import kr.co.accura.wtm.domain.timesheet.entity.TimesheetUpload; +import kr.co.accura.wtm.domain.timesheet.repository.TimesheetEntryRepository; +import kr.co.accura.wtm.domain.timesheet.repository.TimesheetRepository; +import kr.co.accura.wtm.domain.timesheet.repository.TimesheetUploadRepository; +import kr.co.accura.wtm.domain.timesheet.rule.TimesheetRuleEngine; +import kr.co.accura.wtm.domain.timesheet.rule.ValidationResult; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDate; +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class TimesheetService { + + private final TimesheetRepository timesheetRepository; + private final TimesheetEntryRepository timesheetEntryRepository; + private final TimesheetUploadRepository timesheetUploadRepository; + private final TimesheetRuleEngine ruleEngine; + + public TimesheetDto getOrCreateWeekly(Long userId, LocalDate weekStart) { + LocalDate weekEnd = weekStart.plusDays(6); + Timesheet ts = timesheetRepository.findByUserIdAndWeekStartDate(userId, weekStart) + .orElseGet(() -> { + Timesheet newTs = Timesheet.builder() + .userId(userId) + .weekStartDate(weekStart) + .weekEndDate(weekEnd) + .build(); + return timesheetRepository.save(newTs); + }); + return toDto(ts); + } + + public TimesheetEntryDto saveEntry(Long timesheetId, TimesheetEntryRequest request) { + Timesheet ts = findTimesheet(timesheetId); + ensureEditable(ts); + + TimesheetEntry entry; + if (request.id() != null) { + entry = timesheetEntryRepository.findById(request.id()) + .orElseThrow(() -> new NotFoundException("시수 항목을 찾을 수 없습니다.")); + updateEntry(entry, request); + } else { + entry = createEntry(ts, request); + ts.addEntry(entry); + } + + timesheetEntryRepository.save(entry); + ts.recalculateTotal(); + timesheetRepository.save(ts); + + return toEntryDto(entry); + } + + public TimesheetDto saveBatch(Long timesheetId, List requests) { + Timesheet ts = findTimesheet(timesheetId); + ensureEditable(ts); + + // Clear existing entries and replace + ts.getEntries().clear(); + for (TimesheetEntryRequest request : requests) { + TimesheetEntry entry = createEntry(ts, request); + ts.addEntry(entry); + } + + ts.recalculateTotal(); + timesheetRepository.save(ts); + return toDto(ts); + } + + public TimesheetDto submit(Long timesheetId) { + Timesheet ts = findTimesheet(timesheetId); + + // Validate before submission + ValidationResult validation = ruleEngine.validate(ts); + if (validation.hasErrors()) { + String errorMsg = validation.getErrors().stream() + .map(e -> e.message()) + .reduce((a, b) -> a + "; " + b) + .orElse("검증 오류"); + throw new BusinessException(errorMsg); + } + + ts.submit(); + timesheetRepository.save(ts); + return toDto(ts); + } + + @Transactional(readOnly = true) + public Page getHistory(Long userId, LocalDate from, LocalDate to, Pageable pageable) { + return timesheetRepository.findByUserIdAndDateRange(userId, from, to, pageable) + .map(this::toDto); + } + + public UploadResultDto uploadExcel(Long userId, MultipartFile file, LocalDate weekStart) { + TimesheetUpload upload = TimesheetUpload.builder() + .userId(userId) + .filename(file.getOriginalFilename()) + .status("PROCESSING") + .totalRows(0) + .successRows(0) + .errorRows(0) + .build(); + upload = timesheetUploadRepository.save(upload); + + // TODO: Implement Excel parsing with Apache POI + upload.setStatus("COMPLETED"); + timesheetUploadRepository.save(upload); + + return new UploadResultDto( + upload.getId(), + upload.getTotalRows() != null ? upload.getTotalRows() : 0, + upload.getSuccessRows() != null ? upload.getSuccessRows() : 0, + upload.getErrorRows() != null ? upload.getErrorRows() : 0, + upload.getStatus(), + upload.getErrorLog() + ); + } + + public void deleteEntry(Long timesheetId, Long entryId) { + Timesheet ts = findTimesheet(timesheetId); + ensureEditable(ts); + ts.getEntries().removeIf(e -> e.getId().equals(entryId)); + ts.recalculateTotal(); + timesheetRepository.save(ts); + } + + // --- Helper methods --- + + private Timesheet findTimesheet(Long id) { + return timesheetRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Timesheet not found: " + id)); + } + + private void ensureEditable(Timesheet ts) { + if (ts.getStatus() != TimesheetStatus.DRAFT && ts.getStatus() != TimesheetStatus.REJECTED) { + throw new BusinessException("수정 가능한 상태가 아닙니다 (현재: " + ts.getStatus() + ")"); + } + } + + private TimesheetEntry createEntry(Timesheet ts, TimesheetEntryRequest req) { + return TimesheetEntry.builder() + .timesheet(ts) + .entryType(TimesheetEntryType.valueOf(req.entryType())) + .entryDate(req.entryDate()) + .hours(req.hours()) + .npCategory(req.npCategory()) + .otherProjectId(req.otherProjectId()) + .otherCategory(req.otherCategory()) + .epcProjectId(req.epcProjectId()) + .canonicalWbsId(req.canonicalWbsId()) + .tealEntryId(req.tealEntryId()) + .revisionNumber(req.revisionNumber() != null ? req.revisionNumber() : 1) + .remark(req.remark()) + .build(); + } + + private void updateEntry(TimesheetEntry entry, TimesheetEntryRequest req) { + entry.setEntryType(TimesheetEntryType.valueOf(req.entryType())); + entry.setEntryDate(req.entryDate()); + entry.setHours(req.hours()); + entry.setNpCategory(req.npCategory()); + entry.setOtherProjectId(req.otherProjectId()); + entry.setOtherCategory(req.otherCategory()); + entry.setEpcProjectId(req.epcProjectId()); + entry.setCanonicalWbsId(req.canonicalWbsId()); + entry.setTealEntryId(req.tealEntryId()); + entry.setRevisionNumber(req.revisionNumber() != null ? req.revisionNumber() : 1); + entry.setRemark(req.remark()); + } + + private TimesheetDto toDto(Timesheet ts) { + List entryDtos = ts.getEntries().stream() + .map(this::toEntryDto) + .toList(); + return new TimesheetDto( + ts.getId(), ts.getUserId(), + ts.getWeekStartDate(), ts.getWeekEndDate(), + ts.getStatus().name(), ts.getTotalHours(), + ts.getSubmittedAt(), entryDtos + ); + } + + private TimesheetEntryDto toEntryDto(TimesheetEntry e) { + return new TimesheetEntryDto( + e.getId(), e.getEntryType().name(), e.getEntryDate(), + e.getHours(), e.getNpCategory(), + e.getOtherProjectId(), e.getOtherCategory(), + e.getEpcProjectId(), e.getCanonicalWbsId(), + e.getTealEntryId(), e.getRevisionNumber(), e.getRemark() + ); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/EmploymentType.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/EmploymentType.java new file mode 100644 index 0000000..2054bd7 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/EmploymentType.java @@ -0,0 +1,6 @@ +package kr.co.accura.wtm.domain.user; + +public enum EmploymentType { + INTERNAL, + SUBCONTRACTOR +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/dto/RoleAssignRequest.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/dto/RoleAssignRequest.java new file mode 100644 index 0000000..c67d9b4 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/dto/RoleAssignRequest.java @@ -0,0 +1,13 @@ +package kr.co.accura.wtm.domain.user.dto; + +import java.util.List; + +public record RoleAssignRequest( + List roles +) { + public record RoleEntry( + String roleCode, + Long projectId + ) { + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/dto/UserCreateRequest.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/dto/UserCreateRequest.java new file mode 100644 index 0000000..986989b --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/dto/UserCreateRequest.java @@ -0,0 +1,17 @@ +package kr.co.accura.wtm.domain.user.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record UserCreateRequest( + @NotBlank String employeeId, + @NotBlank @Email String email, + @NotBlank String username, + String fullName, + String department, + String discipline, + String positionTitle, + String location, + String employmentType +) { +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/dto/UserDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/dto/UserDto.java new file mode 100644 index 0000000..02c8706 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/dto/UserDto.java @@ -0,0 +1,40 @@ +package kr.co.accura.wtm.domain.user.dto; + +import kr.co.accura.wtm.domain.user.entity.User; + +import java.util.List; + +public record UserDto( + Long id, + String employeeId, + String email, + String username, + String fullName, + String department, + String discipline, + String positionTitle, + String location, + String employmentType, + Boolean isActive, + List roles +) { + public static UserDto from(User user) { + List roleNames = user.getUserRoles().stream() + .map(ur -> ur.getRole().getCode()) + .toList(); + return new UserDto( + user.getId(), + user.getEmployeeId(), + user.getEmail(), + user.getUsername(), + user.getFullName(), + user.getDepartment(), + user.getDiscipline(), + user.getPositionTitle(), + user.getLocation(), + user.getEmploymentType() != null ? user.getEmploymentType().name() : null, + user.getIsActive(), + roleNames + ); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/dto/UserUpdateRequest.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/dto/UserUpdateRequest.java new file mode 100644 index 0000000..8cb5588 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/dto/UserUpdateRequest.java @@ -0,0 +1,13 @@ +package kr.co.accura.wtm.domain.user.dto; + +public record UserUpdateRequest( + String username, + String fullName, + String department, + String discipline, + String positionTitle, + String location, + String employmentType, + Boolean isActive +) { +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/entity/HrUpload.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/entity/HrUpload.java new file mode 100644 index 0000000..9d7b816 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/entity/HrUpload.java @@ -0,0 +1,52 @@ +package kr.co.accura.wtm.domain.user.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "hr_uploads") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class HrUpload { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(length = 500) + private String filename; + + @Column(name = "file_path", length = 1000) + private String filePath; + + @Column(name = "total_rows") + private Integer totalRows; + + @Column(name = "success_rows") + private Integer successRows; + + @Column(name = "error_rows") + private Integer errorRows; + + @Column(name = "error_log", columnDefinition = "TEXT") + private String errorLog; + + @Column(length = 20) + private String status; + + @Column(name = "sync_source", length = 50) + private String syncSource; + + @Column(name = "created_at") + @Builder.Default + private LocalDateTime createdAt = LocalDateTime.now(); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/entity/OrgHierarchy.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/entity/OrgHierarchy.java new file mode 100644 index 0000000..ba04128 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/entity/OrgHierarchy.java @@ -0,0 +1,48 @@ +package kr.co.accura.wtm.domain.user.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "org_hierarchy") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OrgHierarchy { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 255) + private String name; + + @Column(length = 50) + private String code; + + @Column(nullable = false) + private Integer level; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private OrgHierarchy parent; + + @Column(name = "sort_order") + @Builder.Default + private Integer sortOrder = 0; + + @Column(name = "is_active") + @Builder.Default + private Boolean isActive = true; + + @Column(name = "created_at") + @Builder.Default + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/entity/Role.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/entity/Role.java new file mode 100644 index 0000000..da618bf --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/entity/Role.java @@ -0,0 +1,31 @@ +package kr.co.accura.wtm.domain.user.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "roles") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Role { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false, length = 20) + private String code; + + @Column(nullable = false, length = 100) + private String name; + + @Column(length = 500) + private String description; + + @Column + @Builder.Default + private Integer level = 0; +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/entity/User.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/entity/User.java new file mode 100644 index 0000000..4014f32 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/entity/User.java @@ -0,0 +1,97 @@ +package kr.co.accura.wtm.domain.user.entity; + +import jakarta.persistence.*; +import kr.co.accura.wbx.spring.common.BaseEntity; +import kr.co.accura.wtm.domain.user.EmploymentType; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Entity +@Table(name = "users") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class User extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "employee_id", unique = true, nullable = false, length = 50) + private String employeeId; + + @Column(unique = true, nullable = false, length = 255) + private String email; + + @Column(nullable = false, length = 100) + private String username; + + @Column(name = "full_name", length = 255) + private String fullName; + + @Column(name = "hashed_password", length = 255) + private String hashedPassword; + + @Column(length = 100) + private String department; + + @Column(length = 100) + private String discipline; + + @Column(name = "position_title", length = 100) + private String positionTitle; + + @Column(length = 50) + private String location; + + @Enumerated(EnumType.STRING) + @Column(name = "employment_type", length = 20) + @Builder.Default + private EmploymentType employmentType = EmploymentType.INTERNAL; + + @Column(name = "is_active") + @Builder.Default + private Boolean isActive = true; + + @Column(name = "is_locked") + @Builder.Default + private Boolean isLocked = false; + + @Column(name = "failed_attempts") + @Builder.Default + private Integer failedAttempts = 0; + + @Column(name = "last_login_at") + private LocalDateTime lastLoginAt; + + @Column(name = "password_changed_at") + private LocalDateTime passwordChangedAt; + + @Column(name = "azure_oid", length = 255) + private String azureOid; + + @Column(name = "mfa_enabled") + @Builder.Default + private Boolean mfaEnabled = false; + + @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) + @Builder.Default + private List userRoles = new ArrayList<>(); + + public boolean hasRole(String roleCode) { + return userRoles.stream() + .anyMatch(ur -> ur.getRole().getCode().equals(roleCode)); + } + + public boolean hasProjectRole(String roleCode, Long projectId) { + return userRoles.stream() + .anyMatch(ur -> ur.getRole().getCode().equals(roleCode) + && Objects.equals(ur.getProjectId(), projectId)); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/entity/UserRole.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/entity/UserRole.java new file mode 100644 index 0000000..366ab86 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/entity/UserRole.java @@ -0,0 +1,39 @@ +package kr.co.accura.wtm.domain.user.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "user_roles", + uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "role_id", "project_id"})) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserRole { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "role_id", nullable = false) + private Role role; + + @Column(name = "project_id") + private Long projectId; + + @Column(name = "granted_at") + @Builder.Default + private LocalDateTime grantedAt = LocalDateTime.now(); + + @Column(name = "granted_by") + private Long grantedBy; +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/HrUploadRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/HrUploadRepository.java new file mode 100644 index 0000000..4d23a84 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/HrUploadRepository.java @@ -0,0 +1,15 @@ +package kr.co.accura.wtm.domain.user.repository; + +import kr.co.accura.wtm.domain.user.entity.HrUpload; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface HrUploadRepository extends JpaRepository { + + List findByUser_IdOrderByCreatedAtDesc(Long userId); + + List findByStatus(String status); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/OrgHierarchyRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/OrgHierarchyRepository.java new file mode 100644 index 0000000..f60fc21 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/OrgHierarchyRepository.java @@ -0,0 +1,20 @@ +package kr.co.accura.wtm.domain.user.repository; + +import kr.co.accura.wtm.domain.user.entity.OrgHierarchy; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface OrgHierarchyRepository extends JpaRepository { + + List findByParent_Id(Long parentId); + + List findByLevel(Integer level); + + List findByIsActiveTrueOrderBySortOrder(); + + Optional findByCode(String code); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/RoleRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/RoleRepository.java new file mode 100644 index 0000000..43c2325 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/RoleRepository.java @@ -0,0 +1,13 @@ +package kr.co.accura.wtm.domain.user.repository; + +import kr.co.accura.wtm.domain.user.entity.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RoleRepository extends JpaRepository { + + Optional findByCode(String code); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/UserRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..27249ff --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/UserRepository.java @@ -0,0 +1,19 @@ +package kr.co.accura.wtm.domain.user.repository; + +import kr.co.accura.wtm.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + + Optional findByEmail(String email); + + Optional findByEmployeeId(String employeeId); + + boolean existsByEmail(String email); + + boolean existsByEmployeeId(String employeeId); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/UserRoleRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/UserRoleRepository.java new file mode 100644 index 0000000..ec43d8b --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/UserRoleRepository.java @@ -0,0 +1,17 @@ +package kr.co.accura.wtm.domain.user.repository; + +import kr.co.accura.wtm.domain.user.entity.UserRole; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface UserRoleRepository extends JpaRepository { + + List findByUser_Id(Long userId); + + List findByUser_IdAndProjectId(Long userId, Long projectId); + + void deleteByUser_IdAndProjectId(Long userId, Long projectId); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/service/UserService.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/service/UserService.java new file mode 100644 index 0000000..1536150 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/service/UserService.java @@ -0,0 +1,119 @@ +package kr.co.accura.wtm.domain.user.service; + +import kr.co.accura.wbx.spring.common.BusinessException; +import kr.co.accura.wbx.spring.common.NotFoundException; +import kr.co.accura.wtm.domain.user.EmploymentType; +import kr.co.accura.wtm.domain.user.dto.*; +import kr.co.accura.wtm.domain.user.entity.Role; +import kr.co.accura.wtm.domain.user.entity.User; +import kr.co.accura.wtm.domain.user.entity.UserRole; +import kr.co.accura.wtm.domain.user.repository.RoleRepository; +import kr.co.accura.wtm.domain.user.repository.UserRepository; +import kr.co.accura.wtm.domain.user.repository.UserRoleRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final UserRoleRepository userRoleRepository; + + @Transactional(readOnly = true) + public Page findAll(Pageable pageable) { + return userRepository.findAll(pageable).map(UserDto::from); + } + + @Transactional(readOnly = true) + public UserDto findById(Long id) { + User user = userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("User not found: " + id)); + return UserDto.from(user); + } + + @Transactional(readOnly = true) + public UserDto findByEmail(String email) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new NotFoundException("User not found: " + email)); + return UserDto.from(user); + } + + public UserDto create(UserCreateRequest request) { + if (userRepository.existsByEmail(request.email())) { + throw new BusinessException("Email already exists: " + request.email()); + } + if (userRepository.existsByEmployeeId(request.employeeId())) { + throw new BusinessException("Employee ID already exists: " + request.employeeId()); + } + + User user = User.builder() + .employeeId(request.employeeId()) + .email(request.email()) + .username(request.username()) + .fullName(request.fullName()) + .department(request.department()) + .discipline(request.discipline()) + .positionTitle(request.positionTitle()) + .location(request.location()) + .employmentType(request.employmentType() != null + ? EmploymentType.valueOf(request.employmentType()) + : EmploymentType.INTERNAL) + .build(); + + userRepository.save(user); + return UserDto.from(user); + } + + public UserDto update(Long id, UserUpdateRequest request) { + User user = userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("User not found: " + id)); + + if (request.username() != null) user.setUsername(request.username()); + if (request.fullName() != null) user.setFullName(request.fullName()); + if (request.department() != null) user.setDepartment(request.department()); + if (request.discipline() != null) user.setDiscipline(request.discipline()); + if (request.positionTitle() != null) user.setPositionTitle(request.positionTitle()); + if (request.location() != null) user.setLocation(request.location()); + if (request.employmentType() != null) { + user.setEmploymentType(EmploymentType.valueOf(request.employmentType())); + } + if (request.isActive() != null) user.setIsActive(request.isActive()); + + userRepository.save(user); + return UserDto.from(user); + } + + public void assignRoles(Long userId, RoleAssignRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException("User not found: " + userId)); + + // Remove existing roles + userRoleRepository.deleteByUser_IdAndProjectId(userId, null); + + // Assign new roles + for (RoleAssignRequest.RoleEntry entry : request.roles()) { + Role role = roleRepository.findByCode(entry.roleCode()) + .orElseThrow(() -> new NotFoundException("Role not found: " + entry.roleCode())); + + UserRole userRole = UserRole.builder() + .user(user) + .role(role) + .projectId(entry.projectId()) + .build(); + userRoleRepository.save(userRole); + } + } + + @Transactional(readOnly = true) + public List getUserRoles(Long userId) { + return userRoleRepository.findByUser_Id(userId); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/CanonicalWbsDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/CanonicalWbsDto.java new file mode 100644 index 0000000..e57014b --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/CanonicalWbsDto.java @@ -0,0 +1,29 @@ +package kr.co.accura.wtm.domain.wbs.dto; + +import kr.co.accura.wtm.domain.wbs.entity.CanonicalWbs; + +public record CanonicalWbsDto( + Long id, + Long projectId, + String wbsCode, + Integer level, + String name, + String parentCode, + String discipline, + Boolean isActive, + String mappedP6Code +) { + public static CanonicalWbsDto from(CanonicalWbs cw) { + return new CanonicalWbsDto( + cw.getId(), + cw.getProjectId(), + cw.getWbsCode(), + cw.getLevel(), + cw.getName(), + cw.getParentCode(), + cw.getDiscipline(), + cw.getIsActive(), + cw.getMappedP6Code() + ); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/WbsCompareResult.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/WbsCompareResult.java new file mode 100644 index 0000000..43e4b08 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/WbsCompareResult.java @@ -0,0 +1,30 @@ +package kr.co.accura.wtm.domain.wbs.dto; + +import kr.co.accura.wtm.domain.wbs.entity.WbsNode; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +public class WbsCompareResult { + + private final List added = new ArrayList<>(); + private final List removed = new ArrayList<>(); + private final List modified = new ArrayList<>(); + + public void addAdded(WbsNode node) { + added.add(WbsNodeDto.from(node)); + } + + public void addRemoved(WbsNode node) { + removed.add(WbsNodeDto.from(node)); + } + + public void addModified(WbsNode before, WbsNode after) { + modified.add(new ModifiedEntry(WbsNodeDto.from(before), WbsNodeDto.from(after))); + } + + public record ModifiedEntry(WbsNodeDto before, WbsNodeDto after) { + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/WbsDisciplineAssignmentDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/WbsDisciplineAssignmentDto.java new file mode 100644 index 0000000..ef225b5 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/WbsDisciplineAssignmentDto.java @@ -0,0 +1,26 @@ +package kr.co.accura.wtm.domain.wbs.dto; + +import kr.co.accura.wtm.domain.wbs.entity.WbsDisciplineAssignment; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WbsDisciplineAssignmentDto { + + private Long id; + private Long canonicalWbsId; + private String discipline; + private Boolean isActive; + + public static WbsDisciplineAssignmentDto from(WbsDisciplineAssignment entity) { + return WbsDisciplineAssignmentDto.builder() + .id(entity.getId()) + .canonicalWbsId(entity.getCanonicalWbs().getId()) + .discipline(entity.getDiscipline()) + .isActive(entity.getIsActive()) + .build(); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/WbsNodeDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/WbsNodeDto.java new file mode 100644 index 0000000..fd07996 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/WbsNodeDto.java @@ -0,0 +1,36 @@ +package kr.co.accura.wtm.domain.wbs.dto; + +import kr.co.accura.wtm.domain.wbs.entity.WbsNode; +import lombok.*; + +import java.math.BigDecimal; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WbsNodeDto { + + private Long id; + private String wbsCode; + private String name; + private Integer level; + private String parentCode; + private String discipline; + private BigDecimal plannedHours; + private Boolean isLeaf; + + public static WbsNodeDto from(WbsNode node) { + return WbsNodeDto.builder() + .id(node.getId()) + .wbsCode(node.getWbsCode()) + .name(node.getName()) + .level(node.getLevel()) + .parentCode(node.getParent() != null ? node.getParent().getWbsCode() : null) + .discipline(node.getDiscipline()) + .plannedHours(node.getPlannedHours()) + .isLeaf(node.getIsLeaf()) + .build(); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/WbsParseResult.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/WbsParseResult.java new file mode 100644 index 0000000..559dd49 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/WbsParseResult.java @@ -0,0 +1,31 @@ +package kr.co.accura.wtm.domain.wbs.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +public class WbsParseResult { + + private final List nodes = new ArrayList<>(); + private final List errors = new ArrayList<>(); + + public void addNode(WbsNodeDto node) { + nodes.add(node); + } + + public void addError(String error) { + errors.add(error); + } + + public boolean hasErrors() { + return !errors.isEmpty(); + } + + public void setError(String error) { + errors.add(error); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/WbsVersionDto.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/WbsVersionDto.java new file mode 100644 index 0000000..4aa0638 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/dto/WbsVersionDto.java @@ -0,0 +1,36 @@ +package kr.co.accura.wtm.domain.wbs.dto; + +import kr.co.accura.wtm.domain.wbs.entity.WbsVersion; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +public record WbsVersionDto( + Long id, + Long projectId, + Integer versionNumber, + LocalDate effectiveDate, + String sourceType, + String sourceFilename, + String description, + String status, + Long uploadedBy, + LocalDateTime createdAt, + int nodeCount +) { + public static WbsVersionDto from(WbsVersion version, int nodeCount) { + return new WbsVersionDto( + version.getId(), + version.getProjectId(), + version.getVersionNumber(), + version.getEffectiveDate(), + version.getSourceType(), + version.getSourceFilename(), + version.getDescription(), + version.getStatus(), + version.getUploadedBy(), + version.getCreatedAt(), + nodeCount + ); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/entity/CanonicalWbs.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/entity/CanonicalWbs.java new file mode 100644 index 0000000..3fad6f6 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/entity/CanonicalWbs.java @@ -0,0 +1,68 @@ +package kr.co.accura.wtm.domain.wbs.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "canonical_wbs", + uniqueConstraints = @UniqueConstraint(columnNames = {"project_id", "wbs_code"})) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CanonicalWbs { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "project_id", nullable = false) + private Long projectId; + + @Column(name = "wbs_code", nullable = false, length = 100) + private String wbsCode; + + @Column(nullable = false) + private Integer level; + + @Column(nullable = false, length = 500) + private String name; + + @Column(name = "parent_code", length = 100) + private String parentCode; + + @Column(length = 50) + private String discipline; + + @Column(name = "is_active") + @Builder.Default + private Boolean isActive = true; + + @Column(name = "mapped_p6_code", length = 100) + private String mappedP6Code; + + @Column(name = "created_at") + @Builder.Default + private LocalDateTime createdAt = LocalDateTime.now(); + + public static CanonicalWbs fromWbsNode(Long projectId, WbsNode node) { + return CanonicalWbs.builder() + .projectId(projectId) + .wbsCode(node.getWbsCode()) + .level(node.getLevel()) + .name(node.getName()) + .parentCode(node.getParent() != null ? node.getParent().getWbsCode() : null) + .discipline(node.getDiscipline()) + .mappedP6Code(node.getWbsCode()) + .build(); + } + + public void updateFrom(WbsNode node) { + this.name = node.getName(); + this.level = node.getLevel(); + this.discipline = node.getDiscipline(); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/entity/WbsDisciplineAssignment.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/entity/WbsDisciplineAssignment.java new file mode 100644 index 0000000..dfc4925 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/entity/WbsDisciplineAssignment.java @@ -0,0 +1,29 @@ +package kr.co.accura.wtm.domain.wbs.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "wbs_discipline_assignments") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WbsDisciplineAssignment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "canonical_wbs_id", nullable = false) + private CanonicalWbs canonicalWbs; + + @Column(length = 50) + private String discipline; + + @Column(name = "is_active") + @Builder.Default + private Boolean isActive = true; +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/entity/WbsNode.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/entity/WbsNode.java new file mode 100644 index 0000000..4315de7 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/entity/WbsNode.java @@ -0,0 +1,64 @@ +package kr.co.accura.wtm.domain.wbs.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.math.BigDecimal; + +@Entity +@Table(name = "wbs_nodes", + uniqueConstraints = @UniqueConstraint(columnNames = {"wbs_version_id", "wbs_code"})) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WbsNode { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "wbs_version_id", nullable = false) + private WbsVersion wbsVersion; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private WbsNode parent; + + @Column(name = "wbs_code", nullable = false, length = 100) + private String wbsCode; + + @Column(nullable = false) + private Integer level; + + @Column(nullable = false, length = 500) + private String name; + + @Column(length = 50) + private String discipline; + + @Column(name = "planned_hours", precision = 10, scale = 2) + private BigDecimal plannedHours; + + @Column(name = "sort_order") + @Builder.Default + private Integer sortOrder = 0; + + @Column(name = "is_leaf") + @Builder.Default + private Boolean isLeaf = false; + + /** + * Content equality check for version comparison. + */ + public boolean contentEquals(WbsNode other) { + if (other == null) return false; + return java.util.Objects.equals(this.name, other.name) + && java.util.Objects.equals(this.level, other.level) + && java.util.Objects.equals(this.discipline, other.discipline) + && (this.plannedHours == null ? other.plannedHours == null + : this.plannedHours.compareTo(other.plannedHours) == 0); + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/entity/WbsVersion.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/entity/WbsVersion.java new file mode 100644 index 0000000..02b6257 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/entity/WbsVersion.java @@ -0,0 +1,66 @@ +package kr.co.accura.wtm.domain.wbs.entity; + +import jakarta.persistence.*; +import kr.co.accura.wtm.domain.project.entity.Project; +import lombok.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Entity +@Table(name = "wbs_versions", + uniqueConstraints = @UniqueConstraint(columnNames = {"project_id", "version_number"})) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WbsVersion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "project_id", nullable = false) + private Project project; + + @Column(name = "version_number", nullable = false) + private Integer versionNumber; + + @Column(name = "effective_date", nullable = false) + private LocalDate effectiveDate; + + @Column(name = "source_type", length = 20) + @Builder.Default + private String sourceType = "P6_UPLOAD"; + + @Column(name = "source_filename", length = 500) + private String sourceFilename; + + @Column(length = 500) + private String description; + + @Column(length = 20) + @Builder.Default + private String status = "DRAFT"; + + @Column(name = "uploaded_by") + private Long uploadedBy; + + @Column(name = "created_at") + @Builder.Default + private LocalDateTime createdAt = LocalDateTime.now(); + + public void activate() { + this.status = "ACTIVE"; + } + + public void archive() { + this.status = "ARCHIVED"; + } + + public Long getProjectId() { + return project != null ? project.getId() : null; + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/parser/P6WbsParser.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/parser/P6WbsParser.java new file mode 100644 index 0000000..1e4431e --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/parser/P6WbsParser.java @@ -0,0 +1,46 @@ +package kr.co.accura.wtm.domain.wbs.parser; + +import kr.co.accura.wtm.domain.wbs.dto.WbsNodeDto; +import kr.co.accura.wtm.domain.wbs.dto.WbsParseResult; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.math.BigDecimal; + +/** + * P6 WBS file parser stub. + * Parses P6 Export files (.xls/.csv) into WBS tree structure. + * Full implementation will use Apache POI for Excel parsing. + */ +@Service +public class P6WbsParser { + + /** + * Parse a P6 Export file into WBS nodes. + * Expected P6 columns: + * Activity ID | Activity Name | WBS Code | WBS Name | Level | Planned Hours + */ + public WbsParseResult parse(MultipartFile file) { + var result = new WbsParseResult(); + + try { + // TODO: Implement Apache POI-based parsing + // Stub: return empty result for now + result.setError("P6 WBS parser not yet implemented. Upload a file to test."); + } catch (Exception e) { + result.setError("File parsing failed: " + e.getMessage()); + } + + return result; + } + + /** + * Derive parent WBS code from a given WBS code. + * Example: "E.01.03.02.01" (Level 5) -> "E.01.03.02" (Level 4) + */ + public String deriveParentCode(String wbsCode, int level) { + if (level <= 1) return null; + int lastDot = wbsCode.lastIndexOf('.'); + return lastDot > 0 ? wbsCode.substring(0, lastDot) : null; + } +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/repository/CanonicalWbsRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/repository/CanonicalWbsRepository.java new file mode 100644 index 0000000..c46ebd9 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/repository/CanonicalWbsRepository.java @@ -0,0 +1,22 @@ +package kr.co.accura.wtm.domain.wbs.repository; + +import kr.co.accura.wtm.domain.wbs.entity.CanonicalWbs; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +@Repository +public interface CanonicalWbsRepository extends JpaRepository { + + List findByProjectIdAndIsActiveTrue(Long projectId); + + Optional findByProjectIdAndWbsCode(Long projectId, String wbsCode); + + @Query("SELECT c.wbsCode FROM CanonicalWbs c WHERE c.projectId = :projectId AND c.isActive = true") + Set findActiveCodesByProjectId(@Param("projectId") Long projectId); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/repository/WbsDisciplineAssignmentRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/repository/WbsDisciplineAssignmentRepository.java new file mode 100644 index 0000000..c7ddfc5 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/repository/WbsDisciplineAssignmentRepository.java @@ -0,0 +1,15 @@ +package kr.co.accura.wtm.domain.wbs.repository; + +import kr.co.accura.wtm.domain.wbs.entity.WbsDisciplineAssignment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface WbsDisciplineAssignmentRepository extends JpaRepository { + + List findByCanonicalWbs_Id(Long canonicalWbsId); + + List findByCanonicalWbs_IdIn(List canonicalWbsIds); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/repository/WbsNodeRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/repository/WbsNodeRepository.java new file mode 100644 index 0000000..12e719e --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/repository/WbsNodeRepository.java @@ -0,0 +1,20 @@ +package kr.co.accura.wtm.domain.wbs.repository; + +import kr.co.accura.wtm.domain.wbs.entity.WbsNode; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface WbsNodeRepository extends JpaRepository { + + List findByWbsVersion_Id(Long wbsVersionId); + + @Query("SELECT n FROM WbsNode n WHERE n.wbsVersion.project.id = :projectId " + + "AND n.wbsVersion.versionNumber = :versionNumber ORDER BY n.sortOrder") + List findByProjectIdAndVersion(@Param("projectId") Long projectId, + @Param("versionNumber") int versionNumber); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/repository/WbsVersionRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/repository/WbsVersionRepository.java new file mode 100644 index 0000000..ca64d74 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/repository/WbsVersionRepository.java @@ -0,0 +1,26 @@ +package kr.co.accura.wtm.domain.wbs.repository; + +import kr.co.accura.wtm.domain.wbs.entity.WbsVersion; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface WbsVersionRepository extends JpaRepository { + + List findByProject_IdOrderByVersionNumberDesc(Long projectId); + + @Query("SELECT MAX(v.versionNumber) FROM WbsVersion v WHERE v.project.id = :projectId") + Optional findMaxVersionByProjectId(@Param("projectId") Long projectId); + + @Modifying + @Query("UPDATE WbsVersion v SET v.status = 'ARCHIVED' WHERE v.project.id = :projectId AND v.status = 'ACTIVE'") + void archiveActiveVersions(@Param("projectId") Long projectId); + + Optional findByProject_IdAndVersionNumber(Long projectId, Integer versionNumber); +} diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/service/WbsService.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/service/WbsService.java new file mode 100644 index 0000000..5d6e1f6 --- /dev/null +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/wbs/service/WbsService.java @@ -0,0 +1,230 @@ +package kr.co.accura.wtm.domain.wbs.service; + +import kr.co.accura.wbx.spring.common.BusinessException; +import kr.co.accura.wbx.spring.common.NotFoundException; +import kr.co.accura.wtm.domain.project.entity.Project; +import kr.co.accura.wtm.domain.project.repository.ProjectRepository; +import kr.co.accura.wtm.domain.wbs.dto.*; +import kr.co.accura.wtm.domain.wbs.entity.CanonicalWbs; +import kr.co.accura.wtm.domain.wbs.entity.WbsDisciplineAssignment; +import kr.co.accura.wtm.domain.wbs.entity.WbsNode; +import kr.co.accura.wtm.domain.wbs.entity.WbsVersion; +import kr.co.accura.wtm.domain.wbs.parser.P6WbsParser; +import kr.co.accura.wtm.domain.wbs.repository.CanonicalWbsRepository; +import kr.co.accura.wtm.domain.wbs.repository.WbsDisciplineAssignmentRepository; +import kr.co.accura.wtm.domain.wbs.repository.WbsNodeRepository; +import kr.co.accura.wtm.domain.wbs.repository.WbsVersionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +@Transactional +@RequiredArgsConstructor +public class WbsService { + + private final WbsVersionRepository wbsVersionRepository; + private final WbsNodeRepository wbsNodeRepository; + private final CanonicalWbsRepository canonicalWbsRepository; + private final WbsDisciplineAssignmentRepository wbsDisciplineAssignmentRepository; + private final ProjectRepository projectRepository; + private final P6WbsParser p6Parser; + + /** + * Upload P6 WBS file and create a new version. + */ + public WbsVersionDto uploadP6Wbs(Long projectId, MultipartFile file, + LocalDate effectiveDate, String description) { + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> new NotFoundException("Project not found: " + projectId)); + + // 1. Parse file + WbsParseResult parseResult = p6Parser.parse(file); + if (parseResult.hasErrors()) { + throw new BusinessException("WBS parsing error: " + String.join(", ", parseResult.getErrors())); + } + + // 2. Auto-increment version number + int nextVersion = wbsVersionRepository + .findMaxVersionByProjectId(projectId) + .map(v -> v + 1).orElse(1); + + // 3. Save WBS version + WbsVersion version = WbsVersion.builder() + .project(project) + .versionNumber(nextVersion) + .effectiveDate(effectiveDate) + .sourceType("P6_UPLOAD") + .sourceFilename(file.getOriginalFilename()) + .description(description) + .status("DRAFT") + .build(); + wbsVersionRepository.save(version); + + // 4. Bulk save WBS nodes + List nodes = parseResult.getNodes().stream() + .map(dto -> WbsNode.builder() + .wbsVersion(version) + .wbsCode(dto.getWbsCode()) + .level(dto.getLevel()) + .name(dto.getName()) + .discipline(dto.getDiscipline()) + .plannedHours(dto.getPlannedHours()) + .isLeaf(dto.getLevel() != null && dto.getLevel() == 5) + .build()) + .toList(); + wbsNodeRepository.saveAll(nodes); + + return WbsVersionDto.from(version, nodes.size()); + } + + /** + * Activate a WBS version (DRAFT -> ACTIVE). + * Archives any existing ACTIVE version. + */ + public void activateVersion(Long versionId) { + WbsVersion version = wbsVersionRepository.findById(versionId) + .orElseThrow(() -> new NotFoundException("WBS version not found: " + versionId)); + + // Archive existing ACTIVE versions + wbsVersionRepository.archiveActiveVersions(version.getProjectId()); + + version.activate(); + wbsVersionRepository.save(version); + + // Sync canonical WBS + syncCanonicalWbs(version); + } + + /** + * Get WBS versions for a project. + */ + @Transactional(readOnly = true) + public List getVersions(Long projectId) { + return wbsVersionRepository.findByProject_IdOrderByVersionNumberDesc(projectId).stream() + .map(v -> { + int count = wbsNodeRepository.findByWbsVersion_Id(v.getId()).size(); + return WbsVersionDto.from(v, count); + }) + .toList(); + } + + /** + * Get WBS nodes for a specific version. + */ + @Transactional(readOnly = true) + public List getVersionNodes(Long projectId, Integer versionNumber) { + WbsVersion version = wbsVersionRepository.findByProject_IdAndVersionNumber(projectId, versionNumber) + .orElseThrow(() -> new NotFoundException("WBS version not found")); + return wbsNodeRepository.findByWbsVersion_Id(version.getId()).stream() + .map(WbsNodeDto::from) + .toList(); + } + + /** + * Get canonical WBS for a project. + */ + @Transactional(readOnly = true) + public List getCanonicalWbs(Long projectId) { + return canonicalWbsRepository.findByProjectIdAndIsActiveTrue(projectId).stream() + .map(CanonicalWbsDto::from) + .toList(); + } + + /** + * Compare two WBS versions. + */ + @Transactional(readOnly = true) + public WbsCompareResult compareVersions(Long projectId, int versionA, int versionB) { + List nodesA = wbsNodeRepository.findByProjectIdAndVersion(projectId, versionA); + List nodesB = wbsNodeRepository.findByProjectIdAndVersion(projectId, versionB); + + Map mapA = nodesA.stream() + .collect(Collectors.toMap(WbsNode::getWbsCode, Function.identity())); + Map mapB = nodesB.stream() + .collect(Collectors.toMap(WbsNode::getWbsCode, Function.identity())); + + var result = new WbsCompareResult(); + + // Added in B + mapB.keySet().stream() + .filter(code -> !mapA.containsKey(code)) + .forEach(code -> result.addAdded(mapB.get(code))); + + // Removed from A + mapA.keySet().stream() + .filter(code -> !mapB.containsKey(code)) + .forEach(code -> result.addRemoved(mapA.get(code))); + + // Modified + mapA.keySet().stream() + .filter(mapB::containsKey) + .filter(code -> !mapA.get(code).contentEquals(mapB.get(code))) + .forEach(code -> result.addModified(mapA.get(code), mapB.get(code))); + + return result; + } + + /** + * Get WBS-Discipline assignments for a project. + */ + @Transactional(readOnly = true) + public List getWbsDisciplines(Long projectId) { + List canonicalWbsList = canonicalWbsRepository.findByProjectIdAndIsActiveTrue(projectId); + List canonicalWbsIds = canonicalWbsList.stream().map(CanonicalWbs::getId).toList(); + return wbsDisciplineAssignmentRepository.findByCanonicalWbs_IdIn(canonicalWbsIds).stream() + .map(WbsDisciplineAssignmentDto::from) + .toList(); + } + + /** + * Save WBS-Discipline assignments for a project. + */ + public List saveWbsDisciplines(Long projectId, + List assignments) { + projectRepository.findById(projectId) + .orElseThrow(() -> new NotFoundException("Project not found: " + projectId)); + + List entities = assignments.stream().map(dto -> { + CanonicalWbs cw = canonicalWbsRepository.findById(dto.getCanonicalWbsId()) + .orElseThrow(() -> new NotFoundException("Canonical WBS not found: " + dto.getCanonicalWbsId())); + return WbsDisciplineAssignment.builder() + .id(dto.getId()) + .canonicalWbs(cw) + .discipline(dto.getDiscipline()) + .isActive(dto.getIsActive() != null ? dto.getIsActive() : true) + .build(); + }).toList(); + + return wbsDisciplineAssignmentRepository.saveAll(entities).stream() + .map(WbsDisciplineAssignmentDto::from) + .toList(); + } + + /** + * Sync canonical WBS from active WBS version. + */ + private void syncCanonicalWbs(WbsVersion version) { + List nodes = wbsNodeRepository.findByWbsVersion_Id(version.getId()); + + for (WbsNode node : nodes) { + canonicalWbsRepository.findByProjectIdAndWbsCode( + version.getProjectId(), node.getWbsCode() + ).ifPresentOrElse( + existing -> { + existing.updateFrom(node); + existing.setMappedP6Code(node.getWbsCode()); + }, + () -> canonicalWbsRepository.save( + CanonicalWbs.fromWbsNode(version.getProjectId(), node)) + ); + } + } +} diff --git a/wtm-api/src/main/resources/application.yml b/wtm-api/src/main/resources/application.yml new file mode 100644 index 0000000..5e0a9fb --- /dev/null +++ b/wtm-api/src/main/resources/application.yml @@ -0,0 +1,62 @@ +spring: + application: + name: wtm-api + + datasource: + url: jdbc:mysql://${DB_HOST:ws.ubuilder.co.kr}:${DB_PORT:3306}/${DB_NAME:mos}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul + username: ${DB_USER:jsh} + password: ${DB_PASS:jsh@} + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: update # 개발 단계: Flyway + Hibernate 보완 + open-in-view: false + + flyway: + enabled: true + locations: classpath:db/migration/mysql + baseline-on-migrate: true + baseline-version: 0 + + data: + redis: + host: ${REDIS_HOST:localhost} + port: 6379 + +server: + port: ${SERVER_PORT:8081} + +wbx: + spring: + api-prefix: /api/wtm + jwt: + secret: ${JWT_SECRET:dev-secret-key-minimum-256-bits-long-for-wtm-project} + expiration: 28800 + admin-ui: + enabled: false # WTM은 자체 Admin 불필요 + cors: + allowed-origins: ${CORS_ORIGINS:http://localhost:5173} + notification: + sse-enabled: true + +wtm: + work-rules: + default-min-daily-hours: 8 + default-max-weekly-hours: 52 + +management: + endpoints: + web: + exposure: + include: health,info,metrics + endpoint: + health: + show-details: when-authorized + +springdoc: + api-docs: + path: /api-docs + swagger-ui: + path: /swagger-ui + packages-to-scan: kr.co.accura.wtm,kr.co.accura.wbx.spring diff --git a/wtm-api/src/main/resources/db/migration/mysql/V100__init_users.sql b/wtm-api/src/main/resources/db/migration/mysql/V100__init_users.sql new file mode 100644 index 0000000..4ef07f4 --- /dev/null +++ b/wtm-api/src/main/resources/db/migration/mysql/V100__init_users.sql @@ -0,0 +1,56 @@ +-- V1__init_users.sql (MySQL) +-- Converted from MSSQL DDL in 02-database-schema.md + +CREATE TABLE users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + employee_id VARCHAR(50) NOT NULL UNIQUE, + email VARCHAR(255) NOT NULL UNIQUE, + username VARCHAR(100) NOT NULL, + full_name VARCHAR(255), + hashed_password VARCHAR(255), + department VARCHAR(100), + discipline VARCHAR(100), + position_title VARCHAR(100), + location VARCHAR(50), + employment_type VARCHAR(20) DEFAULT 'INTERNAL', + is_active TINYINT(1) DEFAULT 1, + is_locked TINYINT(1) DEFAULT 0, + failed_attempts INT DEFAULT 0, + last_login_at DATETIME, + password_changed_at DATETIME, + azure_oid VARCHAR(255), + mfa_enabled TINYINT(1) DEFAULT 0, + created_at DATETIME DEFAULT NOW(), + updated_at DATETIME, + created_by BIGINT, + updated_by BIGINT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE roles ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + code VARCHAR(20) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + description VARCHAR(500), + level INT DEFAULT 0 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE user_roles ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + role_id BIGINT NOT NULL, + project_id BIGINT NULL, + granted_at DATETIME DEFAULT NOW(), + granted_by BIGINT, + UNIQUE (user_id, role_id, project_id), + CONSTRAINT fk_user_roles_user FOREIGN KEY (user_id) REFERENCES users(id), + CONSTRAINT fk_user_roles_role FOREIGN KEY (role_id) REFERENCES roles(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Seed roles +INSERT INTO roles (code, name, level) VALUES + ('SA', 'System Administrator', 100), + ('PM', 'Project Manager', 80), + ('PCM', 'Project Control Manager', 70), + ('PTK', 'Project Timekeeper', 60), + ('DL', 'Discipline Lead', 50), + ('USER', 'General User', 10); diff --git a/wtm-api/src/main/resources/db/migration/mysql/V101__init_projects_wbs.sql b/wtm-api/src/main/resources/db/migration/mysql/V101__init_projects_wbs.sql new file mode 100644 index 0000000..933626d --- /dev/null +++ b/wtm-api/src/main/resources/db/migration/mysql/V101__init_projects_wbs.sql @@ -0,0 +1,68 @@ +-- V2__init_projects_wbs.sql (MySQL) +-- Projects, WBS versions, WBS nodes, Canonical WBS + +CREATE TABLE projects ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + project_code VARCHAR(50) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + description VARCHAR(1000) CHARACTER SET utf8mb4, + project_type VARCHAR(20) NOT NULL, + status VARCHAR(20) DEFAULT 'ACTIVE', + start_date DATE, + end_date DATE, + pm_user_id BIGINT, + created_at DATETIME DEFAULT NOW(), + updated_at DATETIME, + CONSTRAINT fk_projects_pm FOREIGN KEY (pm_user_id) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- user_roles FK to projects (deferred from V1 because projects did not exist yet) +ALTER TABLE user_roles + ADD CONSTRAINT fk_user_roles_project FOREIGN KEY (project_id) REFERENCES projects(id); + +CREATE TABLE wbs_versions ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + project_id BIGINT NOT NULL, + version_number INT NOT NULL, + effective_date DATE NOT NULL, + source_type VARCHAR(20) DEFAULT 'P6_UPLOAD', + source_filename VARCHAR(500), + description VARCHAR(500) CHARACTER SET utf8mb4, + status VARCHAR(20) DEFAULT 'DRAFT', + uploaded_by BIGINT, + created_at DATETIME DEFAULT NOW(), + UNIQUE (project_id, version_number), + CONSTRAINT fk_wbs_ver_project FOREIGN KEY (project_id) REFERENCES projects(id), + CONSTRAINT fk_wbs_ver_uploader FOREIGN KEY (uploaded_by) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE wbs_nodes ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + wbs_version_id BIGINT NOT NULL, + parent_id BIGINT NULL, + wbs_code VARCHAR(100) NOT NULL, + level INT NOT NULL, + name VARCHAR(500) CHARACTER SET utf8mb4 NOT NULL, + discipline VARCHAR(50), + planned_hours DECIMAL(10,2), + sort_order INT DEFAULT 0, + is_leaf TINYINT(1) DEFAULT 0, + UNIQUE (wbs_version_id, wbs_code), + CONSTRAINT fk_wbs_node_version FOREIGN KEY (wbs_version_id) REFERENCES wbs_versions(id), + CONSTRAINT fk_wbs_node_parent FOREIGN KEY (parent_id) REFERENCES wbs_nodes(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE canonical_wbs ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + project_id BIGINT NOT NULL, + wbs_code VARCHAR(100) NOT NULL, + level INT NOT NULL, + name VARCHAR(500) CHARACTER SET utf8mb4 NOT NULL, + parent_code VARCHAR(100), + discipline VARCHAR(50), + is_active TINYINT(1) DEFAULT 1, + mapped_p6_code VARCHAR(100), + created_at DATETIME DEFAULT NOW(), + UNIQUE (project_id, wbs_code), + CONSTRAINT fk_canonical_wbs_project FOREIGN KEY (project_id) REFERENCES projects(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/wtm-api/src/main/resources/db/migration/mysql/V102__init_teal.sql b/wtm-api/src/main/resources/db/migration/mysql/V102__init_teal.sql new file mode 100644 index 0000000..f2000c8 --- /dev/null +++ b/wtm-api/src/main/resources/db/migration/mysql/V102__init_teal.sql @@ -0,0 +1,28 @@ +-- V3__init_teal.sql (MySQL) +-- TEAL versions and entries + +CREATE TABLE teal_versions ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + project_id BIGINT NOT NULL, + version_number INT NOT NULL, + effective_date DATE NOT NULL, + description VARCHAR(500) CHARACTER SET utf8mb4, + status VARCHAR(20) DEFAULT 'DRAFT', + uploaded_by BIGINT, + created_at DATETIME DEFAULT NOW(), + UNIQUE (project_id, version_number), + CONSTRAINT fk_teal_ver_project FOREIGN KEY (project_id) REFERENCES projects(id), + CONSTRAINT fk_teal_ver_uploader FOREIGN KEY (uploaded_by) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE teal_entries ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + teal_version_id BIGINT NOT NULL, + canonical_wbs_id BIGINT, + activity_code VARCHAR(100) NOT NULL, + activity_name VARCHAR(500) CHARACTER SET utf8mb4, + discipline VARCHAR(50), + is_active TINYINT(1) DEFAULT 1, + CONSTRAINT fk_teal_entry_version FOREIGN KEY (teal_version_id) REFERENCES teal_versions(id), + CONSTRAINT fk_teal_entry_cwbs FOREIGN KEY (canonical_wbs_id) REFERENCES canonical_wbs(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/wtm-api/src/main/resources/db/migration/mysql/V103__init_timesheets.sql b/wtm-api/src/main/resources/db/migration/mysql/V103__init_timesheets.sql new file mode 100644 index 0000000..dd71cce --- /dev/null +++ b/wtm-api/src/main/resources/db/migration/mysql/V103__init_timesheets.sql @@ -0,0 +1,57 @@ +-- V4__init_timesheets.sql (MySQL) +-- Timesheets, timesheet entries, timesheet uploads + +CREATE TABLE timesheets ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + week_start_date DATE NOT NULL, + week_end_date DATE NOT NULL, + status VARCHAR(20) DEFAULT 'DRAFT', + total_hours DECIMAL(10,2) DEFAULT 0, + submitted_at DATETIME, + created_at DATETIME DEFAULT NOW(), + updated_at DATETIME, + UNIQUE (user_id, week_start_date), + CONSTRAINT fk_ts_user FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE timesheet_entries ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + timesheet_id BIGINT NOT NULL, + entry_type VARCHAR(20) NOT NULL, + entry_date DATE NOT NULL, + hours DECIMAL(5,2) NOT NULL DEFAULT 0, + np_category VARCHAR(100), + other_project_id BIGINT NULL, + other_category VARCHAR(100), + epc_project_id BIGINT NULL, + canonical_wbs_id BIGINT NULL, + teal_entry_id BIGINT NULL, + revision_number INT DEFAULT 1, + remark VARCHAR(500) CHARACTER SET utf8mb4, + created_at DATETIME DEFAULT NOW(), + updated_at DATETIME, + CONSTRAINT chk_hours CHECK (hours >= 0 AND hours <= 24), + CONSTRAINT fk_tse_timesheet FOREIGN KEY (timesheet_id) REFERENCES timesheets(id), + CONSTRAINT fk_tse_other_proj FOREIGN KEY (other_project_id) REFERENCES projects(id), + CONSTRAINT fk_tse_epc_proj FOREIGN KEY (epc_project_id) REFERENCES projects(id), + CONSTRAINT fk_tse_cwbs FOREIGN KEY (canonical_wbs_id) REFERENCES canonical_wbs(id), + CONSTRAINT fk_tse_teal FOREIGN KEY (teal_entry_id) REFERENCES teal_entries(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE INDEX idx_ts_entries_date ON timesheet_entries(entry_date); +CREATE INDEX idx_ts_entries_type ON timesheet_entries(entry_type); + +CREATE TABLE timesheet_uploads ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + filename VARCHAR(500), + file_path VARCHAR(1000), + total_rows INT, + success_rows INT, + error_rows INT, + error_log TEXT CHARACTER SET utf8mb4, + status VARCHAR(20), + created_at DATETIME DEFAULT NOW(), + CONSTRAINT fk_tsu_user FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/wtm-api/src/main/resources/db/migration/mysql/V104__init_approvals.sql b/wtm-api/src/main/resources/db/migration/mysql/V104__init_approvals.sql new file mode 100644 index 0000000..b6fe453 --- /dev/null +++ b/wtm-api/src/main/resources/db/migration/mysql/V104__init_approvals.sql @@ -0,0 +1,40 @@ +-- V5__init_approvals.sql (MySQL) +-- Approvals, approval lines, approval comments + +CREATE TABLE approvals ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + timesheet_id BIGINT NOT NULL, + requester_id BIGINT NOT NULL, + project_id BIGINT, + status VARCHAR(20) DEFAULT 'PENDING', + submitted_at DATETIME DEFAULT NOW(), + completed_at DATETIME, + UNIQUE (timesheet_id), + CONSTRAINT fk_appr_timesheet FOREIGN KEY (timesheet_id) REFERENCES timesheets(id), + CONSTRAINT fk_appr_requester FOREIGN KEY (requester_id) REFERENCES users(id), + CONSTRAINT fk_appr_project FOREIGN KEY (project_id) REFERENCES projects(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE approval_lines ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + approval_id BIGINT NOT NULL, + approver_id BIGINT NOT NULL, + approval_order INT NOT NULL, + role_code VARCHAR(20), + status VARCHAR(20) DEFAULT 'PENDING', + acted_at DATETIME, + created_at DATETIME DEFAULT NOW(), + CONSTRAINT fk_aline_approval FOREIGN KEY (approval_id) REFERENCES approvals(id), + CONSTRAINT fk_aline_approver FOREIGN KEY (approver_id) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE approval_comments ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + approval_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + comment VARCHAR(2000) CHARACTER SET utf8mb4, + action VARCHAR(20), + created_at DATETIME DEFAULT NOW(), + CONSTRAINT fk_acomm_approval FOREIGN KEY (approval_id) REFERENCES approvals(id), + CONSTRAINT fk_acomm_user FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/wtm-api/src/main/resources/db/migration/mysql/V105__init_report_views.sql b/wtm-api/src/main/resources/db/migration/mysql/V105__init_report_views.sql new file mode 100644 index 0000000..8650b51 --- /dev/null +++ b/wtm-api/src/main/resources/db/migration/mysql/V105__init_report_views.sql @@ -0,0 +1,41 @@ +-- V6__init_report_views.sql (MySQL) +-- Report views with MySQL date functions + +CREATE OR REPLACE VIEW v_project_hours AS +SELECT + te.epc_project_id AS project_id, + p.project_code, + p.name AS project_name, + te.entry_date, + YEAR(te.entry_date) AS `year`, + MONTH(te.entry_date) AS `month`, + WEEK(te.entry_date) AS `week`, + u.discipline, + u.department, + te.entry_type, + SUM(te.hours) AS total_hours, + COUNT(DISTINCT te.timesheet_id) AS timesheet_count +FROM timesheet_entries te +JOIN timesheets ts ON te.timesheet_id = ts.id +JOIN users u ON ts.user_id = u.id +LEFT JOIN projects p ON te.epc_project_id = p.id +WHERE ts.status = 'APPROVED' +GROUP BY te.epc_project_id, p.project_code, p.name, + te.entry_date, YEAR(te.entry_date), + MONTH(te.entry_date), WEEK(te.entry_date), + u.discipline, u.department, te.entry_type; + +CREATE OR REPLACE VIEW v_wbs_hours AS +SELECT + cw.project_id, + cw.wbs_code, + cw.level AS wbs_level, + cw.name AS wbs_name, + cw.discipline, + SUM(te.hours) AS total_hours, + COUNT(DISTINCT ts.user_id) AS user_count +FROM timesheet_entries te +JOIN timesheets ts ON te.timesheet_id = ts.id +JOIN canonical_wbs cw ON te.canonical_wbs_id = cw.id +WHERE ts.status = 'APPROVED' +GROUP BY cw.project_id, cw.wbs_code, cw.level, cw.name, cw.discipline; diff --git a/wtm-api/src/main/resources/db/migration/mysql/V106__init_config_audit.sql b/wtm-api/src/main/resources/db/migration/mysql/V106__init_config_audit.sql new file mode 100644 index 0000000..654fa8a --- /dev/null +++ b/wtm-api/src/main/resources/db/migration/mysql/V106__init_config_audit.sql @@ -0,0 +1,53 @@ +-- V7__init_config_audit.sql (MySQL) +-- Overhead types, work rules, SA access logs + seed overhead types + +CREATE TABLE overhead_types ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + code VARCHAR(50) NOT NULL UNIQUE, + name VARCHAR(200) NOT NULL, + category VARCHAR(50), + is_active TINYINT(1) DEFAULT 1, + sort_order INT DEFAULT 0, + created_at DATETIME DEFAULT NOW(), + updated_at DATETIME +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE work_rules ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + location VARCHAR(50), + min_daily_hours DECIMAL(4,2) DEFAULT 8.00, + max_daily_hours DECIMAL(4,2) DEFAULT 12.00, + max_weekly_hours DECIMAL(5,2) DEFAULT 52.00, + is_active TINYINT(1) DEFAULT 1, + created_at DATETIME DEFAULT NOW(), + updated_at DATETIME +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE sa_access_logs ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + action VARCHAR(50) NOT NULL, + resource VARCHAR(100), + resource_id BIGINT, + ip_address VARCHAR(50), + user_agent VARCHAR(500), + detail VARCHAR(2000) CHARACTER SET utf8mb4, + created_at DATETIME DEFAULT NOW(), + CONSTRAINT fk_sa_log_user FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE INDEX idx_sa_log_user ON sa_access_logs(user_id, created_at); +CREATE INDEX idx_sa_log_action ON sa_access_logs(action, created_at); + +-- Seed overhead types (Non-Project categories) +INSERT INTO overhead_types (code, name, category, sort_order) VALUES + ('LEAVE', 'Annual Leave', 'LEAVE', 1), + ('SICK', 'Sick Leave', 'LEAVE', 2), + ('HOLIDAY', 'Public Holiday', 'LEAVE', 3), + ('TRAINING', 'Training', 'ADMIN', 10), + ('MEETING', 'General Meeting', 'ADMIN', 11), + ('ADMIN', 'Administrative Work', 'ADMIN', 12), + ('TRAVEL', 'Business Travel', 'TRAVEL', 20), + ('SAFETY', 'Safety Training', 'ADMIN', 13), + ('IT_SUPPORT', 'IT Support', 'ADMIN', 14), + ('OTHER', 'Other Overhead', 'OTHER', 99); diff --git a/wtm-api/src/main/resources/db/migration/mysql/V107__init_missing_tables.sql b/wtm-api/src/main/resources/db/migration/mysql/V107__init_missing_tables.sql new file mode 100644 index 0000000..f6052e3 --- /dev/null +++ b/wtm-api/src/main/resources/db/migration/mysql/V107__init_missing_tables.sql @@ -0,0 +1,68 @@ +-- V107__init_missing_tables.sql (MySQL) +-- Missing tables: project_assignments, wbs_discipline_assignments, org_hierarchy, hr_uploads, project_type_config + +CREATE TABLE IF NOT EXISTS project_assignments ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + project_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + role VARCHAR(50), + assigned_at DATETIME DEFAULT NOW(), + assigned_by BIGINT, + created_at DATETIME DEFAULT NOW(), + updated_at DATETIME, + created_by BIGINT, + updated_by BIGINT, + UNIQUE (project_id, user_id), + CONSTRAINT fk_proj_assign_project FOREIGN KEY (project_id) REFERENCES projects(id), + CONSTRAINT fk_proj_assign_user FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS wbs_discipline_assignments ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + canonical_wbs_id BIGINT NOT NULL, + discipline VARCHAR(100) NOT NULL, + is_active TINYINT(1) DEFAULT 1, + created_at DATETIME DEFAULT NOW(), + updated_at DATETIME, + CONSTRAINT fk_wbs_disc_canonical FOREIGN KEY (canonical_wbs_id) REFERENCES canonical_wbs(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS org_hierarchy ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + code VARCHAR(50), + level INT NOT NULL, + parent_id BIGINT, + sort_order INT DEFAULT 0, + is_active TINYINT(1) DEFAULT 1, + created_at DATETIME DEFAULT NOW(), + updated_at DATETIME, + CONSTRAINT fk_org_hierarchy_parent FOREIGN KEY (parent_id) REFERENCES org_hierarchy(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS hr_uploads ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + filename VARCHAR(500), + file_path VARCHAR(1000), + total_rows INT, + success_rows INT, + error_rows INT, + error_log TEXT CHARACTER SET utf8mb4, + status VARCHAR(20), + sync_source VARCHAR(50), + created_at DATETIME DEFAULT NOW(), + CONSTRAINT fk_hr_uploads_user FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS project_type_config ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + project_type VARCHAR(20) NOT NULL, + config_key VARCHAR(100) NOT NULL, + config_value VARCHAR(500), + description VARCHAR(500), + is_active TINYINT(1) DEFAULT 1, + created_at DATETIME DEFAULT NOW(), + updated_at DATETIME, + UNIQUE (project_type, config_key) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/wtm-frontend/.env b/wtm-frontend/.env new file mode 100644 index 0000000..47e26ed --- /dev/null +++ b/wtm-frontend/.env @@ -0,0 +1 @@ +VITE_APP_TITLE=WTM - Work Time Manager diff --git a/wtm-frontend/.env.development b/wtm-frontend/.env.development new file mode 100644 index 0000000..6c6a0cc --- /dev/null +++ b/wtm-frontend/.env.development @@ -0,0 +1,2 @@ +VITE_API_BASE_URL=http://localhost:8080 +VITE_APP_ENV=development diff --git a/wtm-frontend/.env.production b/wtm-frontend/.env.production new file mode 100644 index 0000000..814af35 --- /dev/null +++ b/wtm-frontend/.env.production @@ -0,0 +1,2 @@ +VITE_API_BASE_URL=https://wtmgr.hanwhaocean.com +VITE_APP_ENV=production diff --git a/wtm-frontend/.gitignore b/wtm-frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/wtm-frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/wtm-frontend/.prettierrc.json b/wtm-frontend/.prettierrc.json new file mode 100644 index 0000000..08740cc --- /dev/null +++ b/wtm-frontend/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 100, + "endOfLine": "lf", + "vueIndentScriptAndStyle": false +} diff --git a/wtm-frontend/README.md b/wtm-frontend/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/wtm-frontend/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/wtm-frontend/package-lock.json b/wtm-frontend/package-lock.json new file mode 100644 index 0000000..7e7138a --- /dev/null +++ b/wtm-frontend/package-lock.json @@ -0,0 +1,3944 @@ +{ + "name": "wtm-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wtm-frontend", + "version": "0.0.0", + "dependencies": { + "@primeuix/themes": "^2.0.3", + "@primevue/forms": "^4.5.4", + "@vueuse/core": "^14.2.1", + "axios": "^1.13.6", + "chart.js": "^4.5.1", + "pinia": "^3.0.4", + "primevue": "^4.5.4", + "vue": "^3.5.30", + "vue-router": "^5.0.4" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.0", + "@vitejs/plugin-vue": "^6.0.5", + "@vue/tsconfig": "^0.9.0", + "eslint": "^10.1.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-vue": "^10.8.0", + "globals": "^17.4.0", + "prettier": "^3.8.1", + "sass": "^1.98.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.57.2", + "vite": "^8.0.1", + "vue-tsc": "^3.2.5" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.3", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@primeuix/forms": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@primeuix/forms/-/forms-0.1.0.tgz", + "integrity": "sha512-LctcQidb+B5PuvAFWH24YH/SIzmHlOabLHpaTeGY/k51iBv1WyCp+5w9JMYuMB/BplSvV0ZGySxQVkN5Azr/aQ==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.6.0" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primeuix/styled": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.7.4.tgz", + "integrity": "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.6.1" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primeuix/styles": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-2.0.3.tgz", + "integrity": "sha512-2ykAB6BaHzR/6TwF8ShpJTsZrid6cVIEBVlookSdvOdmlWuevGu5vWOScgIwqWwlZcvkFYAGR/SUV3OHCTBMdw==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.4" + } + }, + "node_modules/@primeuix/themes": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@primeuix/themes/-/themes-2.0.3.tgz", + "integrity": "sha512-3fS1883mtCWhgUgNf/feiaaDSOND4EBIOu9tZnzJlJ8QtYyL6eFLcA6V3ymCWqLVXQ1+lTVEZv1gl47FIdXReg==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.4" + } + }, + "node_modules/@primeuix/utils": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.6.4.tgz", + "integrity": "sha512-pZ5f+vj7wSzRhC7KoEQRU5fvYAe+RP9+m39CTscZ3UywCD1Y2o6Fe1rRgklMPSkzUcty2jzkA0zMYkiJBD1hgg==", + "license": "MIT", + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/core": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.5.4.tgz", + "integrity": "sha512-lYJJB3wTrDJ8MkLctzHfrPZAqXVxoatjIsswSJzupatf6ZogJHVYADUKcn1JAkLLk8dtV1FA2AxDek663fHO5Q==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.4", + "@primeuix/utils": "^0.6.2" + }, + "engines": { + "node": ">=12.11.0" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@primevue/forms": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/@primevue/forms/-/forms-4.5.4.tgz", + "integrity": "sha512-2TlD8oJEtb8vuKzY3jY0W+7NVBC/Qj0m57iWzpMUmGnEKg9sbQ2/ZiU1sTof710/liYgm4FneRTOYHIpVkiJNA==", + "license": "MIT", + "dependencies": { + "@primeuix/forms": "^0.1.0", + "@primeuix/utils": "^0.6.2", + "@primevue/core": "4.5.4" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/icons": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.5.4.tgz", + "integrity": "sha512-DxgryEc7ZmUqcEhYMcxGBRyFzdtLIoy3jLtlH1zsVSRZaG+iSAcjQ88nvfkZxGUZtZBFL7sRjF6KLq3bJZJwUw==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.6.2", + "@primevue/core": "4.5.4" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.11.tgz", + "integrity": "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.11.tgz", + "integrity": "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.11.tgz", + "integrity": "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.11.tgz", + "integrity": "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.11.tgz", + "integrity": "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.11.tgz", + "integrity": "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.11.tgz", + "integrity": "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.11.tgz", + "integrity": "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.11.tgz", + "integrity": "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.11.tgz", + "integrity": "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.11.tgz", + "integrity": "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", + "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue-macros/common": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz", + "integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==", + "license": "MIT", + "dependencies": { + "@vue/compiler-sfc": "^3.5.22", + "ast-kit": "^2.1.2", + "local-pkg": "^1.1.2", + "magic-string-ast": "^1.0.2", + "unplugin-utils": "^0.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/vue-macros" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz", + "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.31", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz", + "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz", + "integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.31", + "@vue/compiler-dom": "3.5.31", + "@vue/compiler-ssr": "3.5.31", + "@vue/shared": "3.5.31", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz", + "integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz", + "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.31.tgz", + "integrity": "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.31.tgz", + "integrity": "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz", + "integrity": "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.31", + "@vue/runtime-core": "3.5.31", + "@vue/shared": "3.5.31", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.31.tgz", + "integrity": "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.31", + "@vue/shared": "3.5.31" + }, + "peerDependencies": { + "vue": "3.5.31" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz", + "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz", + "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 5.8", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz", + "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.2.1", + "@vueuse/shared": "14.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz", + "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz", + "integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/alien-signals": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz", + "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-kit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", + "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz", + "integrity": "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.4", + "ast-kit": "^2.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", + "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.3", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-vue": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.8.0.tgz", + "integrity": "sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^7.1.0", + "semver": "^7.6.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "vue-eslint-parser": "^10.0.0" + }, + "peerDependenciesMeta": { + "@stylistic/eslint-plugin": { + "optional": true + }, + "@typescript-eslint/parser": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magic-string-ast": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz", + "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==", + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.19" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/primevue": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.5.4.tgz", + "integrity": "sha512-nTyEohZABFJhVIpeUxgP0EJ8vKcJAhD+Z7DYj95e7ie/MNUCjRNcGjqmE1cXtXi4z54qDfTSI9h2uJ51qz2DIw==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.4", + "@primeuix/styles": "^2.0.2", + "@primeuix/utils": "^0.6.2", + "@primevue/core": "4.5.4", + "@primevue/icons": "4.5.4" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.11.tgz", + "integrity": "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.11" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.11", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.11", + "@rolldown/binding-darwin-x64": "1.0.0-rc.11", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.11", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz", + "integrity": "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", + "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unplugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", + "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz", + "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.2.tgz", + "integrity": "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.11", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz", + "integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.31", + "@vue/compiler-sfc": "3.5.31", + "@vue/runtime-dom": "3.5.31", + "@vue/server-renderer": "3.5.31", + "@vue/shared": "3.5.31" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz", + "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/vue-router": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.4.tgz", + "integrity": "sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg==", + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.28.6", + "@vue-macros/common": "^3.1.1", + "@vue/devtools-api": "^8.0.6", + "ast-walker-scope": "^0.8.3", + "chokidar": "^5.0.0", + "json5": "^2.2.3", + "local-pkg": "^1.1.2", + "magic-string": "^0.30.21", + "mlly": "^1.8.0", + "muggle-string": "^0.4.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "scule": "^1.3.0", + "tinyglobby": "^0.2.15", + "unplugin": "^3.0.0", + "unplugin-utils": "^0.3.1", + "yaml": "^2.8.2" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@pinia/colada": ">=0.21.2", + "@vue/compiler-sfc": "^3.5.17", + "pinia": "^3.0.4", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "@pinia/colada": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "pinia": { + "optional": true + } + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.1.tgz", + "integrity": "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.1.1" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-kit": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz", + "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.1.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-shared": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz", + "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==", + "license": "MIT" + }, + "node_modules/vue-router/node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/vue-tsc": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz", + "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.2.6" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/wtm-frontend/package.json b/wtm-frontend/package.json new file mode 100644 index 0000000..eac5386 --- /dev/null +++ b/wtm-frontend/package.json @@ -0,0 +1,38 @@ +{ + "name": "wtm-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@primeuix/themes": "^2.0.3", + "@primevue/forms": "^4.5.4", + "@vueuse/core": "^14.2.1", + "axios": "^1.13.6", + "chart.js": "^4.5.1", + "pinia": "^3.0.4", + "primevue": "^4.5.4", + "vue": "^3.5.30", + "vue-router": "^5.0.4" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.0", + "@vitejs/plugin-vue": "^6.0.5", + "@vue/tsconfig": "^0.9.0", + "eslint": "^10.1.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-vue": "^10.8.0", + "globals": "^17.4.0", + "prettier": "^3.8.1", + "sass": "^1.98.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.57.2", + "vite": "^8.0.1", + "vue-tsc": "^3.2.5" + } +} diff --git a/wtm-frontend/public/favicon.svg b/wtm-frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/wtm-frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/wtm-frontend/public/icons.svg b/wtm-frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/wtm-frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wtm-frontend/src/app/App.vue b/wtm-frontend/src/app/App.vue new file mode 100644 index 0000000..01739ef --- /dev/null +++ b/wtm-frontend/src/app/App.vue @@ -0,0 +1,10 @@ + + + diff --git a/wtm-frontend/src/app/main.ts b/wtm-frontend/src/app/main.ts new file mode 100644 index 0000000..f470318 --- /dev/null +++ b/wtm-frontend/src/app/main.ts @@ -0,0 +1,14 @@ +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import App from './App.vue'; +import router from './router'; +import { setupPrimeVue } from './plugins/primevue'; +import '@/assets/styles/main.scss'; + +const app = createApp(App); + +app.use(createPinia()); +app.use(router); +setupPrimeVue(app); + +app.mount('#app'); diff --git a/wtm-frontend/src/app/plugins/primevue.ts b/wtm-frontend/src/app/plugins/primevue.ts new file mode 100644 index 0000000..ca4bb59 --- /dev/null +++ b/wtm-frontend/src/app/plugins/primevue.ts @@ -0,0 +1,23 @@ +import PrimeVue from 'primevue/config'; +import Aura from '@primeuix/themes/aura'; +import ConfirmationService from 'primevue/confirmationservice'; +import ToastService from 'primevue/toastservice'; +import DialogService from 'primevue/dialogservice'; +import Tooltip from 'primevue/tooltip'; +import type { App } from 'vue'; + +export function setupPrimeVue(app: App) { + app.use(PrimeVue, { + theme: { + preset: Aura, + options: { + darkModeSelector: '.app-dark', + }, + }, + ripple: true, + }); + app.use(ConfirmationService); + app.use(ToastService); + app.use(DialogService); + app.directive('tooltip', Tooltip); +} diff --git a/wtm-frontend/src/app/router.ts b/wtm-frontend/src/app/router.ts new file mode 100644 index 0000000..2aa99a9 --- /dev/null +++ b/wtm-frontend/src/app/router.ts @@ -0,0 +1,43 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import { authGuard } from '@/core/auth/auth.guard'; +import { authRoutes } from '@/modules/auth/auth.routes'; +import { dashboardRoutes } from '@/modules/dashboard/dashboard.routes'; +import { userRoutes } from '@/modules/user/user.routes'; +import { projectRoutes } from '@/modules/project/project.routes'; +import { wbsRoutes } from '@/modules/wbs/wbs.routes'; +import { tealRoutes } from '@/modules/teal/teal.routes'; +import { timesheetRoutes } from '@/modules/timesheet/timesheet.routes'; +import { approvalRoutes } from '@/modules/approval/approval.routes'; +import { reportRoutes } from '@/modules/report/report.routes'; +import { settingsRoutes } from '@/modules/settings/settings.routes'; + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + ...authRoutes, + { + path: '/', + component: () => import('@/core/components/AppLayout.vue'), + beforeEnter: authGuard, + redirect: '/dashboard', + children: [ + ...dashboardRoutes, + ...userRoutes, + ...projectRoutes, + ...wbsRoutes, + ...tealRoutes, + ...timesheetRoutes, + ...approvalRoutes, + ...reportRoutes, + ...settingsRoutes, + ], + }, + { + path: '/:pathMatch(.*)*', + name: 'not-found', + component: () => import('@/core/components/NotFoundView.vue'), + }, + ], +}); + +export default router; diff --git a/wtm-frontend/src/assets/hero.png b/wtm-frontend/src/assets/hero.png new file mode 100644 index 0000000..cc51a3d Binary files /dev/null and b/wtm-frontend/src/assets/hero.png differ diff --git a/wtm-frontend/src/assets/images/logo.svg b/wtm-frontend/src/assets/images/logo.svg new file mode 100644 index 0000000..f1fa1b7 --- /dev/null +++ b/wtm-frontend/src/assets/images/logo.svg @@ -0,0 +1,4 @@ + + + W + diff --git a/wtm-frontend/src/assets/styles/_form-grid.scss b/wtm-frontend/src/assets/styles/_form-grid.scss new file mode 100644 index 0000000..6c8ffb2 --- /dev/null +++ b/wtm-frontend/src/assets/styles/_form-grid.scss @@ -0,0 +1,49 @@ +@use 'variables' as *; + +.form-grid { + display: grid; + gap: $space-md; + grid-template-columns: repeat(12, 1fr); + + @media (max-width: $bp-mobile) { + grid-template-columns: 1fr; + } +} + +.col-1 { grid-column: span 1; } +.col-2 { grid-column: span 2; } +.col-3 { grid-column: span 3; } +.col-4 { grid-column: span 4; } +.col-6 { grid-column: span 6; } +.col-8 { grid-column: span 8; } +.col-12 { grid-column: span 12; } + +@media (max-width: $bp-mobile) { + [class^='col-'] { + grid-column: span 1 !important; + } +} + +.form-field { + display: flex; + flex-direction: column; + gap: $space-xs; + + &__label { + font-size: $font-size-sm; + font-weight: 600; + color: $color-text; + &--required::after { + content: ' *'; + color: $color-danger; + } + } + &__error { + font-size: $font-size-xs; + color: $color-danger; + } + &__hint { + font-size: $font-size-xs; + color: $color-text-muted; + } +} diff --git a/wtm-frontend/src/assets/styles/_overrides.scss b/wtm-frontend/src/assets/styles/_overrides.scss new file mode 100644 index 0000000..cf4cf32 --- /dev/null +++ b/wtm-frontend/src/assets/styles/_overrides.scss @@ -0,0 +1 @@ +// PrimeVue theme overrides diff --git a/wtm-frontend/src/assets/styles/_variables.scss b/wtm-frontend/src/assets/styles/_variables.scss new file mode 100644 index 0000000..c2b4e0c --- /dev/null +++ b/wtm-frontend/src/assets/styles/_variables.scss @@ -0,0 +1,53 @@ +// Breakpoints +$bp-mobile: 576px; +$bp-tablet: 768px; +$bp-desktop: 992px; +$bp-wide: 1200px; +$bp-ultra: 1400px; + +// Layout +$sidebar-width: 260px; +$sidebar-collapsed-width: 64px; +$topbar-height: 56px; +$page-padding-x: 1.5rem; +$page-padding-y: 1.25rem; + +// Spacing (8px base) +$space-xs: 0.25rem; +$space-sm: 0.5rem; +$space-md: 1rem; +$space-lg: 1.5rem; +$space-xl: 2rem; +$space-2xl: 3rem; + +// Typography +$font-size-xs: 0.75rem; +$font-size-sm: 0.875rem; +$font-size-base: 1rem; +$font-size-lg: 1.125rem; +$font-size-xl: 1.25rem; +$font-size-2xl: 1.5rem; + +// Border Radius +$radius-sm: 6px; +$radius-md: 8px; +$radius-lg: 12px; + +// Z-Index +$z-sidebar: 100; +$z-topbar: 110; +$z-overlay: 200; +$z-dialog: 300; +$z-toast: 400; + +// Semantic Colors (PrimeVue tokens) +$color-surface: var(--p-surface-0); +$color-surface-card: var(--p-surface-0); +$color-surface-hover: var(--p-surface-100); +$color-border: var(--p-surface-200); +$color-text: var(--p-text-color); +$color-text-muted: var(--p-text-muted-color); +$color-primary: var(--p-primary-color); +$color-danger: var(--p-red-500); +$color-success: var(--p-green-500); +$color-warning: var(--p-yellow-500); diff --git a/wtm-frontend/src/assets/styles/main.scss b/wtm-frontend/src/assets/styles/main.scss new file mode 100644 index 0000000..b3508b2 --- /dev/null +++ b/wtm-frontend/src/assets/styles/main.scss @@ -0,0 +1,39 @@ +@use 'variables' as v; +@use 'form-grid'; +@use 'overrides'; + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; +} + +body { + margin: 0; + font-family: var(--p-font-family); + color: var(--p-text-color); + background: var(--p-surface-ground); +} + +.text-center { text-align: center; } +.text-right { text-align: right; } +.text-muted { color: var(--p-text-muted-color); } +.text-sm { font-size: v.$font-size-sm; } +.text-xs { font-size: v.$font-size-xs; } + +.card { + background: var(--p-surface-0); + border: 1px solid var(--p-surface-200); + border-radius: v.$radius-md; + padding: v.$space-lg; + + @media (max-width: v.$bp-mobile) { + padding: v.$space-md; + border-radius: v.$radius-sm; + } +} diff --git a/wtm-frontend/src/assets/vite.svg b/wtm-frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/wtm-frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/wtm-frontend/src/assets/vue.svg b/wtm-frontend/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/wtm-frontend/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/wtm-frontend/src/core/api/api.types.ts b/wtm-frontend/src/core/api/api.types.ts new file mode 100644 index 0000000..745e28c --- /dev/null +++ b/wtm-frontend/src/core/api/api.types.ts @@ -0,0 +1,13 @@ +/** WBX compatible list response */ +export interface PageResponse { + items: T[]; + total: number; + skip: number; + limit: number; +} + +/** WBX compatible error response */ +export interface ErrorResponse { + detail: string; + code?: string; +} diff --git a/wtm-frontend/src/core/api/axios.ts b/wtm-frontend/src/core/api/axios.ts new file mode 100644 index 0000000..a14dc8e --- /dev/null +++ b/wtm-frontend/src/core/api/axios.ts @@ -0,0 +1,64 @@ +import axios from 'axios'; +import type { InternalAxiosRequestConfig, AxiosError } from 'axios'; +import { authService } from '@/core/auth/auth.service'; +import router from '@/app/router'; + +const api = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL, + timeout: 30000, + headers: { 'Content-Type': 'application/json' }, +}); + +// Request: attach JWT +api.interceptors.request.use((config: InternalAxiosRequestConfig) => { + const token = authService.getAccessToken(); + if (token) config.headers.Authorization = `Bearer ${token}`; + return config; +}); + +// Response: 401 token refresh + retry +let isRefreshing = false; +let failedQueue: Array<{ resolve: (token: string) => void; reject: (error: unknown) => void }> = + []; + +api.interceptors.response.use( + (response) => response, + async (error: AxiosError) => { + const original = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; + + if (error.response?.status === 401 && !original._retry) { + if (isRefreshing) { + return new Promise((resolve, reject) => { + failedQueue.push({ + resolve: (token: string) => { + original.headers.Authorization = `Bearer ${token}`; + resolve(api(original)); + }, + reject, + }); + }); + } + + original._retry = true; + isRefreshing = true; + try { + const newToken = await authService.refreshToken(); + failedQueue.forEach((q) => q.resolve(newToken)); + failedQueue = []; + original.headers.Authorization = `Bearer ${newToken}`; + return api(original); + } catch { + failedQueue.forEach((q) => q.reject(error)); + failedQueue = []; + authService.clearTokens(); + router.push({ name: 'login' }); + return Promise.reject(error); + } finally { + isRefreshing = false; + } + } + return Promise.reject(error); + }, +); + +export default api; diff --git a/wtm-frontend/src/core/auth/auth.guard.ts b/wtm-frontend/src/core/auth/auth.guard.ts new file mode 100644 index 0000000..b4b40a3 --- /dev/null +++ b/wtm-frontend/src/core/auth/auth.guard.ts @@ -0,0 +1,10 @@ +import type { NavigationGuardWithThis } from 'vue-router'; +import { authService } from './auth.service'; + +export const authGuard: NavigationGuardWithThis = (_to, _from, next) => { + if (authService.isAuthenticated()) { + next(); + } else { + next({ name: 'login' }); + } +}; diff --git a/wtm-frontend/src/core/auth/auth.service.ts b/wtm-frontend/src/core/auth/auth.service.ts new file mode 100644 index 0000000..0cacb5e --- /dev/null +++ b/wtm-frontend/src/core/auth/auth.service.ts @@ -0,0 +1,43 @@ +const ACCESS_TOKEN_KEY = 'wtm_access_token'; +const REFRESH_TOKEN_KEY = 'wtm_refresh_token'; + +export const authService = { + getAccessToken(): string | null { + return localStorage.getItem(ACCESS_TOKEN_KEY); + }, + + getRefreshToken(): string | null { + return localStorage.getItem(REFRESH_TOKEN_KEY); + }, + + setTokens(accessToken: string, refreshToken: string) { + localStorage.setItem(ACCESS_TOKEN_KEY, accessToken); + localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); + }, + + clearTokens() { + localStorage.removeItem(ACCESS_TOKEN_KEY); + localStorage.removeItem(REFRESH_TOKEN_KEY); + }, + + isAuthenticated(): boolean { + return !!this.getAccessToken(); + }, + + async refreshToken(): Promise { + const refreshToken = this.getRefreshToken(); + if (!refreshToken) throw new Error('No refresh token'); + + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/wtm/auth/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken }), + }); + + if (!response.ok) throw new Error('Refresh failed'); + + const data = await response.json(); + this.setTokens(data.accessToken, data.refreshToken); + return data.accessToken; + }, +}; diff --git a/wtm-frontend/src/core/auth/auth.types.ts b/wtm-frontend/src/core/auth/auth.types.ts new file mode 100644 index 0000000..1655b98 --- /dev/null +++ b/wtm-frontend/src/core/auth/auth.types.ts @@ -0,0 +1,18 @@ +export interface AuthUser { + id: number; + email: string; + fullName: string; + roles: string[]; + departmentId?: number; +} + +export interface LoginRequest { + email: string; + password: string; +} + +export interface LoginResponse { + accessToken: string; + refreshToken: string; + user: AuthUser; +} diff --git a/wtm-frontend/src/core/components/AppLayout.vue b/wtm-frontend/src/core/components/AppLayout.vue new file mode 100644 index 0000000..e39adb7 --- /dev/null +++ b/wtm-frontend/src/core/components/AppLayout.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/wtm-frontend/src/core/components/AppSidebar.vue b/wtm-frontend/src/core/components/AppSidebar.vue new file mode 100644 index 0000000..1479c37 --- /dev/null +++ b/wtm-frontend/src/core/components/AppSidebar.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/wtm-frontend/src/core/components/AppTopbar.vue b/wtm-frontend/src/core/components/AppTopbar.vue new file mode 100644 index 0000000..4320bf9 --- /dev/null +++ b/wtm-frontend/src/core/components/AppTopbar.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/wtm-frontend/src/core/components/BaseCrudTable.vue b/wtm-frontend/src/core/components/BaseCrudTable.vue new file mode 100644 index 0000000..16d08ba --- /dev/null +++ b/wtm-frontend/src/core/components/BaseCrudTable.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/wtm-frontend/src/core/components/BaseFormDialog.vue b/wtm-frontend/src/core/components/BaseFormDialog.vue new file mode 100644 index 0000000..78b366b --- /dev/null +++ b/wtm-frontend/src/core/components/BaseFormDialog.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/wtm-frontend/src/core/components/BasePageHeader.vue b/wtm-frontend/src/core/components/BasePageHeader.vue new file mode 100644 index 0000000..1f37c52 --- /dev/null +++ b/wtm-frontend/src/core/components/BasePageHeader.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/wtm-frontend/src/core/components/NotFoundView.vue b/wtm-frontend/src/core/components/NotFoundView.vue new file mode 100644 index 0000000..1c1c15a --- /dev/null +++ b/wtm-frontend/src/core/components/NotFoundView.vue @@ -0,0 +1,7 @@ + diff --git a/wtm-frontend/src/core/composables/useCurrentUser.ts b/wtm-frontend/src/core/composables/useCurrentUser.ts new file mode 100644 index 0000000..06f9d5f --- /dev/null +++ b/wtm-frontend/src/core/composables/useCurrentUser.ts @@ -0,0 +1,20 @@ +import { computed } from 'vue'; +import { useAuthStore } from '@/modules/auth/auth.store'; + +export function useCurrentUser() { + const authStore = useAuthStore(); + + const currentUser = computed(() => authStore.currentUser); + const isAuthenticated = computed(() => !!authStore.currentUser); + const roles = computed(() => authStore.currentUser?.roles ?? []); + + function hasRole(role: string): boolean { + return roles.value.includes(role); + } + + function hasAnyRole(requiredRoles: string[]): boolean { + return requiredRoles.some((r) => roles.value.includes(r)); + } + + return { currentUser, isAuthenticated, roles, hasRole, hasAnyRole }; +} diff --git a/wtm-frontend/src/core/constants/app.constants.ts b/wtm-frontend/src/core/constants/app.constants.ts new file mode 100644 index 0000000..c39c760 --- /dev/null +++ b/wtm-frontend/src/core/constants/app.constants.ts @@ -0,0 +1,143 @@ +// Breakpoints +export const BREAKPOINTS = { + mobile: 576, + tablet: 768, + desktop: 992, + wide: 1200, + ultra: 1400, +} as const; + +// Layout +export const LAYOUT = { + sidebarWidth: 260, + sidebarCollapsedWidth: 64, + topbarHeight: 56, +} as const; + +// Pagination +export const PAGINATION = { + defaultPageSize: 20, + pageSizeOptions: [10, 20, 50, 100], +} as const; + +// Toast +export const TOAST = { + defaultLife: 3000, + errorLife: 5000, +} as const; + +// Date formats +export const DATE_FORMAT = { + display: 'YYYY-MM-DD', + api: 'YYYY-MM-DD', + datetime: 'YYYY-MM-DD HH:mm', + weekStart: 1, +} as const; + +// Timesheet rules +export const TIMESHEET_RULES = { + maxDailyHours: 24, + warnDailyHours: 10, + defaultDailyHours: 8, + maxWeeklyHours: 52, +} as const; + +// Roles +export const ROLES = { + SA: 'SA', + PM: 'PM', + PCM: 'PCM', + PTK: 'PTK', + DL: 'DL', + USER: 'USER', +} as const; + +// Timesheet status +export const TIMESHEET_STATUS: Record = { + DRAFT: { label: '작성중', severity: 'secondary' }, + SUBMITTED: { label: '제출됨', severity: 'info' }, + DL_APPROVED: { label: 'DL승인', severity: 'warn' }, + APPROVED: { label: '승인', severity: 'success' }, + REJECTED: { label: '반려', severity: 'danger' }, +}; + +// Project status +export const PROJECT_STATUS: Record = { + ACTIVE: { label: '진행중', severity: 'success' }, + CLOSED: { label: '종료', severity: 'secondary' }, + HOLD: { label: '보류', severity: 'warn' }, +}; + +// Entry types +export const ENTRY_TYPES: Record = { + NON_PROJECT: { label: 'Non-Project', icon: 'pi pi-calendar' }, + OTHER_PROJECT: { label: 'Other Project', icon: 'pi pi-briefcase' }, + EPC: { label: 'EPC Project', icon: 'pi pi-building' }, +}; + +// Non-Project categories +export const NP_CATEGORIES = [ + { value: 'ANNUAL_LEAVE', label: '연차' }, + { value: 'SICK_LEAVE', label: '병가' }, + { value: 'TRAINING', label: '교육' }, + { value: 'ADMIN', label: '행정' }, + { value: 'PUBLIC_HOLIDAY', label: '공휴일' }, + { value: 'OTHER', label: '기타' }, +] as const; + +// Sidebar menu +export const MENU_ITEMS = [ + { + label: '대시보드', + icon: 'pi pi-home', + to: '/dashboard', + roles: ['SA', 'PM', 'PCM', 'PTK', 'DL', 'USER'], + }, + { + label: '시수 관리', + icon: 'pi pi-clock', + roles: ['SA', 'PM', 'DL', 'USER'], + items: [ + { label: '시수 입력', to: '/timesheets', roles: ['USER', 'DL', 'PM', 'SA'] }, + { label: '시수 이력', to: '/timesheets/history', roles: ['USER', 'DL', 'PM', 'SA'] }, + { label: 'Excel 업로드', to: '/timesheets/upload', roles: ['USER'] }, + ], + }, + { + label: '결재', + icon: 'pi pi-check-square', + roles: ['DL', 'PM', 'SA'], + items: [ + { label: '결재 대기', to: '/approvals', roles: ['DL', 'PM', 'SA'] }, + { label: '결재 이력', to: '/approvals/history', roles: ['DL', 'PM', 'SA'] }, + ], + }, + { + label: '프로젝트', + icon: 'pi pi-briefcase', + roles: ['SA', 'PM', 'PCM'], + items: [ + { label: '프로젝트 목록', to: '/projects', roles: ['SA', 'PM', 'PCM'] }, + { label: 'WBS 관리', to: '/wbs', roles: ['SA', 'PM', 'PCM'] }, + { label: 'TEAL 관리', to: '/teal', roles: ['SA', 'PM', 'PCM'] }, + ], + }, + { + label: '리포트', + icon: 'pi pi-chart-bar', + to: '/reports', + roles: ['SA', 'PM', 'PCM', 'DL'], + }, + { + label: '사용자 관리', + icon: 'pi pi-users', + to: '/users', + roles: ['SA', 'PTK'], + }, + { + label: '시스템 설정', + icon: 'pi pi-cog', + to: '/settings', + roles: ['SA'], + }, +]; diff --git a/wtm-frontend/src/env.d.ts b/wtm-frontend/src/env.d.ts new file mode 100644 index 0000000..41bec47 --- /dev/null +++ b/wtm-frontend/src/env.d.ts @@ -0,0 +1,9 @@ +/// +interface ImportMetaEnv { + readonly VITE_API_BASE_URL: string; + readonly VITE_APP_TITLE: string; + readonly VITE_APP_ENV: 'development' | 'staging' | 'production'; +} +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/wtm-frontend/src/modules/approval/approval.routes.ts b/wtm-frontend/src/modules/approval/approval.routes.ts new file mode 100644 index 0000000..1f46137 --- /dev/null +++ b/wtm-frontend/src/modules/approval/approval.routes.ts @@ -0,0 +1,5 @@ +import type { RouteRecordRaw } from 'vue-router'; +export const approvalRoutes: RouteRecordRaw[] = [ + { path: '/approvals', name: 'approval-pending', component: () => import('./views/ApprovalPendingView.vue'), meta: { title: '결재 대기' } }, + { path: '/approvals/history', name: 'approval-history', component: () => import('./views/ApprovalHistoryView.vue'), meta: { title: '결재 이력' } }, +]; diff --git a/wtm-frontend/src/modules/approval/approval.service.ts b/wtm-frontend/src/modules/approval/approval.service.ts new file mode 100644 index 0000000..9602050 --- /dev/null +++ b/wtm-frontend/src/modules/approval/approval.service.ts @@ -0,0 +1,12 @@ +import api from '@/core/api/axios'; +const BASE = '/api/wtm/approvals'; +export const approvalService = { + getPending: () => api.get(`${BASE}/pending`), + approve: (id: number, comment?: string) => api.post(`${BASE}/${id}/approve`, { comment }), + reject: (id: number, comment: string) => api.post(`${BASE}/${id}/reject`, { comment }), + batchApprove: (ids: number[]) => api.post(`${BASE}/batch-approve`, { ids }), + addComment: (id: number, comment: string) => api.post(`${BASE}/${id}/comments`, { comment }), + getById: (id: number) => api.get(`${BASE}/${id}`), + getHistory: (p?: Record) => api.get(`${BASE}/history`, { params: p }), + getOverdue: () => api.get(`${BASE}/overdue`), +}; diff --git a/wtm-frontend/src/modules/approval/approval.store.ts b/wtm-frontend/src/modules/approval/approval.store.ts new file mode 100644 index 0000000..0f580c4 --- /dev/null +++ b/wtm-frontend/src/modules/approval/approval.store.ts @@ -0,0 +1,7 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +export const useApprovalStore = defineStore('approval', () => { + const pending = ref([]); + const loading = ref(false); + return { pending, loading }; +}); diff --git a/wtm-frontend/src/modules/approval/approval.types.ts b/wtm-frontend/src/modules/approval/approval.types.ts new file mode 100644 index 0000000..ce053a8 --- /dev/null +++ b/wtm-frontend/src/modules/approval/approval.types.ts @@ -0,0 +1,2 @@ +export interface Approval { id: number; timesheetId: number; requesterId: number; status: string; submittedAt: string; } +export interface ApprovalLine { id: number; approverId: number; approvalOrder: number; roleCode: string; status: string; } diff --git a/wtm-frontend/src/modules/approval/components/ApprovalDetailDialog.vue b/wtm-frontend/src/modules/approval/components/ApprovalDetailDialog.vue new file mode 100644 index 0000000..529975b --- /dev/null +++ b/wtm-frontend/src/modules/approval/components/ApprovalDetailDialog.vue @@ -0,0 +1,128 @@ + + +