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/CI.yml b/.github/workflows/CI.yml index a96161b..680093b 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,42 +1,35 @@ -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: (Set Up) Grant Execute permission for gradlew - run: chmod 777 gradlew + - name: Update Git submodules + run: git submodule update --remote --recursive - - 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/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 -}