diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..2eb0506 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: "ko-KR" +ignored_branch: "" +early_access: false +reviews: + profile: "chill" + request_changes_workflow: false + high_level_summary: true + poem: true + review_status: true + collapse_walkthrough: false + auto_review: + enabled: true + drafts: false + base_branches: + - test +chat: + auto_reply: true \ No newline at end of file diff --git a/.github/workflows/spring-boot-cicd.yml b/.github/workflows/spring-boot-cicd.yml index 22250a0..a52590b 100644 --- a/.github/workflows/spring-boot-cicd.yml +++ b/.github/workflows/spring-boot-cicd.yml @@ -43,13 +43,13 @@ jobs: - name: Gradle Wrapper 실행권한 부여 run: chmod +x gradlew - # application-prod.yml 파일을 빌드 전에 생성 - - name: Create application-prod.yml from secret + - name: application-prod.yml 파일 생성 run: | - mkdir -p src/main/resources - cat << 'EOF' > ./src/main/resources/application-prod.yml - ${{ secrets.APPLICATION_PROD_YML }} - EOF + echo "${{ secrets.APPLICATION_PROD_YML }}" > CT-web/src/main/resources/application-prod.yml + + - name: config-imports.yml 파일 생성 + run: | + echo "${{ secrets.CONFIG_IMPORTS_YML }}" > CT-web/src/main/resources/config-imports.yml # 브랜치 별 active profile 설정 - name: Decide active profile diff --git a/.gitignore b/.gitignore index 50b60a4..73aa13d 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,6 @@ out/ .kotlin ### ENV ### -application-*.yml +CT-web/src/main/resources/application-*.yml +CT-web/src/main/resources/config-imports.yml +CT-web/src/main/resources/springdoc.yml diff --git a/CT-auth/build.gradle.kts b/CT-auth/build.gradle.kts new file mode 100644 index 0000000..b5c05c1 --- /dev/null +++ b/CT-auth/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("java-library") +} + +tasks.bootJar { + enabled = false +} + +tasks.jar { + enabled = true + archiveClassifier.set("") +} + +dependencies { + implementation(project(":CT-common")) + + api(libs.spring.boot.starter.security) + api(libs.spring.security.test) +} + diff --git a/CT-common/build.gradle.kts b/CT-common/build.gradle.kts new file mode 100644 index 0000000..ff31113 --- /dev/null +++ b/CT-common/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + id("java-library") +} + +tasks.bootJar { + enabled = false +} + +tasks.jar { + enabled = true + archiveClassifier.set("") +} + +// common 모듈 api 의존성은 모든 모듈에 적용 +dependencies { + // Spring Starter Web + api(libs.spring.boot.starter.web) + + // JPA + api(libs.spring.boot.starter.data.jpa) + + // Validation + api(libs.spring.boot.starter.validation) + + // Jackson + api(libs.jackson.module.kotlin) + + api(libs.kotlin.reflect) + api(libs.kotlin.stdlib) +} + diff --git a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/core/util/CommonUtil.kt b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/core/util/CommonUtil.kt new file mode 100644 index 0000000..da84520 --- /dev/null +++ b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/core/util/CommonUtil.kt @@ -0,0 +1,13 @@ +package com.chuseok22.ctcommon.core.util + +/** + * null 또는 빈 문자열을 기본값으로 대체 + */ +fun String?.nvl(fallback: String): String { + return when { + this == null -> fallback + this == "null" -> fallback + this.isBlank() -> fallback + else -> this + } +} \ No newline at end of file diff --git a/CT-member/build.gradle.kts b/CT-member/build.gradle.kts new file mode 100644 index 0000000..c5a1c62 --- /dev/null +++ b/CT-member/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("java-library") +} + +tasks.bootJar { + enabled = false +} + +tasks.jar { + enabled = true + archiveClassifier.set("") +} + +dependencies { + implementation(project(":CT-common")) + + // Postgres + api(libs.postgresql) +} diff --git a/CT-web/build.gradle.kts b/CT-web/build.gradle.kts new file mode 100644 index 0000000..9826098 --- /dev/null +++ b/CT-web/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("java-library") + id("org.springframework.boot") +} + +dependencies { + implementation(project(":CT-auth")) + implementation(project(":CT-common")) + implementation(project(":CT-member")) + + implementation(libs.swagger.ui) + implementation(libs.http.logging) + implementation(libs.api.change.log) +} + diff --git a/src/main/kotlin/com/chuseok22/campustableserver/CampusTableServerApplication.kt b/CT-web/src/main/kotlin/com/chuseok22/ctweb/CampusTableServerApplication.kt similarity index 86% rename from src/main/kotlin/com/chuseok22/campustableserver/CampusTableServerApplication.kt rename to CT-web/src/main/kotlin/com/chuseok22/ctweb/CampusTableServerApplication.kt index 2c38939..6583231 100644 --- a/src/main/kotlin/com/chuseok22/campustableserver/CampusTableServerApplication.kt +++ b/CT-web/src/main/kotlin/com/chuseok22/ctweb/CampusTableServerApplication.kt @@ -1,4 +1,4 @@ -package com.chuseok22.campustableserver +package com.chuseok22.ctweb import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication diff --git a/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/ComponentScanConfig.kt b/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/ComponentScanConfig.kt new file mode 100644 index 0000000..c619122 --- /dev/null +++ b/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/ComponentScanConfig.kt @@ -0,0 +1,11 @@ +package com.chuseok22.ctweb.infrastructure.config + +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration + +@Configuration +@ComponentScan(basePackages = [ + "com.chuseok22.*" +]) +class ComponentScanConfig { +} \ No newline at end of file diff --git a/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/DatabaseScanConfig.kt b/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/DatabaseScanConfig.kt new file mode 100644 index 0000000..50222e6 --- /dev/null +++ b/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/DatabaseScanConfig.kt @@ -0,0 +1,11 @@ +package com.chuseok22.ctweb.infrastructure.config + +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.context.annotation.Configuration +import org.springframework.data.jpa.repository.config.EnableJpaRepositories + +@Configuration +@EntityScan(basePackages = ["com.chuseok22.*"]) +@EnableJpaRepositories(basePackages = ["com.chuseok22.*"]) +class DatabaseScanConfig { +} \ No newline at end of file diff --git a/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/SwaggerConfig.kt b/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/SwaggerConfig.kt new file mode 100644 index 0000000..82fbf78 --- /dev/null +++ b/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/SwaggerConfig.kt @@ -0,0 +1,53 @@ +package com.chuseok22.ctweb.infrastructure.config + +import com.chuseok22.ctweb.infrastructure.properties.SpringDocProperties +import io.swagger.v3.oas.annotations.OpenAPIDefinition +import io.swagger.v3.oas.annotations.info.Info +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.security.SecurityRequirement +import io.swagger.v3.oas.models.security.SecurityScheme +import io.swagger.v3.oas.models.servers.Server +import org.springdoc.core.customizers.OpenApiCustomizer +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@OpenAPIDefinition( + info = Info( + title = "캠퍼스 테이블 CampusTable" + ) +) +@Configuration +@EnableConfigurationProperties(SpringDocProperties::class) +class SwaggerConfig( + private val properties: SpringDocProperties +) { + + @Bean + fun OpenAPI(): OpenAPI { + val apiKey: SecurityScheme = SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .`in`(SecurityScheme.In.HEADER) + .name("Authorization") + + return OpenAPI() + .components(Components().addSecuritySchemes("Bearer Token", apiKey)) + .addSecurityItem(SecurityRequirement().addList("Bearer Token")) + } + + @Bean + fun serverCustomizer(): OpenApiCustomizer { + return OpenApiCustomizer { openApi -> + properties.servers.forEach { server -> + openApi.addServersItem( + Server() + .url(server.url) + .description(server.description) + ) + } + } + } +} \ No newline at end of file diff --git a/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/properties/SpringDocProperties.kt b/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/properties/SpringDocProperties.kt new file mode 100644 index 0000000..2494a1b --- /dev/null +++ b/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/properties/SpringDocProperties.kt @@ -0,0 +1,20 @@ +package com.chuseok22.ctweb.infrastructure.properties + +import jakarta.validation.Valid +import jakarta.validation.constraints.NotBlank +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.validation.annotation.Validated + +@Validated +@ConfigurationProperties(prefix = "springdoc") +data class SpringDocProperties( + @field:Valid + val servers: List +) { + data class Servers( + @field:NotBlank + val url: String, + @field:NotBlank + val description: String + ) +} diff --git a/CT-web/src/main/resources/application.yml b/CT-web/src/main/resources/application.yml new file mode 100644 index 0000000..5e06972 --- /dev/null +++ b/CT-web/src/main/resources/application.yml @@ -0,0 +1,30 @@ +spring: + profiles: + active: prod + config: + import: classpath:config-imports.yml + jpa: + open-in-view: false + properties: + hibernate: + jdbc: + time_zone: UTC + dialect: org.hibernate.dialect.PostgreSQLDialect + show_sql: false + format_sql: true + use_sql_comments: true + flyway: + enabled: true + locations: classpath:db/migration + baseline-on-migrate: false + validate-on-migrate: true + out-of-order: false # 과거 버전이 뒤늦게 들어오는 경우 방지 + clean-disabled: true # 실수로 clean 되는 경우 방지 + jackson: + time-zone: Asia/Seoul + servlet: + multipart: + enabled: true # 파일 업로드 허용 + max-file-size: 200MB # 업로드 파일 최대 크기 + max-request-size: 1000MB # 요청 전체 최대 크기 + file-size-threshold: 10MB diff --git a/build.gradle.kts b/build.gradle.kts index 5bc473b..2299c8d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,82 +1,59 @@ -plugins { - kotlin("jvm") version "1.9.25" - kotlin("plugin.spring") version "1.9.25" - id("org.springframework.boot") version "3.5.9" - id("io.spring.dependency-management") version "1.1.7" - kotlin("plugin.jpa") version "1.9.25" -} +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -group = "com.chuseok22" -version = "0.0.1-SNAPSHOT" -description = "campus-table-server" +plugins { + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.kotlin.spring) apply false + alias(libs.plugins.kotlin.jpa) apply false + alias(libs.plugins.kotlin.allopen) apply false + alias(libs.plugins.spring.boot) apply false + alias(libs.plugins.spring.dependency.management) apply false -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } } -configurations { - compileOnly { - extendsFrom(configurations.annotationProcessor.get()) - } -} +allprojects { + group = "com.chuseok22" + version = "0.0.1-SNAPSHOT" + description = "campus-table-server" -repositories { - maven { - url = uri("https://nexus.chuseok22.com/repository/maven-releases/") + repositories { + maven { + url = uri("https://nexus.chuseok22.com/repository/maven-releases/") + } + mavenCentral() } - mavenCentral() } -dependencies { - // Spring Web - implementation("org.springframework.boot:spring-boot-starter-web") - testImplementation("org.springframework.boot:spring-boot-starter-test") - - // JPA - implementation("org.springframework.boot:spring-boot-starter-data-jpa") - - // Postgres - runtimeOnly("org.postgresql:postgresql") - - // Redis - implementation("org.springframework.boot:spring-boot-starter-data-redis") - - // Spring Security - implementation("org.springframework.boot:spring-boot-starter-security") - testImplementation("org.springframework.security:spring-security-test") +subprojects { + apply(plugin = "java-library") + apply(plugin = "org.jetbrains.kotlin.jvm") + apply(plugin = "org.jetbrains.kotlin.plugin.spring") + apply(plugin = "org.jetbrains.kotlin.plugin.jpa") + apply(plugin = "org.springframework.boot") + apply(plugin = "io.spring.dependency-management") - // Spring Validation - implementation("org.springframework.boot:spring-boot-starter-validation") - - // Jackson - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") - - // Reflect - implementation("org.jetbrains.kotlin:kotlin-reflect") - - // Lombok - compileOnly("org.projectlombok:lombok") - annotationProcessor("org.projectlombok:lombok") + configure { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } + } - // JUnit5 - testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") -} + configurations.named("compileOnly") { + extendsFrom(configurations.named("annotationProcessor").get()) + } -kotlin { - compilerOptions { - freeCompilerArgs.addAll("-Xjsr305=strict") + dependencies { + add("testImplementation", rootProject.libs.kotlin.test.junit5) + add("testRuntimeOnly", rootProject.libs.junit.platform.launcher) } -} -allOpen { - annotation("jakarta.persistence.Entity") - annotation("jakarta.persistence.MappedSuperclass") - annotation("jakarta.persistence.Embeddable") -} + tasks.withType { + useJUnitPlatform() + } -tasks.withType { - useJUnitPlatform() + tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) + freeCompilerArgs.add("-Xjsr305=strict") + } + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..7a0388e --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,62 @@ +[versions] +# Plugins & Core +kotlin = "1.9.25" +springBoot = "3.5.9" +springDependencyManagement = "1.1.7" +swaggerUI = "3.0.1" +apiChangeLog = "1.0.1" +sejongPortalLogin = "1.0.0" +httpLogging = "0.0.9" + +[libraries] +# Spring Starter Web +spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" } + +# JPA +spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa" } + +# Validation +spring-boot-starter-validation = { module = "org.springframework.boot:spring-boot-starter-validation" } + +# Test +spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test" } + +# Redis +spring-boot-starter-data-redis = { module = "org.springframework.boot:spring-boot-starter-data-redis" } + +# Spring Security +spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security" } +spring-security-test = { module = "org.springframework.security:spring-security-test" } + +# Postgres +postgresql = { module = "org.postgresql:postgresql" } + +# JSON +jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin" } + +# Kotlin +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib" } +kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5" } +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } + +# Swagger UI +swagger-ui = { module = "org.springdoc:springdoc-openapi-starter-webmvc-ui", version.ref = "swaggerUI" } + +# Chuseok22 ApiChangeLog +api-change-log = { module = "com.chuseok22:ApiChangeLog", version.ref = "apiChangeLog" } + +# Chuseok22 Sejong Portal Login +sejong-portal-login = { module = "com.chuseok22:sejong-portal-login", version.ref = "sejongPortalLogin" } + +# Chuseok22 Logging +http-logging = { module = "com.chuseok22:logging", version.ref = "httpLogging" } + + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } +kotlin-jpa = { id = "org.jetbrains.kotlin.plugin.jpa", version.ref = "kotlin" } +kotlin-allopen = { id = "org.jetbrains.kotlin.plugin.allopen", version.ref = "kotlin" } +spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" } +spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "springDependencyManagement" } diff --git a/settings.gradle.kts b/settings.gradle.kts index c793f26..ea89535 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,6 @@ rootProject.name = "campus-table-server" + +include("CT-auth") +include("CT-common") +include("CT-member") +include("CT-web") \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index e69de29..0000000 diff --git a/src/test/kotlin/com/chuseok22/campustableserver/CampusTableServerApplicationTests.kt b/src/test/kotlin/com/chuseok22/campustableserver/CampusTableServerApplicationTests.kt deleted file mode 100644 index 6c378db..0000000 --- a/src/test/kotlin/com/chuseok22/campustableserver/CampusTableServerApplicationTests.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.chuseok22.campustableserver - -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest - -@SpringBootTest -class CampusTableServerApplicationTests { - - @Test - fun contextLoads() { - } - -}