diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6c94357..1a0e6de 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,16 +1,19 @@ ## PULL REQUEST -### πŸŽ‹ μž‘μ—… 쀑인 브랜치 +### πŸŽ‹ Issue Ticket - ### πŸ”‘ μ£Όμš” μž‘μ—…μ‚¬ν•­ - ### 🏞 (Optional) μ°Έκ³  자료 -- μŠ€ν¬λ¦°μƒ·μ„ μ²¨λΆ€ν•΄μ£Όμ„Έμš”. - -### κ΄€λ ¨ 이슈 - +### (μ€‘μš”) μ„œλΈŒλͺ¨λ“ˆμ΄ μˆ˜μ •λ˜μ—ˆλ‚˜μš”? +- κΈ°μ‘΄ 컀밋 : +- λ³€κ²½ 컀밋 : +- λ³€κ²½ 사항 : +- [] ν•΄λ‹Ή μ„œλΈŒλͺ¨λ“ˆ 변경사항이 PR에 잘 λ°˜μ˜λ˜μ—ˆλ‚˜μš”? + ### κΌ­ 확인해 μ£Όμ„Έμš”!! -- +- diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 1269951..87a4b52 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -5,6 +5,11 @@ on: branches: - master +permissions: + contents: read + actions: read + id-token: write + jobs: deploy: runs-on: ubuntu-latest @@ -15,59 +20,48 @@ jobs: with: token: ${{ secrets.GIT_TOKEN }} submodules: true - - name: Setup Java 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'adopt' - - name: Update Git submodules run: git submodule update --remote --recursive - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Build with Gradle - run: ./gradlew clean build --debug - - - name: Get current time - uses: 1466587594/get-current-time@v2 - id: current-time + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 with: - format: YYYY-MM-DDTHH-mm-ss - utcOffset: "+09:00" - - - name: Show Current Time - run: echo "CurrentTime=${{steps.current-time.outputs.formattedTime}}" - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + role-to-assume: arn:aws:iam::125183404358:role/VacgomGithubActionAssumeRole aws-region: ap-northeast-2 - - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 - + - name: Invoke Gradle + uses: gradle/gradle-build-action@v2 + - name: Grant execute permission for gradlew + run: chmod +x gradlew - name: Build, tag, and push image to Amazon ECR + env: + PROFILE: dev + IMAGE_REPO_URL: ${{ steps.login-ecr.outputs.registry }}/vacgom + IMAGE_TAG: ${{ github.sha }} + run: ./gradlew jib --parallel + - name: Download Task Definition run: | - docker build -t vacgom:${{steps.current-time.outputs.formattedTime}} . - docker tag vacgom:${{steps.current-time.outputs.formattedTime}} ${{ secrets.ECR_URI }}:${{steps.current-time.outputs.formattedTime}} - docker push ${{ secrets.ECR_URI }}:${{steps.current-time.outputs.formattedTime}} - - - name: SSH into EC2 instance - uses: appleboy/ssh-action@master + aws ecs describe-task-definition \ + --task-definition vacgom-taskdef \ + --query taskDefinition \ + > task-definition.json + - name: Update Task Definition + id: task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: task-definition.json + container-name: backend + image: ${{ steps.login-ecr.outputs.registry }}/vacgom:${{ github.sha }} + - name: Deploy Amazon ECS task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@v1 with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USERNAME }} - key: ${{ secrets.EC2_PRIVATE_KEY }} - port: ${{ secrets.EC2_SSH_PORT }} - script: | - aws ecr get-login-password | docker login --username AWS --password-stdin ${{ secrets.ECR_URI }} - docker pull ${{ secrets.ECR_URI }}:${{ steps.current-time.outputs.formattedTime }} - docker ps -f name=vacgom-api -q | xargs --no-run-if-empty docker container stop - docker ps -a -f name=vacgom-api -q | xargs --no-run-if-empty docker container rm - docker run -d --name vacgom-api -p 80:8080 ${{ secrets.ECR_URI }}:${{ steps.current-time.outputs.formattedTime }} + task-definition: ${{ steps.task-def.outputs.task-definition }} + service: vacgom-best-service + cluster: vacgom-cluster + wait-for-service-stability: true diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a96161b..230894e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,42 +1,38 @@ -name: λΉŒλ“œ ν…ŒμŠ€νŠΈ / 리뷰어 ν• λ‹Ή +name: ν…ŒμŠ€νŠΈ μ½”λ“œ 및 λΉŒλ“œ on: pull_request: - types: [opened, synchronize, closed] + types: [ opened, synchronize, closed ] branches: - 'develop' - 'master' jobs: - build_and_review_assign: - name: "[CI] Check Build/Testcases and Assign Reviewer" + test: + name: "[CI] Check Tests" runs-on: ubuntu-latest steps: - - name: (Set Up) checkout + - name: Checkout code uses: actions/checkout@v3 with: token: ${{ secrets.GIT_TOKEN }} submodules: true - - name: (Set Up) Set up JDK 17 + - name: Setup Java 17 uses: actions/setup-java@v3 with: java-version: '17' - distribution: 'temurin' + distribution: 'adopt' + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 - name: (Set Up) Grant Execute permission for gradlew run: chmod 777 gradlew - - name: (Build) Build with Gradle - id: build - run: ./gradlew test -i + - name: Grant execute permission for gradlew + run: chmod +x gradlew - - name: (Assign Reviewer) - if: steps.build.outcome == 'success' - uses: hkusu/review-assign-action@v1 - with: - assignees: ${{ github.actor }} - reviewers: HyungJu, h-beeen - ready-comment: 'μ½”λ“œ 리뷰 μš”μ²­ν•©λ‹ˆλ‹€ πŸ™† ' - merged-comment: 'μ„±κ³΅μ μœΌλ‘œ Merge λ˜μ—ˆμŠ΅λ‹ˆλ‹€. Shout out to :wink:' + - name: Test with Gradle + run: ./gradlew test diff --git a/build.gradle.kts b/build.gradle.kts index ca64428..94a4a9f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,13 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +val activeProfile = System.getenv("PROFILE") ?: "dev" +val imageTag = System.getenv("IMAGE_TAG") ?: "latest" +val repoURL: String? = System.getenv("IMAGE_REPO_URL") + plugins { id("org.springframework.boot") version "3.2.3" id("io.spring.dependency-management") version "1.1.4" + id("com.google.cloud.tools.jib") version "3.4.1" kotlin("jvm") version "1.9.22" kotlin("plugin.spring") version "1.9.22" kotlin("plugin.jpa") version "1.9.22" @@ -56,3 +61,21 @@ tasks.register("initConfig") { include("*.yml") into("./src/main/resources") } + +jib { + from { + image = "amazoncorretto:17-alpine3.18" + } + to { + image = repoURL + tags = setOf(imageTag) + } + container { + jvmFlags = listOf( + "-Dspring.profiles.active=${activeProfile}", + "-Dserver.port=8080", + "-XX:+UseContainerSupport", + ) + ports = listOf("8080") + } +} diff --git a/src/main/kotlin/com/vacgom/backend/domain/member/Member.kt b/src/main/kotlin/com/vacgom/backend/domain/member/Member.kt new file mode 100644 index 0000000..de0538c --- /dev/null +++ b/src/main/kotlin/com/vacgom/backend/domain/member/Member.kt @@ -0,0 +1,21 @@ +package com.vacgom.backend.domain.member + +import com.vacgom.backend.global.auditing.BaseEntity +import jakarta.persistence.* + +@Entity +@Table(name = "t_member") +class Member(nickname: String) : BaseEntity() { + + @Id + @Column(name = "member_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null + + @Column(name = "nickname") + val nickname: String + + init { + this.nickname = nickname + } +} diff --git a/src/main/kotlin/com/vacgom/backend/global/exception/ApiExceptionHandler.kt b/src/main/kotlin/com/vacgom/backend/global/exception/ApiExceptionHandler.kt new file mode 100644 index 0000000..36f6c8a --- /dev/null +++ b/src/main/kotlin/com/vacgom/backend/global/exception/ApiExceptionHandler.kt @@ -0,0 +1,55 @@ +package com.vacgom.backend.global.exception + +import com.vacgom.backend.global.exception.error.BusinessException +import com.vacgom.backend.global.exception.error.ErrorCode +import com.vacgom.backend.global.exception.error.ErrorResponse +import com.vacgom.backend.global.exception.error.GlobalError +import com.vacgom.backend.global.logger.CommonLogger +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.MissingServletRequestParameterException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestControllerAdvice + +@RestControllerAdvice +class ApiExceptionHandler { + companion object : CommonLogger(); + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MethodArgumentNotValidException::class) + fun handleMethodArgumentNotValidException(ex: MethodArgumentNotValidException): ErrorResponse? { + return ErrorResponse(ex.fieldErrors) + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MissingServletRequestParameterException::class) + fun missingServletRequestParameterException(): ErrorResponse? { + return ErrorResponse(GlobalError.INVALID_REQUEST_PARAM) + } + + @ExceptionHandler(BusinessException::class) + fun handleBusinessException( + exception: BusinessException + ): ResponseEntity { + logBusinessException(exception) + return convert(exception.errorCode) + } + + private fun convert( + errorCode: ErrorCode + ): ResponseEntity { + return ResponseEntity + .status(errorCode.status) + .body(ErrorResponse(errorCode)) + } + + private fun logBusinessException(exception: BusinessException) { + if (exception.errorCode.status.is5xxServerError) { + log.error("", exception) + } else { + log.error("Error Message = {}", exception.message) + } + } +} diff --git a/src/main/kotlin/com/vacgom/backend/global/exception/ApiExceptionHandlingFilter.kt b/src/main/kotlin/com/vacgom/backend/global/exception/ApiExceptionHandlingFilter.kt new file mode 100644 index 0000000..37cb8e1 --- /dev/null +++ b/src/main/kotlin/com/vacgom/backend/global/exception/ApiExceptionHandlingFilter.kt @@ -0,0 +1,45 @@ +package com.vacgom.backend.global.exception + +import com.fasterxml.jackson.databind.ObjectMapper +import com.vacgom.backend.global.exception.error.BusinessException +import com.vacgom.backend.global.exception.error.ErrorResponse +import jakarta.servlet.FilterChain +import jakarta.servlet.ServletException +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import java.io.IOException + + +@Component +class ApiExceptionHandlingFilter( + private val om: ObjectMapper +) : OncePerRequestFilter() { + + @Throws(ServletException::class, IOException::class) + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + chain: FilterChain + ) { + try { + chain.doFilter(request, response) + } catch (exception: BusinessException) { + setErrorResponse(response, exception) + } + } + + private fun setErrorResponse( + response: HttpServletResponse, + exception: BusinessException + ) = try { + response.contentType = MediaType.APPLICATION_JSON_VALUE + response.status = HttpStatus.UNAUTHORIZED.value() + om.writeValue(response.outputStream, ErrorResponse(exception.errorCode)) + } catch (exception: IOException) { + throw RuntimeException(exception) + } +} diff --git a/src/main/kotlin/com/vacgom/backend/global/exception/error/BusinessException.kt b/src/main/kotlin/com/vacgom/backend/global/exception/error/BusinessException.kt new file mode 100644 index 0000000..638ee2b --- /dev/null +++ b/src/main/kotlin/com/vacgom/backend/global/exception/error/BusinessException.kt @@ -0,0 +1,6 @@ +package com.vacgom.backend.global.exception.error + + +class BusinessException( + val errorCode: ErrorCode +) : RuntimeException(errorCode.message) diff --git a/src/main/kotlin/com/vacgom/backend/global/exception/error/ErrorCode.kt b/src/main/kotlin/com/vacgom/backend/global/exception/error/ErrorCode.kt new file mode 100644 index 0000000..550d1dc --- /dev/null +++ b/src/main/kotlin/com/vacgom/backend/global/exception/error/ErrorCode.kt @@ -0,0 +1,9 @@ +package com.vacgom.backend.global.exception.error + +import org.springframework.http.HttpStatus + +interface ErrorCode { + val message: String + val status: HttpStatus + val code: String +} diff --git a/src/main/kotlin/com/vacgom/backend/global/exception/error/ErrorResponse.kt b/src/main/kotlin/com/vacgom/backend/global/exception/error/ErrorResponse.kt new file mode 100644 index 0000000..4b7b4a0 --- /dev/null +++ b/src/main/kotlin/com/vacgom/backend/global/exception/error/ErrorResponse.kt @@ -0,0 +1,41 @@ +package com.vacgom.backend.global.exception.error + +import org.springframework.validation.FieldError +import java.time.LocalDateTime + +data class ErrorResponse( + val timeStamp: String = LocalDateTime.now().toString(), + val errorCode: String, + val errorMessage: String, + val details: Any? = null +) { + constructor( + errorCode: ErrorCode + ) : this( + errorCode = errorCode.code, + errorMessage = errorCode.message + ) + + constructor( + errorCode: ErrorCode, + details: Any? + ) : this( + errorCode = errorCode.code, + errorMessage = errorCode.message, + details = details + ) + + constructor( + fieldError: FieldError? + ) : this( + errorCode = fieldError?.code ?: "", + errorMessage = fieldError?.defaultMessage ?: "" + ) + + constructor(fieldErrors: List) : this( + GlobalError.INVALID_REQUEST_PARAM, + fieldErrors.associate { + it.field to (it.defaultMessage ?: "null") + } + ) +} diff --git a/src/main/kotlin/com/vacgom/backend/global/exception/error/GlobalError.kt b/src/main/kotlin/com/vacgom/backend/global/exception/error/GlobalError.kt new file mode 100644 index 0000000..432f9c5 --- /dev/null +++ b/src/main/kotlin/com/vacgom/backend/global/exception/error/GlobalError.kt @@ -0,0 +1,12 @@ +package com.vacgom.backend.global.exception.error + +import org.springframework.http.HttpStatus + +enum class GlobalError( + override val message: String, + override val status: HttpStatus, + override val code: String +) : ErrorCode { + GLOBAL_NOT_FOUND("λ¦¬μ†ŒμŠ€κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", HttpStatus.NOT_FOUND, "G_001"), + INVALID_REQUEST_PARAM("μš”μ²­ νŒŒλΌλ―Έν„°κ°€ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", HttpStatus.BAD_REQUEST, "G_002"); +} diff --git a/src/main/kotlin/com/vacgom/backend/global/logger/CommonLogger.kt b/src/main/kotlin/com/vacgom/backend/global/logger/CommonLogger.kt new file mode 100644 index 0000000..0d8e1c9 --- /dev/null +++ b/src/main/kotlin/com/vacgom/backend/global/logger/CommonLogger.kt @@ -0,0 +1,7 @@ +package com.vacgom.backend.global.logger + +import org.slf4j.LoggerFactory + +abstract class CommonLogger { + val log = LoggerFactory.getLogger(this.javaClass)!! +} diff --git a/src/main/kotlin/com/vacgom/backend/member/domain/Member.kt b/src/main/kotlin/com/vacgom/backend/member/domain/Member.kt deleted file mode 100644 index f0f339e..0000000 --- a/src/main/kotlin/com/vacgom/backend/member/domain/Member.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.vacgom.backend.member.domain - -import com.vacgom.backend.global.auditing.BaseEntity -import jakarta.persistence.* - -@Entity -@Table(name = "t_user") -class User( - val nickname: String -) : BaseEntity() { - - @Id - @Column(name = "user_id") - @GeneratedValue(strategy = GenerationType.IDENTITY) - var id: Long? = null -}