diff --git a/.github/workflows/_deploy-native.yml b/.github/workflows/_deploy-native.yml new file mode 100644 index 00000000..dc58cdfc --- /dev/null +++ b/.github/workflows/_deploy-native.yml @@ -0,0 +1,52 @@ +name: deploy-native-template + +on: + workflow_call: + inputs: + ocir_repository: + required: true + type: string + dockerfile: + required: true + type: string + description: 'Dockerfile path (e.g., api/Dockerfile-native)' + secrets: + OCI_AUTH_TOKEN: + required: true + DEPLOYER_APP_ID: + required: true + DEPLOYER_APP_PRIVATE_KEY: + required: true + +jobs: + deploy: + name: Deploy Native + runs-on: ubuntu-24.04-arm + + env: + IMAGE_TAG: ${{ github.run_number }} + BUILD_NUMBER: ${{ github.run_number }} + OCIR_REGISTRY: yny.ocir.io + OCIR_NAMESPACE: ax1dvc8vmenm + OCIR_REPOSITORY: ${{ inputs.ocir_repository }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to OCIR + run: echo "${{ secrets.OCI_AUTH_TOKEN }}" | docker login $OCIR_REGISTRY -u ax1dvc8vmenm/members/snutt-deployer --password-stdin + + - name: Docker build, tag, and push image to OCIR + id: build-push-image + run: | + echo "${{ github.token }}" > .github_token + docker build \ + --secret id=github_token,src=./.github_token \ + -f ${{ inputs.dockerfile }} \ + -t $OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG \ + . \ + --platform linux/arm64 + rm -f .github_token + docker push $OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG + echo "image=$OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT diff --git a/.github/workflows/_deploy.yml b/.github/workflows/_deploy.yml new file mode 100644 index 00000000..9bb79339 --- /dev/null +++ b/.github/workflows/_deploy.yml @@ -0,0 +1,52 @@ +name: deploy-template + +on: + workflow_call: + inputs: + ocir_repository: + required: true + type: string + dockerfile: + required: true + type: string + description: 'Dockerfile path (e.g., api/Dockerfile)' + secrets: + OCI_AUTH_TOKEN: + required: true + DEPLOYER_APP_ID: + required: true + DEPLOYER_APP_PRIVATE_KEY: + required: true + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-24.04-arm + + env: + IMAGE_TAG: ${{ github.run_number }} + BUILD_NUMBER: ${{ github.run_number }} + OCIR_REGISTRY: yny.ocir.io + OCIR_NAMESPACE: ax1dvc8vmenm + OCIR_REPOSITORY: ${{ inputs.ocir_repository }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to OCIR + run: echo "${{ secrets.OCI_AUTH_TOKEN }}" | docker login $OCIR_REGISTRY -u ax1dvc8vmenm/members/snutt-deployer --password-stdin + + - name: Docker build, tag, and push image to OCIR + id: build-push-image + run: | + echo "${{ github.token }}" > .github_token + docker build \ + --secret id=github_token,src=./.github_token \ + -f ${{ inputs.dockerfile }} \ + -t $OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG \ + . \ + --platform linux/arm64 + rm -f .github_token + docker push $OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG + echo "image=$OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT diff --git a/.github/workflows/deploy-dev-native.yml b/.github/workflows/deploy-dev-native.yml new file mode 100644 index 00000000..7586173b --- /dev/null +++ b/.github/workflows/deploy-dev-native.yml @@ -0,0 +1,26 @@ +name: Deploy-dev-native + +on: + push: + branches: [ develop ] + +jobs: + deploy-api: + uses: ./.github/workflows/_deploy-native.yml + with: + ocir_repository: snutt-dev/snutt-ev + dockerfile: api/Dockerfile-native + secrets: + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} + + deploy-batch: + uses: ./.github/workflows/_deploy-native.yml + with: + ocir_repository: snutt-dev/snutt-ev-batch + dockerfile: batch/Dockerfile-native + secrets: + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index c3cca76a..5f10d191 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -5,41 +5,22 @@ on: branches: [ develop ] jobs: - deploy: - name: Deploy - runs-on: ubuntu-24.04-arm - env: - IMAGE_TAG: ${{ github.run_number }} - BUILD_NUMBER: ${{ github.run_number }} - ECR_REGISTRY: 405906814034.dkr.ecr.ap-northeast-2.amazonaws.com - ECR_REPOSITORY: snutt-dev/snutt-ev - ECR_BATCH_REPOSITORY: snutt-dev/snutt-ev-batch + deploy-api: + uses: ./.github/workflows/_deploy.yml + with: + ocir_repository: snutt-dev/snutt-ev + dockerfile: api/Dockerfile + secrets: + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ap-northeast-2 - - - name: Login to ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Get and save Auth Token for CodeArtifact - id: get-save-codeartifact-auth-token - run: | - aws codeartifact get-authorization-token --domain wafflestudio --domain-owner 405906814034 --query authorizationToken --region ap-northeast-1 --output text > .codeartifact-auth-token - - - name: Docker build, tag, and push image to ECR - id: build-image - run: | - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . --build-arg CODEARTIFACT_AUTH_TOKEN=$(cat .codeartifact-auth-token) - docker build -f Dockerfile-batch -t $ECR_REGISTRY/$ECR_BATCH_REPOSITORY:$IMAGE_TAG . --build-arg CODEARTIFACT_AUTH_TOKEN=$(cat .codeartifact-auth-token) - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - docker push $ECR_REGISTRY/$ECR_BATCH_REPOSITORY:$IMAGE_TAG - echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" + deploy-batch: + uses: ./.github/workflows/_deploy.yml + with: + ocir_repository: snutt-dev/snutt-ev-batch + dockerfile: batch/Dockerfile + secrets: + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} diff --git a/.github/workflows/deploy-manual.yml b/.github/workflows/deploy-manual.yml new file mode 100644 index 00000000..3ec77994 --- /dev/null +++ b/.github/workflows/deploy-manual.yml @@ -0,0 +1,24 @@ +name: Deploy Manual + +on: + workflow_dispatch: + inputs: + ocir_repository: + description: 'OCIR 리포지토리 (예: snutt-dev/snutt-ev)' + required: true + type: string + dockerfile: + description: 'Dockerfile 경로 (예: api/Dockerfile, batch/Dockerfile)' + required: true + type: string + +jobs: + deploy: + uses: ./.github/workflows/_deploy.yml + with: + ocir_repository: ${{ inputs.ocir_repository }} + dockerfile: ${{ inputs.dockerfile }} + secrets: + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} diff --git a/.github/workflows/deploy-prod-native.yml b/.github/workflows/deploy-prod-native.yml new file mode 100644 index 00000000..a6d4f028 --- /dev/null +++ b/.github/workflows/deploy-prod-native.yml @@ -0,0 +1,41 @@ +name: Deploy-prod-native + +on: + push: + branches: [ main ] + +jobs: + deploy-api: + uses: ./.github/workflows/_deploy-native.yml + with: + ocir_repository: snutt-prod/snutt-ev + dockerfile: api/Dockerfile-native + secrets: + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} + + deploy-batch: + uses: ./.github/workflows/_deploy-native.yml + with: + ocir_repository: snutt-prod/snutt-ev-batch + dockerfile: batch/Dockerfile-native + secrets: + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} + + notify: + needs: [deploy-api, deploy-batch] + runs-on: ubuntu-latest + steps: + - name: Slack Notify + uses: rtCamp/action-slack-notify@v2.3.3 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_CHANNEL: team-snutt-deploy + SLACK_TITLE: NEW RELEASE + SLACK_USERNAME: snutt-ev + SLACK_ICON: https://user-images.githubusercontent.com/35535636/103177470-2237cb00-48be-11eb-9211-3ffa567c8ac3.png + SLACK_MESSAGE: Check for updated environment + SLACK_FOOTER: https://snutt-ev-api.wafflestudio.com/swagger-ui/index.html diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 41d5be4b..7a69acab 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -5,45 +5,30 @@ on: branches: [ main ] jobs: - deploy: - name: Deploy - runs-on: ubuntu-24.04-arm - env: - IMAGE_TAG: ${{ github.run_number }} - BUILD_NUMBER: ${{ github.run_number }} - ECR_REGISTRY: 405906814034.dkr.ecr.ap-northeast-2.amazonaws.com - ECR_REPOSITORY: snutt-prod/snutt-ev - ECR_BATCH_REPOSITORY: snutt-prod/snutt-ev-batch - + deploy-api: + uses: ./.github/workflows/_deploy.yml + with: + ocir_repository: snutt-prod/snutt-ev + dockerfile: api/Dockerfile + secrets: + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} + + deploy-batch: + uses: ./.github/workflows/_deploy.yml + with: + ocir_repository: snutt-prod/snutt-ev-batch + dockerfile: batch/Dockerfile + secrets: + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} + + notify: + needs: [deploy-api, deploy-batch] + runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ap-northeast-2 - - - name: Login to ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Get and save Auth Token for CodeArtifact - id: get-save-codeartifact-auth-token - run: | - aws codeartifact get-authorization-token --domain wafflestudio --domain-owner 405906814034 --query authorizationToken --region ap-northeast-1 --output text > .codeartifact-auth-token - - - name: Docker build, tag, and push image to ECR - id: build-image - run: | - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . --build-arg CODEARTIFACT_AUTH_TOKEN=$(cat .codeartifact-auth-token) - docker build -f Dockerfile-batch -t $ECR_REGISTRY/$ECR_BATCH_REPOSITORY:$IMAGE_TAG . --build-arg CODEARTIFACT_AUTH_TOKEN=$(cat .codeartifact-auth-token) - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - docker push $ECR_REGISTRY/$ECR_BATCH_REPOSITORY:$IMAGE_TAG - echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" - - name: Slack Notify uses: rtCamp/action-slack-notify@v2.3.3 env: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 068dc885..c1a69e16 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,12 +14,13 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Set up JDK 21 + - name: Set up JDK 25 uses: actions/setup-java@v4 with: - java-version: '21' + java-version: '25' distribution: 'temurin' - name: Run Lint run: | - ./gradlew clean ktlintCheck + ./gradlew clean ktlintCheck -x compileKotlin -x compileTestKotlin \ + -x compileJava -x processAot -x processTestAot diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 492651fc..2f03bf5a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,17 +14,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ap-northeast-2 - - - name: Set up JDK 21 + - name: Set up JDK 25 uses: actions/setup-java@v4 with: - java-version: '21' + java-version: '25' distribution: 'temurin' - name: Set up redis @@ -33,5 +26,7 @@ jobs: redis-version: 6 - name: Run Tests + env: + GITHUB_TOKEN: ${{ github.token }} run: | - ./gradlew clean test + ./gradlew clean test -x processAot -x processTestAot diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index a896fd60..00000000 --- a/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM eclipse-temurin:21-alpine -WORKDIR /app -ARG CODEARTIFACT_AUTH_TOKEN -COPY . /app -RUN ./gradlew :api:bootJar -PcodeArtifactAuthToken=$CODEARTIFACT_AUTH_TOKEN -EXPOSE 8080 -ENTRYPOINT java $JAVA_OPTS -jar api/build/libs/snuttev-api.jar diff --git a/Dockerfile-batch b/Dockerfile-batch deleted file mode 100644 index 654a3195..00000000 --- a/Dockerfile-batch +++ /dev/null @@ -1,6 +0,0 @@ -FROM eclipse-temurin:21-alpine -WORKDIR /app -ARG CODEARTIFACT_AUTH_TOKEN -COPY . /app -RUN ./gradlew :batch:bootJar -PcodeArtifactAuthToken=$CODEARTIFACT_AUTH_TOKEN -ENTRYPOINT java $JAVA_OPTS -jar batch/build/libs/snuttev-batch.jar --job.name=${JOB_NAME} diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 00000000..b3020101 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,7 @@ +FROM container-registry.oracle.com/graalvm/jdk:25 +WORKDIR /app +COPY . /app +RUN microdnf install -y findutils --nodocs +RUN --mount=type=secret,id=github_token GITHUB_TOKEN=$(cat /run/secrets/github_token) ./gradlew :api:bootJar +EXPOSE 8080 +ENTRYPOINT java $JAVA_OPTS -jar api/build/libs/snuttev-api.jar diff --git a/api/Dockerfile-native b/api/Dockerfile-native new file mode 100644 index 00000000..ac75859a --- /dev/null +++ b/api/Dockerfile-native @@ -0,0 +1,12 @@ +# Build stage +FROM container-registry.oracle.com/graalvm/native-image:25 AS builder +WORKDIR /app +COPY . /app +RUN --mount=type=secret,id=github_token GITHUB_TOKEN=$(cat /run/secrets/github_token) ./gradlew :api:nativeCompile + +# Runtime stage +FROM docker.io/debian:stable-slim +WORKDIR /app +COPY --from=builder /app/api/build/native/nativeCompile/api /app/api +EXPOSE 8080 +ENTRYPOINT ["/app/api"] diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 5231b3cd..f1caa42d 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -2,6 +2,15 @@ dependencies { implementation(project(":core")) implementation("org.springframework.boot:spring-boot-starter-validation") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") - implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9") + implementation("tools.jackson.module:jackson-module-kotlin") + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1") +} + +graalvmNative { + binaries { + named("main") { + buildArgs.add("--gc=G1") + buildArgs.add("-R:MaxRAMPercentage=60.0") + } + } } diff --git a/api/src/main/kotlin/com/wafflestudio/snuttev/DataLoader.kt b/api/src/main/kotlin/com/wafflestudio/snuttev/DataLoader.kt deleted file mode 100644 index 385166ef..00000000 --- a/api/src/main/kotlin/com/wafflestudio/snuttev/DataLoader.kt +++ /dev/null @@ -1,235 +0,0 @@ -package com.wafflestudio.snuttev - -import com.wafflestudio.snuttev.core.common.type.LectureClassification -import com.wafflestudio.snuttev.core.common.type.Semester -import com.wafflestudio.snuttev.core.domain.lecture.model.Lecture -import com.wafflestudio.snuttev.core.domain.lecture.model.SemesterLecture -import com.wafflestudio.snuttev.core.domain.lecture.repository.LectureRepository -import com.wafflestudio.snuttev.core.domain.lecture.repository.SemesterLectureRepository -import com.wafflestudio.snuttev.core.domain.tag.model.Tag -import com.wafflestudio.snuttev.core.domain.tag.model.TagGroup -import com.wafflestudio.snuttev.core.domain.tag.model.TagValueType -import com.wafflestudio.snuttev.core.domain.tag.repository.TagGroupRepository -import com.wafflestudio.snuttev.core.domain.tag.repository.TagRepository -import org.springframework.boot.ApplicationArguments -import org.springframework.boot.ApplicationRunner -import org.springframework.context.annotation.Profile -import org.springframework.stereotype.Component - -@Component -@Profile("local") -class DataLoader( - private val lectureRepository: LectureRepository, - private val semesterLectureRepository: SemesterLectureRepository, - private val tagGroupRepository: TagGroupRepository, - private val tagRepository: TagRepository, -) : ApplicationRunner { - override fun run(args: ApplicationArguments?) { - val lecture = - Lecture( - title = "소프트웨어 개발의 원리와 실습", - instructor = "전병곤", - department = "컴퓨터공학부", - courseNumber = "M1522.002400", - credit = 4, - academicYear = "3학년", - category = "", - classification = LectureClassification.ELECTIVE_SUBJECT, - ) - lectureRepository.save(lecture) - val semesterLectures = - listOf( - SemesterLecture( - lecture = lecture, - year = 2019, - semester = Semester.AUTUMN.value, - credit = 4, - academicYear = "3학년", - category = "", - classification = LectureClassification.ELECTIVE_SUBJECT, - ), - SemesterLecture( - lecture = lecture, - year = 2019, - semester = Semester.SPRING.value, - credit = 4, - academicYear = "3학년", - category = "", - classification = LectureClassification.ELECTIVE_SUBJECT, - ), - SemesterLecture( - lecture = lecture, - year = 2020, - semester = Semester.AUTUMN.value, - credit = 4, - academicYear = "3학년", - category = "", - classification = LectureClassification.ELECTIVE_SUBJECT, - ), - SemesterLecture( - lecture = lecture, - year = 2020, - semester = Semester.SPRING.value, - credit = 4, - academicYear = "3학년", - category = "", - classification = LectureClassification.ELECTIVE_SUBJECT, - ), - ) - semesterLectureRepository.saveAll(semesterLectures) - - val lecture2 = - Lecture( - title = "심리학개론", - instructor = "박형생", - department = "심리학과", - courseNumber = "045.012", - credit = 3, - academicYear = "1학년", - category = "인간과 사회", - classification = LectureClassification.LIBERAL_EDUCATION, - ) - lectureRepository.save(lecture2) - val semesterLectures2 = - listOf( - SemesterLecture( - lecture = lecture, - year = 2021, - semester = Semester.WINTER.value, - credit = 3, - academicYear = "1학년", - category = "인간과 사회", - classification = LectureClassification.LIBERAL_EDUCATION, - ), - SemesterLecture( - lecture = lecture, - year = 2021, - semester = Semester.AUTUMN.value, - credit = 3, - academicYear = "1학년", - category = "인간과 사회", - classification = LectureClassification.LIBERAL_EDUCATION, - ), - ) - semesterLectureRepository.saveAll(semesterLectures2) - - val mainTagGroup = - TagGroup( - name = "main", - ordering = -1, - color = null, - valueType = TagValueType.LOGIC, - ) - val academicYearTagGroup = - TagGroup( - name = "학년", - ordering = 1, - color = "#E54459", - valueType = TagValueType.STRING, - ) - val creditTagGroup = - TagGroup( - name = "학점", - ordering = 2, - color = "#A6D930", - valueType = TagValueType.INT, - ) - val tagGroups = - listOf( - mainTagGroup, - academicYearTagGroup, - creditTagGroup, - ) - tagGroupRepository.saveAll(tagGroups) - val tags = - listOf( - Tag( - tagGroup = mainTagGroup, - name = "최신", - description = "최근 등록된 강의평", - ordering = 1, - ), - Tag( - tagGroup = mainTagGroup, - name = "추천", - description = "학우들의 추천 강의", - ordering = 2, - ), - Tag( - tagGroup = mainTagGroup, - name = "명강", - description = "졸업하기 전에 꼭 한 번 들어볼 만한 강의", - ordering = 3, - ), - Tag( - tagGroup = mainTagGroup, - name = "꿀강", - description = "수업 부담이 크지 않고, 성적도 잘 주는 강의", - ordering = 4, - ), - Tag( - tagGroup = mainTagGroup, - name = "고진감래", - description = "공과 시간을 들인 만큼 거두는 것이 많은 강의", - ordering = 5, - ), - Tag( - tagGroup = academicYearTagGroup, - name = "1학년", - description = null, - ordering = 1, - stringValue = "1학년", - ), - Tag( - tagGroup = academicYearTagGroup, - name = "2학년", - description = null, - ordering = 2, - stringValue = "2학년", - ), - Tag( - tagGroup = academicYearTagGroup, - name = "3학년", - description = null, - ordering = 3, - stringValue = "3학년", - ), - Tag( - tagGroup = academicYearTagGroup, - name = "4학년", - description = null, - ordering = 4, - stringValue = "4학년", - ), - Tag( - tagGroup = creditTagGroup, - name = "1학점", - description = null, - ordering = 1, - intValue = 1, - ), - Tag( - tagGroup = creditTagGroup, - name = "2학점", - description = null, - ordering = 2, - intValue = 2, - ), - Tag( - tagGroup = creditTagGroup, - name = "3학점", - description = null, - ordering = 3, - intValue = 3, - ), - Tag( - tagGroup = creditTagGroup, - name = "4학점", - description = null, - ordering = 4, - intValue = 4, - ), - ) - tagRepository.saveAll(tags) - } -} diff --git a/api/src/main/kotlin/com/wafflestudio/snuttev/config/SwaggerConfig.kt b/api/src/main/kotlin/com/wafflestudio/snuttev/config/SwaggerConfig.kt index 27f7f0b7..c804893d 100644 --- a/api/src/main/kotlin/com/wafflestudio/snuttev/config/SwaggerConfig.kt +++ b/api/src/main/kotlin/com/wafflestudio/snuttev/config/SwaggerConfig.kt @@ -1,7 +1,5 @@ package com.wafflestudio.snuttev.config -import com.fasterxml.jackson.databind.ObjectMapper -import io.swagger.v3.core.jackson.ModelResolver import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.info.Info import org.springframework.context.annotation.Bean @@ -9,9 +7,6 @@ import org.springframework.context.annotation.Configuration @Configuration class SwaggerConfig { - @Bean - fun modelResolver(objectMapper: ObjectMapper): ModelResolver = ModelResolver(objectMapper) - @Bean fun openAPI(): OpenAPI = OpenAPI().info( diff --git a/api/src/main/kotlin/com/wafflestudio/snuttev/controller/LectureController.kt b/api/src/main/kotlin/com/wafflestudio/snuttev/controller/LectureController.kt index 11614717..2ad8ca1e 100644 --- a/api/src/main/kotlin/com/wafflestudio/snuttev/controller/LectureController.kt +++ b/api/src/main/kotlin/com/wafflestudio/snuttev/controller/LectureController.kt @@ -1,13 +1,12 @@ package com.wafflestudio.snuttev.controller -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue import com.wafflestudio.snuttev.core.common.dto.common.ListResponse import com.wafflestudio.snuttev.core.common.dto.common.PaginationResponse import com.wafflestudio.snuttev.core.common.error.ErrorResponse import com.wafflestudio.snuttev.core.domain.lecture.dto.EvLectureSummaryForSnutt import com.wafflestudio.snuttev.core.domain.lecture.dto.LectureAndSemesterLecturesResponse import com.wafflestudio.snuttev.core.domain.lecture.dto.LectureDto +import com.wafflestudio.snuttev.core.domain.lecture.dto.LectureIdListResponse import com.wafflestudio.snuttev.core.domain.lecture.dto.LectureIdResponse import com.wafflestudio.snuttev.core.domain.lecture.dto.LectureTakenByUserResponse import com.wafflestudio.snuttev.core.domain.lecture.dto.SearchLectureRequest @@ -27,6 +26,8 @@ import org.springframework.web.bind.annotation.RequestAttribute import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController +import tools.jackson.databind.ObjectMapper +import tools.jackson.module.kotlin.readValue @RestController class LectureController( @@ -206,5 +207,3 @@ class LectureController( ) } } - -private class LectureIdListResponse : ListResponse(listOf()) diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index a0f04744..8db6497e 100644 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -10,6 +10,10 @@ spring: property-naming-strategy: SNAKE_CASE default-property-inclusion: non_null +springdoc: + pre-loading-enabled: true + pre-loading-locales: ko-KR,en-US + server: tomcat: accesslog: diff --git a/batch/Dockerfile b/batch/Dockerfile new file mode 100644 index 00000000..dc152368 --- /dev/null +++ b/batch/Dockerfile @@ -0,0 +1,6 @@ +FROM container-registry.oracle.com/graalvm/jdk:25 +WORKDIR /app +COPY . /app +RUN microdnf install -y findutils --nodocs +RUN --mount=type=secret,id=github_token GITHUB_TOKEN=$(cat /run/secrets/github_token) ./gradlew :batch:bootJar +ENTRYPOINT java $JAVA_OPTS -jar batch/build/libs/snuttev-batch.jar --job.name=${JOB_NAME} diff --git a/batch/Dockerfile-native b/batch/Dockerfile-native new file mode 100644 index 00000000..6d00262f --- /dev/null +++ b/batch/Dockerfile-native @@ -0,0 +1,11 @@ +# Build stage +FROM container-registry.oracle.com/graalvm/native-image:25 AS builder +WORKDIR /app +COPY . /app +RUN --mount=type=secret,id=github_token GITHUB_TOKEN=$(cat /run/secrets/github_token) ./gradlew :batch:nativeCompile + +# Runtime stage +FROM docker.io/debian:stable-slim +WORKDIR /app +COPY --from=builder /app/batch/build/native/nativeCompile/batch /app/batch +ENTRYPOINT ["/app/batch", "--job.name=${JOB_NAME}"] diff --git a/batch/build.gradle.kts b/batch/build.gradle.kts index e9d15a02..f8699b6b 100644 --- a/batch/build.gradle.kts +++ b/batch/build.gradle.kts @@ -5,3 +5,12 @@ dependencies { runtimeOnly("org.postgresql:postgresql") runtimeOnly("com.h2database:h2") } + +graalvmNative { + binaries { + named("main") { + buildArgs.add("--gc=G1") + buildArgs.add("-R:MaxRAMPercentage=60.0") + } + } +} diff --git a/batch/src/main/kotlin/com/wafflestudio/snuttev/config/BatchConfig.kt b/batch/src/main/kotlin/com/wafflestudio/snuttev/config/BatchConfig.kt index 27c9294e..34a378fd 100644 --- a/batch/src/main/kotlin/com/wafflestudio/snuttev/config/BatchConfig.kt +++ b/batch/src/main/kotlin/com/wafflestudio/snuttev/config/BatchConfig.kt @@ -1,9 +1,8 @@ package com.wafflestudio.snuttev.config import com.zaxxer.hikari.HikariDataSource -import org.springframework.boot.autoconfigure.batch.BatchDataSource -import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.jdbc.autoconfigure.DataSourceProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Primary @@ -13,8 +12,7 @@ import javax.sql.DataSource @Configuration class BatchConfig { - @Bean - @BatchDataSource + @Bean("batchDataSource") fun batchDataSource(): DataSource = EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) @@ -23,7 +21,7 @@ class BatchConfig { .build() /** - * Mimic [org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration.Hikari] + * Mimic [org.springframework.boot.jdbc.autoconfigure.DataSourceConfiguration.Hikari] */ @Bean @Primary diff --git a/batch/src/main/kotlin/com/wafflestudio/snuttev/config/BatchNativeImageConfig.kt b/batch/src/main/kotlin/com/wafflestudio/snuttev/config/BatchNativeImageConfig.kt new file mode 100644 index 00000000..422eab73 --- /dev/null +++ b/batch/src/main/kotlin/com/wafflestudio/snuttev/config/BatchNativeImageConfig.kt @@ -0,0 +1,13 @@ +package com.wafflestudio.snuttev.config + +import com.wafflestudio.snuttev.snuev.model.SnuevEvaluation +import com.wafflestudio.snuttev.sync.model.SnuttSemesterLecture +import org.springframework.aot.hint.annotation.RegisterReflectionForBinding +import org.springframework.context.annotation.Configuration + +@Configuration +@RegisterReflectionForBinding( + SnuevEvaluation::class, + SnuttSemesterLecture::class, +) +class BatchNativeImageConfig diff --git a/batch/src/main/kotlin/com/wafflestudio/snuttev/snuev/SnuevMigrationJobConfig.kt b/batch/src/main/kotlin/com/wafflestudio/snuttev/snuev/SnuevMigrationJobConfig.kt index 0d3cc30c..41837edf 100644 --- a/batch/src/main/kotlin/com/wafflestudio/snuttev/snuev/SnuevMigrationJobConfig.kt +++ b/batch/src/main/kotlin/com/wafflestudio/snuttev/snuev/SnuevMigrationJobConfig.kt @@ -7,15 +7,15 @@ import com.wafflestudio.snuttev.core.domain.lecture.repository.LectureRepository import com.wafflestudio.snuttev.core.domain.lecture.repository.SemesterLectureRepository import com.wafflestudio.snuttev.snuev.model.SnuevEvaluation import jakarta.persistence.EntityManagerFactory -import org.springframework.batch.core.Job -import org.springframework.batch.core.Step +import org.springframework.batch.core.job.Job import org.springframework.batch.core.job.builder.JobBuilder import org.springframework.batch.core.repository.JobRepository +import org.springframework.batch.core.step.Step import org.springframework.batch.core.step.builder.StepBuilder -import org.springframework.batch.item.ItemProcessor -import org.springframework.batch.item.ItemWriter -import org.springframework.batch.item.database.JdbcCursorItemReader -import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder +import org.springframework.batch.infrastructure.item.ItemProcessor +import org.springframework.batch.infrastructure.item.ItemWriter +import org.springframework.batch.infrastructure.item.database.JdbcCursorItemReader +import org.springframework.batch.infrastructure.item.database.builder.JdbcCursorItemReaderBuilder import org.springframework.boot.jdbc.DataSourceBuilder import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -53,8 +53,8 @@ class SnuevMigrationJobConfig( fun customReaderStep(jobRepository: JobRepository): Step = StepBuilder(CUSTOM_READER_JOB_STEP, jobRepository) - .chunk( - CHUNK_SIZE, + .chunk(CHUNK_SIZE) + .transactionManager( JpaTransactionManager().apply { this.entityManagerFactory = this@SnuevMigrationJobConfig.entityManagerFactory }, diff --git a/batch/src/main/kotlin/com/wafflestudio/snuttev/sync/SnuttLectureSyncJobConfig.kt b/batch/src/main/kotlin/com/wafflestudio/snuttev/sync/SnuttLectureSyncJobConfig.kt index e392c8cf..b4872cbc 100644 --- a/batch/src/main/kotlin/com/wafflestudio/snuttev/sync/SnuttLectureSyncJobConfig.kt +++ b/batch/src/main/kotlin/com/wafflestudio/snuttev/sync/SnuttLectureSyncJobConfig.kt @@ -9,15 +9,15 @@ import com.wafflestudio.snuttev.core.domain.lecture.repository.SemesterLectureRe import com.wafflestudio.snuttev.core.domain.lecture.repository.SnuttLectureIdMapRepository import com.wafflestudio.snuttev.sync.model.SnuttSemesterLecture import jakarta.persistence.EntityManagerFactory -import org.springframework.batch.core.Job -import org.springframework.batch.core.Step +import org.springframework.batch.core.job.Job import org.springframework.batch.core.job.builder.JobBuilder import org.springframework.batch.core.repository.JobRepository +import org.springframework.batch.core.step.Step import org.springframework.batch.core.step.builder.StepBuilder -import org.springframework.batch.item.ItemProcessor -import org.springframework.batch.item.ItemWriter -import org.springframework.batch.item.data.MongoCursorItemReader -import org.springframework.batch.item.data.builder.MongoCursorItemReaderBuilder +import org.springframework.batch.infrastructure.item.ItemProcessor +import org.springframework.batch.infrastructure.item.ItemWriter +import org.springframework.batch.infrastructure.item.data.MongoCursorItemReader +import org.springframework.batch.infrastructure.item.data.builder.MongoCursorItemReaderBuilder import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Profile @@ -113,8 +113,8 @@ class SnuttLectureSyncJobConfig( query: Query, ): Step = StepBuilder(CUSTOM_READER_JOB_STEP, jobRepository) - .chunk( - CHUNK_SIZE, + .chunk(CHUNK_SIZE) + .transactionManager( JpaTransactionManager().apply { this.entityManagerFactory = this@SnuttLectureSyncJobConfig.entityManagerFactory }, diff --git a/batch/src/main/kotlin/com/wafflestudio/snuttev/sync/SnuttRatingSyncJobConfig.kt b/batch/src/main/kotlin/com/wafflestudio/snuttev/sync/SnuttRatingSyncJobConfig.kt index 0e1ac3aa..6b71c404 100644 --- a/batch/src/main/kotlin/com/wafflestudio/snuttev/sync/SnuttRatingSyncJobConfig.kt +++ b/batch/src/main/kotlin/com/wafflestudio/snuttev/sync/SnuttRatingSyncJobConfig.kt @@ -3,14 +3,14 @@ package com.wafflestudio.snuttev.sync import com.wafflestudio.snuttev.core.domain.lecture.model.SnuttLectureIdMap import com.wafflestudio.snuttev.core.domain.lecture.repository.LectureRepository import jakarta.persistence.EntityManagerFactory -import org.springframework.batch.core.Job -import org.springframework.batch.core.Step +import org.springframework.batch.core.job.Job import org.springframework.batch.core.job.builder.JobBuilder import org.springframework.batch.core.repository.JobRepository +import org.springframework.batch.core.step.Step import org.springframework.batch.core.step.builder.StepBuilder -import org.springframework.batch.item.ItemWriter -import org.springframework.batch.item.database.JpaPagingItemReader -import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder +import org.springframework.batch.infrastructure.item.ItemWriter +import org.springframework.batch.infrastructure.item.database.JpaPagingItemReader +import org.springframework.batch.infrastructure.item.database.builder.JpaPagingItemReaderBuilder import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Profile @@ -42,8 +42,8 @@ class SnuttRatingSyncJobConfig( private fun customReaderStep(jobRepository: JobRepository): Step = StepBuilder(CUSTOM_READER_JOB_STEP, jobRepository) - .chunk( - CHUNK_SIZE, + .chunk(CHUNK_SIZE) + .transactionManager( JpaTransactionManager().apply { this.entityManagerFactory = this@SnuttRatingSyncJobConfig.entityManagerFactory }, diff --git a/batch/src/main/resources/application.yml b/batch/src/main/resources/application.yml index 1c3d7fdb..c16ece4b 100644 --- a/batch/src/main/resources/application.yml +++ b/batch/src/main/resources/application.yml @@ -3,12 +3,11 @@ spring.profiles.default: local spring: config: import: application-core.yml - data: - mongodb: - host: localhost - port: 27017 - authentication-database: admin - database: snutt + mongodb: + host: localhost + port: 27017 + authentication-database: admin + database: snutt batch: job: name: ${job.name:EMPTY} @@ -20,21 +19,19 @@ spring: spring.config.activate.on-profile: dev spring: - data: - mongodb: - host: - port: - authentication-database: - database: + mongodb: + host: + port: + authentication-database: + database: --- spring.config.activate.on-profile: prod spring: - data: - mongodb: - host: - port: - authentication-database: - database: + mongodb: + host: + port: + authentication-database: + database: diff --git a/build.gradle.kts b/build.gradle.kts index 7468274c..1c111027 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,8 +4,11 @@ import org.jlleitschuh.gradle.ktlint.KtlintExtension import org.springframework.boot.gradle.tasks.bundling.BootJar plugins { - id("org.springframework.boot") version "3.5.4" apply false - kotlin("jvm") version "2.2.0" + id("org.graalvm.buildtools.native") version "0.11.3" apply false + id("org.springframework.boot") version "4.0.1" apply false + id("io.spring.dependency-management") version "1.1.7" + id("org.hibernate.orm") version "7.2.4.Final" apply false + kotlin("jvm") version "2.3.0" kotlin("plugin.spring") version "2.2.0" kotlin("plugin.allopen") version "2.2.0" kotlin("plugin.noarg") version "2.2.0" @@ -14,20 +17,35 @@ plugins { group = "com.wafflestudio" version = "1.0.0" -java.sourceCompatibility = JavaVersion.VERSION_21 +java.sourceCompatibility = JavaVersion.VERSION_25 allprojects { repositories { mavenCentral() - mavenCodeArtifact() + maven { + url = uri("https://maven.pkg.github.com/wafflestudio/spring-waffle") + credentials { + username = "wafflestudio" + password = findProperty("gpr.key") as String? + ?: System.getenv("GITHUB_TOKEN") + ?: runCatching { + ProcessBuilder("gh", "auth", "token") + .start() + .inputStream + .bufferedReader() + .readText() + .trim() + }.getOrDefault("") + } + } mavenLocal() } } subprojects { apply { + plugin("org.graalvm.buildtools.native") plugin("kotlin") - plugin("org.springframework.boot") plugin("org.jetbrains.kotlin.jvm") plugin("org.jetbrains.kotlin.plugin.jpa") plugin("org.jetbrains.kotlin.plugin.spring") @@ -36,19 +54,26 @@ subprojects { apply(plugin = "org.jlleitschuh.gradle.ktlint") } + dependencyManagement { + imports { + mavenBom("org.springframework.boot:spring-boot-dependencies:4.0.1") + } + } + dependencies { - api(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("tools.jackson.module:jackson-module-kotlin") + implementation("com.fasterxml.jackson.core:jackson-annotations:2.20") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-data-mongodb") - implementation("com.wafflestudio.spring:spring-boot-starter-waffle:1.0.4") + implementation("com.wafflestudio.spring:spring-boot-starter-waffle-oci-vault:2.1.0") + implementation("com.wafflestudio.spring.truffle:spring-boot-starter-truffle:2.1.0") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("com.h2database:h2") @@ -61,7 +86,15 @@ subprojects { tasks.withType { compilerOptions { freeCompilerArgs.add("-Xjsr305=strict") - jvmTarget.set(JvmTarget.JVM_21) + jvmTarget.set(JvmTarget.JVM_25) + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 + toolchain { + languageVersion.set(JavaLanguageVersion.of(25)) } } @@ -76,47 +109,15 @@ subprojects { } project(":api") { - val bootJar: BootJar by tasks + apply(plugin = "org.springframework.boot") + val bootJar: BootJar by tasks bootJar.archiveFileName.set("snuttev-api.jar") } project(":batch") { - val bootJar: BootJar by tasks + apply(plugin = "org.springframework.boot") - bootJar.archiveFileName.set("snuttev-batch.jar") -} - -project(":core") { - val jar: Jar by tasks val bootJar: BootJar by tasks - - jar.enabled = true - bootJar.enabled = false -} - -fun RepositoryHandler.mavenCodeArtifact() { - maven { - val authToken = - properties["codeArtifactAuthToken"] as String? ?: ProcessBuilder( - "aws", - "codeartifact", - "get-authorization-token", - "--domain", - "wafflestudio", - "--domain-owner", - "405906814034", - "--query", - "authorizationToken", - "--region", - "ap-northeast-1", - "--output", - "text", - ).start().inputStream.bufferedReader().readText().trim() - url = uri("https://wafflestudio-405906814034.d.codeartifact.ap-northeast-1.amazonaws.com/maven/spring-waffle/") - credentials { - username = "aws" - password = authToken - } - } + bootJar.archiveFileName.set("snuttev-batch.jar") } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 922a94d1..9ed84a19 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,5 +1,13 @@ plugins { kotlin("kapt") + id("org.hibernate.orm") apply false +} + +if (gradle.startParameter.taskNames.none { it.contains("ktlint", ignoreCase = true) }) { + apply(plugin = "org.hibernate.orm") + configure { + enhancement { enableLazyInitialization = true } + } } allOpen { @@ -13,11 +21,8 @@ noArg { dependencies { implementation("com.querydsl:querydsl-jpa::jakarta") - implementation("org.flywaydb:flyway-core:11.13.2") - implementation("org.flywaydb:flyway-mysql:11.13.2") - implementation("software.amazon.awssdk:secretsmanager:2.34.6") - implementation("software.amazon.awssdk:sts:2.34.6") - + implementation("org.springframework.boot:spring-boot-starter-flyway") + implementation("org.flywaydb:flyway-mysql") implementation("org.springframework.boot:spring-boot-starter-data-redis") runtimeOnly("com.mysql:mysql-connector-j") diff --git a/core/src/main/kotlin/com/wafflestudio/snuttev/core/common/dto/common/PaginationResponse.kt b/core/src/main/kotlin/com/wafflestudio/snuttev/core/common/dto/common/PaginationResponse.kt index 0a6b2807..7a15aba3 100644 --- a/core/src/main/kotlin/com/wafflestudio/snuttev/core/common/dto/common/PaginationResponse.kt +++ b/core/src/main/kotlin/com/wafflestudio/snuttev/core/common/dto/common/PaginationResponse.kt @@ -2,7 +2,7 @@ package com.wafflestudio.snuttev.core.common.dto.common import org.springframework.data.domain.Page -data class PaginationResponse( +data class PaginationResponse( val content: List, val page: Int, val size: Int, diff --git a/core/src/main/kotlin/com/wafflestudio/snuttev/core/common/util/PageUtils.kt b/core/src/main/kotlin/com/wafflestudio/snuttev/core/common/util/PageUtils.kt index 62b638f1..1db10331 100644 --- a/core/src/main/kotlin/com/wafflestudio/snuttev/core/common/util/PageUtils.kt +++ b/core/src/main/kotlin/com/wafflestudio/snuttev/core/common/util/PageUtils.kt @@ -1,8 +1,8 @@ package com.wafflestudio.snuttev.core.common.util -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue import org.springframework.stereotype.Component +import tools.jackson.module.kotlin.jacksonObjectMapper +import tools.jackson.module.kotlin.readValue import java.util.Base64 import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec diff --git a/core/src/main/kotlin/com/wafflestudio/snuttev/core/common/util/cache/Cache.kt b/core/src/main/kotlin/com/wafflestudio/snuttev/core/common/util/cache/Cache.kt index 18d4c367..493ecbf8 100644 --- a/core/src/main/kotlin/com/wafflestudio/snuttev/core/common/util/cache/Cache.kt +++ b/core/src/main/kotlin/com/wafflestudio/snuttev/core/common/util/cache/Cache.kt @@ -1,18 +1,18 @@ package com.wafflestudio.snuttev.core.common.util.cache -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.data.redis.core.StringRedisTemplate import org.springframework.stereotype.Component +import tools.jackson.databind.ObjectMapper +import tools.jackson.module.kotlin.readValue import java.time.Duration @Component internal final class Cache( private val redisTemplate: StringRedisTemplate, - @Value("\${spring.data.redis.default-ttl}") private val defaultTtl: Duration, + @param:Value("\${spring.data.redis.default-ttl}") private val defaultTtl: Duration, private val objectMapper: ObjectMapper, ) { private val log: Logger get() = LoggerFactory.getLogger(Cache::class.java) diff --git a/core/src/main/kotlin/com/wafflestudio/snuttev/core/config/NativeImageConfig.kt b/core/src/main/kotlin/com/wafflestudio/snuttev/core/config/NativeImageConfig.kt new file mode 100644 index 00000000..4bccb71a --- /dev/null +++ b/core/src/main/kotlin/com/wafflestudio/snuttev/core/config/NativeImageConfig.kt @@ -0,0 +1,55 @@ +package com.wafflestudio.snuttev.core.config + +import com.wafflestudio.snuttev.core.common.dto.common.CursorPaginationResponse +import com.wafflestudio.snuttev.core.common.dto.common.ListResponse +import com.wafflestudio.snuttev.core.common.dto.common.PaginationResponse +import com.wafflestudio.snuttev.core.common.error.ErrorResponse +import com.wafflestudio.snuttev.core.domain.evaluation.dto.EvaluationCursor +import com.wafflestudio.snuttev.core.domain.evaluation.dto.EvaluationReportDto +import com.wafflestudio.snuttev.core.domain.evaluation.dto.EvaluationWithLectureDto +import com.wafflestudio.snuttev.core.domain.evaluation.dto.EvaluationWithLectureResponse +import com.wafflestudio.snuttev.core.domain.evaluation.dto.EvaluationWithSemesterDto +import com.wafflestudio.snuttev.core.domain.evaluation.dto.EvaluationsResponse +import com.wafflestudio.snuttev.core.domain.evaluation.dto.LectureEvaluationDto +import com.wafflestudio.snuttev.core.domain.evaluation.dto.LectureEvaluationSummaryResponse +import com.wafflestudio.snuttev.core.domain.lecture.dto.EvLectureSummaryForSnutt +import com.wafflestudio.snuttev.core.domain.lecture.dto.LectureAndSemesterLecturesResponse +import com.wafflestudio.snuttev.core.domain.lecture.dto.LectureDto +import com.wafflestudio.snuttev.core.domain.lecture.dto.LectureIdListResponse +import com.wafflestudio.snuttev.core.domain.lecture.dto.LectureIdResponse +import com.wafflestudio.snuttev.core.domain.lecture.dto.LectureTakenByUserResponse +import com.wafflestudio.snuttev.core.domain.lecture.dto.SnuttLectureInfo +import com.wafflestudio.snuttev.core.domain.lecture.model.LectureEvaluationSummaryDao +import com.wafflestudio.snuttev.core.domain.lecture.model.LectureRatingDao +import com.wafflestudio.snuttev.core.domain.tag.dto.SearchTagResponse +import com.wafflestudio.snuttev.core.domain.tag.dto.TagGroupDto +import org.springframework.aot.hint.annotation.RegisterReflectionForBinding +import org.springframework.context.annotation.Configuration + +@Configuration +@RegisterReflectionForBinding( + ErrorResponse::class, + ListResponse::class, + PaginationResponse::class, + CursorPaginationResponse::class, + EvaluationCursor::class, + EvaluationWithSemesterDto::class, + EvaluationWithLectureDto::class, + LectureEvaluationDto::class, + LectureEvaluationSummaryResponse::class, + EvaluationWithLectureResponse::class, + EvaluationsResponse::class, + EvaluationReportDto::class, + LectureDto::class, + LectureIdResponse::class, + LectureIdListResponse::class, + LectureAndSemesterLecturesResponse::class, + LectureTakenByUserResponse::class, + EvLectureSummaryForSnutt::class, + SnuttLectureInfo::class, + TagGroupDto::class, + SearchTagResponse::class, + LectureEvaluationSummaryDao::class, + LectureRatingDao::class, +) +class NativeImageConfig diff --git a/core/src/main/kotlin/com/wafflestudio/snuttev/core/config/RedisConfig.kt b/core/src/main/kotlin/com/wafflestudio/snuttev/core/config/RedisConfig.kt index 2a348d91..e48138d7 100644 --- a/core/src/main/kotlin/com/wafflestudio/snuttev/core/config/RedisConfig.kt +++ b/core/src/main/kotlin/com/wafflestudio/snuttev/core/config/RedisConfig.kt @@ -8,24 +8,32 @@ import org.springframework.context.annotation.Configuration import org.springframework.data.redis.cache.RedisCacheConfiguration import org.springframework.data.redis.cache.RedisCacheManager import org.springframework.data.redis.connection.RedisConnectionFactory -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer +import org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer import org.springframework.data.redis.serializer.RedisSerializationContext import org.springframework.data.redis.serializer.StringRedisSerializer +import tools.jackson.databind.ObjectMapper import java.time.Duration @Configuration @EnableCaching class RedisConfig( - @Value("\${spring.data.redis.default-ttl}") private val redisTtl: Duration, + @param:Value("\${spring.data.redis.default-ttl}") private val redisTtl: Duration, ) { @Bean - fun cacheManager(connectionFactory: RedisConnectionFactory): CacheManager { + fun cacheManager( + connectionFactory: RedisConnectionFactory, + objectMapper: ObjectMapper, + ): CacheManager { val redisCacheConfiguration = RedisCacheConfiguration .defaultCacheConfig() .entryTtl(redisTtl) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer())) - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer( + GenericJacksonJsonRedisSerializer(objectMapper), + ), + ) return RedisCacheManager.RedisCacheManagerBuilder .fromConnectionFactory(connectionFactory) .cacheDefaults(redisCacheConfiguration) diff --git a/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/dto/LectureResponse.kt b/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/dto/LectureResponse.kt index 3b5055f2..91c2e788 100644 --- a/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/dto/LectureResponse.kt +++ b/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/dto/LectureResponse.kt @@ -1,9 +1,10 @@ package com.wafflestudio.snuttev.core.domain.lecture.dto -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming +import com.wafflestudio.snuttev.core.common.dto.common.ListResponse import com.wafflestudio.snuttev.core.common.type.LectureClassification import com.wafflestudio.snuttev.core.domain.evaluation.dto.SemesterLectureDto +import tools.jackson.databind.PropertyNamingStrategies +import tools.jackson.databind.annotation.JsonNaming data class LectureDto( val id: Long, @@ -64,3 +65,5 @@ data class EvLectureSummaryForSnutt( val avgRating: Double?, val evaluationCount: Long, ) + +class LectureIdListResponse : ListResponse(listOf()) diff --git a/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/repository/LectureRepository.kt b/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/repository/LectureRepository.kt index b6b5cdc8..d574c30c 100644 --- a/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/repository/LectureRepository.kt +++ b/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/repository/LectureRepository.kt @@ -7,7 +7,7 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query interface LectureRepository : - JpaRepository, + JpaRepository, LectureRepositoryCustom { fun findByCourseNumberAndInstructor( courseNumber: String, diff --git a/core/src/main/resources/META-INF/native-image/kotlin-compat/reflect-config.json b/core/src/main/resources/META-INF/native-image/kotlin-compat/reflect-config.json new file mode 100644 index 00000000..7ea3d649 --- /dev/null +++ b/core/src/main/resources/META-INF/native-image/kotlin-compat/reflect-config.json @@ -0,0 +1,20 @@ +[ + { + "name": "kotlin.collections.EmptyList", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "kotlin.collections.EmptySet", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "kotlin.collections.EmptyMap", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + } +] diff --git a/core/src/main/resources/META-INF/native-image/org.glassfish.jersey.inject/jersey-hk2/reflect-config.json b/core/src/main/resources/META-INF/native-image/org.glassfish.jersey.inject/jersey-hk2/reflect-config.json new file mode 100644 index 00000000..0f7742dd --- /dev/null +++ b/core/src/main/resources/META-INF/native-image/org.glassfish.jersey.inject/jersey-hk2/reflect-config.json @@ -0,0 +1,80 @@ +[ + { + "name": "org.glassfish.jersey.inject.hk2.JerseyClassAnalyzer", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "org.glassfish.jersey.inject.hk2.JerseyClassAnalyzer$Binder", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "org.glassfish.hk2.internal.PerThreadContext", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "org.glassfish.hk2.osgiresourcelocator.ServiceLoader", + "methods": [{ "name":"lookupProviderClasses", "parameterTypes":["java.lang.Class"] }] + }, + { + "name":"org.glassfish.jersey.inject.hk2.ContextInjectionResolverImpl", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"org.glassfish.jersey.inject.hk2.Hk2InjectionManagerFactory", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"org.glassfish.jersey.inject.hk2.Hk2RequestScope", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"org.glassfish.jersey.inject.hk2.InstanceSupplierFactoryBridge", + "methods":[{"name":"provide","parameterTypes":[] }] + }, + { + "name":"org.glassfish.jersey.inject.hk2.JerseyErrorService", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"org.glassfish.jersey.inject.hk2.RequestContext", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"org.jvnet.hk2.internal.ProxyUtilities$4", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"org.jvnet.hk2.internal.ProxyUtilities", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"org.jvnet.hk2.internal.DynamicConfigurationServiceImpl", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"org.jvnet.hk2.internal.ServiceLocatorRuntimeImpl", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + } +] diff --git a/core/src/main/resources/application-core.yml b/core/src/main/resources/application-core.yml index 4496e304..16663dda 100644 --- a/core/src/main/resources/application-core.yml +++ b/core/src/main/resources/application-core.yml @@ -5,7 +5,6 @@ spring: virtual: enabled: true --- - spring: config: activate: @@ -27,10 +26,11 @@ spring: logging: config: classpath:logback/local.xml -secret-names: dev/snutt-ev +oci: + vault: + secret-ids: ocid1.vaultsecret.oc1.ap-chuncheon-1.amaaaaaat2m5lbqafa7scbk7gagxd5gpvdsk276l557wyntv7tcwqeviumha --- - spring.config.activate.on-profile: test spring: @@ -51,10 +51,11 @@ spring: ddl-auto: none --- - spring.config.activate.on-profile: dev -secret-names: dev/snutt-ev +oci: + vault: + secret-ids: ocid1.vaultsecret.oc1.ap-chuncheon-1.amaaaaaat2m5lbqafa7scbk7gagxd5gpvdsk276l557wyntv7tcwqeviumha spring: datasource: @@ -68,10 +69,11 @@ spring: default-ttl: 5m --- - spring.config.activate.on-profile: prod -secret-names: prod/snutt-ev +oci: + vault: + secret-ids: ocid1.vaultsecret.oc1.ap-chuncheon-1.amaaaaaat2m5lbqacwytts3muhrtzpf44mq75rvycqdowudm27z433s6htma spring: datasource: diff --git a/core/src/test/kotlin/com/wafflestudio/snuttev/core/domain/evaluation/service/EvaluationServiceTest.kt b/core/src/test/kotlin/com/wafflestudio/snuttev/core/domain/evaluation/service/EvaluationServiceTest.kt index 8f1ddd23..c62d12ce 100644 --- a/core/src/test/kotlin/com/wafflestudio/snuttev/core/domain/evaluation/service/EvaluationServiceTest.kt +++ b/core/src/test/kotlin/com/wafflestudio/snuttev/core/domain/evaluation/service/EvaluationServiceTest.kt @@ -22,8 +22,8 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.data.repository.findByIdOrNull +import org.springframework.test.context.bean.override.mockito.MockitoBean import org.springframework.transaction.annotation.Transactional import kotlin.random.Random import kotlin.random.nextInt @@ -38,7 +38,7 @@ class EvaluationServiceTest private val lectureRepository: LectureRepository, private val semesterLectureRepository: SemesterLectureRepository, private val evaluationLikeRepository: EvaluationLikeRepository, - @MockBean private val mongoService: MongoService, + @MockitoBean private val mongoService: MongoService, ) { @BeforeEach fun setup() { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a79..8bdaf60c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 35a1b01f..37f78a6a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index aeb74cbb..ef07e016 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,7 +85,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -111,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -130,10 +133,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -141,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -149,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -198,16 +204,16 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 6689b85b..5eed7ee8 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,22 +59,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell