diff --git a/dangle-api/src/main/kotlin/com/dangle/api/common/error/DangleApiExceptionControllerAdvice.kt b/dangle-api/src/main/kotlin/com/dangle/api/common/error/DangleApiExceptionControllerAdvice.kt new file mode 100644 index 0000000..d9cf140 --- /dev/null +++ b/dangle-api/src/main/kotlin/com/dangle/api/common/error/DangleApiExceptionControllerAdvice.kt @@ -0,0 +1,65 @@ +package com.dangle.api.common.error + +import com.dangle.api.common.phase.ActiveProfilesResolver +import com.dangle.api.common.response.ApiResponse +import com.dangle.common.DangleErrorCode +import com.dangle.common.DangleException +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.http.HttpStatusCode +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice + +@RestControllerAdvice +class DangleApiExceptionControllerAdvice( + private val activeProfilesResolver: ActiveProfilesResolver +) { + + private val logger = LoggerFactory.getLogger(this::class.java) + + @ExceptionHandler(DangleException::class) + fun handleDangleException(e: DangleException): ResponseEntity>{ + return ResponseEntity( + ApiResponse.error( + errorCode = e.errorCode, + error = e.toError(), + debug = e.toDebug() + ), + e.errorCode.httpStatusCode() + ) + } + + // 별도로 Exception 을 처리할 것들은 이앞에서 다시 처리할 것 + @ExceptionHandler(Throwable::class) + fun handleUnKnownException(e: Throwable): ResponseEntity>{ + logger.error(e.stackTraceToString()) + return handleDangleException( + DangleException( + errorCode = DangleErrorCode.INTERNAL_SERVER_ERROR, + debug = e.message, + throwable = e + ) + ) + } + + private fun DangleErrorCode.httpStatusCode(): HttpStatusCode{ + return when (value) { + in 4000 until 4100 -> HttpStatus.BAD_REQUEST + in 4100 until 4200 -> HttpStatus.NOT_FOUND + in 5000 until 5100 -> HttpStatus.INTERNAL_SERVER_ERROR + else -> HttpStatus.INTERNAL_SERVER_ERROR + } + } + + + private fun DangleException.toError(): String { + return this.errorCode.name + } + private fun DangleException.toDebug(): String?{ + if(activeProfilesResolver.isPrd()){ + return null + } + return this.stackTraceToString() + } +} \ No newline at end of file diff --git a/dangle-api/src/main/kotlin/com/dangle/api/common/error/ErrorResponse.kt b/dangle-api/src/main/kotlin/com/dangle/api/common/error/ErrorResponse.kt new file mode 100644 index 0000000..3e64c0a --- /dev/null +++ b/dangle-api/src/main/kotlin/com/dangle/api/common/error/ErrorResponse.kt @@ -0,0 +1,16 @@ +package com.dangle.api.common.error + +import com.dangle.common.DangleErrorCode + +data class ErrorResponse( + val code: Int, + val message: String, + val data: String? = null +){ + constructor(errorCode: DangleErrorCode,data:String?):this( + code = errorCode.value, + message = errorCode.message, + data = data + + ) +} \ No newline at end of file diff --git a/dangle-api/src/main/kotlin/com/dangle/api/common/phase/ActiveProfilesResolver.kt b/dangle-api/src/main/kotlin/com/dangle/api/common/phase/ActiveProfilesResolver.kt new file mode 100644 index 0000000..109d678 --- /dev/null +++ b/dangle-api/src/main/kotlin/com/dangle/api/common/phase/ActiveProfilesResolver.kt @@ -0,0 +1,33 @@ +package com.dangle.api.common.phase + +import org.springframework.core.env.Environment +import org.springframework.stereotype.Component + +@Component +class ActiveProfilesResolver( + env: Environment +){ + + private val activeProfilePhases = ActiveProfilePhase.values().map { it.phase }.toSet() + + private val currentProfiles = env.activeProfiles + .filter { it in activeProfilePhases } + .associateBy{ it } + .ifEmpty { mapOf(ActiveProfilePhase.LOCAL.phase to ActiveProfilePhase.LOCAL) } + fun isPrd() : Boolean{ + return currentProfiles[ActiveProfilePhase.PRD.phase] != null + } + + fun isDev() : Boolean{ + return currentProfiles[ActiveProfilePhase.DEV.phase] != null + } + + fun isLocal(): Boolean{ + return currentProfiles[ActiveProfilePhase.LOCAL.phase] != null + } + private enum class ActiveProfilePhase(val phase: String) { + LOCAL("local"), + DEV("dev"), + PRD("prd"), + } +} \ No newline at end of file diff --git a/dangle-api/src/main/kotlin/com/dangle/api/common/response/ApiResponse.kt b/dangle-api/src/main/kotlin/com/dangle/api/common/response/ApiResponse.kt new file mode 100644 index 0000000..b4cacd5 --- /dev/null +++ b/dangle-api/src/main/kotlin/com/dangle/api/common/response/ApiResponse.kt @@ -0,0 +1,47 @@ +package com.dangle.api.common.response + +import com.dangle.api.common.error.ErrorResponse +import com.dangle.common.DangleErrorCode + +class ApiResponse private constructor( + val result: ResponseType, + val data: T? = null, + val error: ErrorResponse? = null, + val debug: String? = null +){ + + companion object{ + fun success(result: T): ApiResponse{ + return ApiResponse( + result = ResponseType.SUCCESS, + data = result, + ) + } + + fun success(result: List) : ApiResponse>{ + return ApiResponse( + result = ResponseType.SUCCESS, + data = result + ) + } + + fun error(errorCode: DangleErrorCode,error: String?, debug:String? ):ApiResponse{ + return ApiResponse( + result = ResponseType.ERROR, + data = null, + error = ErrorResponse( + errorCode = errorCode, + data = error, + ), + debug = debug + ) + } + } + enum class ResponseType{ + SUCCESS,ERROR + } + + override fun toString(): String { + return "ApiResponse(result=$result, data=$data, error=$error, debug=$debug)" + } +} \ No newline at end of file diff --git a/dangle-api/src/main/kotlin/com/dangle/api/v1/shelter/ShelterController.kt b/dangle-api/src/main/kotlin/com/dangle/api/v1/shelter/ShelterController.kt index 06db605..bc7be4d 100644 --- a/dangle-api/src/main/kotlin/com/dangle/api/v1/shelter/ShelterController.kt +++ b/dangle-api/src/main/kotlin/com/dangle/api/v1/shelter/ShelterController.kt @@ -2,14 +2,17 @@ package com.dangle.api.v1.shelter import com.dangle.api.common.resolver.VolunteerAuthentication import com.dangle.api.common.resolver.VolunteerAuthenticationInfo +import com.dangle.api.common.response.ApiResponse import com.dangle.usecase.shelter.port.`in`.command.ToggleBookmarkCommandUseCase import com.dangle.usecase.shelter.port.`in`.query.GetShelterQueryUseCase import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController +@RequestMapping("/v1/shelter") class ShelterController( private val getShelterQueryUseCase: GetShelterQueryUseCase, private val toggleBookmarkCommandUseCase: ToggleBookmarkCommandUseCase, @@ -18,8 +21,8 @@ class ShelterController( fun getShelterInfo( @PathVariable shelterId: Long, @VolunteerAuthentication volunteerInfo: VolunteerAuthenticationInfo?, - ): GetShelterResponse { - return getShelterQueryUseCase.invoke(shelterId).let { + ): ApiResponse { + val response = getShelterQueryUseCase.invoke(shelterId).let { GetShelterResponse( id = it.id, name = it.name, @@ -37,14 +40,16 @@ class ShelterController( bookMarked = false, // TODO(kang) 북마크 처리 필요 ) } + + return ApiResponse.success(response) } @PostMapping("/{shelterId}/bookmark") fun bookmarkShelter( @PathVariable shelterId: Long, @VolunteerAuthentication volunteerInfo: VolunteerAuthenticationInfo - ): BookMarkShelterResponse { - return toggleBookmarkCommandUseCase.invoke( + ): ApiResponse { + val response = toggleBookmarkCommandUseCase.invoke( ToggleBookmarkCommandUseCase.Command( shelterId = shelterId, volunteerId = volunteerInfo.volunteerId, @@ -56,6 +61,8 @@ class ShelterController( bookMarked = it.bookMarked, ) } + + return ApiResponse.success(response) } data class GetShelterResponse( diff --git a/dangle-api/src/main/resources/application.yml b/dangle-api/src/main/resources/application.yml index 585703b..da04b16 100644 --- a/dangle-api/src/main/resources/application.yml +++ b/dangle-api/src/main/resources/application.yml @@ -12,6 +12,7 @@ spring: mvc: pathmatch: matching-strategy: ant_path_matcher + throw-exception-if-no-handler-found: true jackson: property-naming-strategy: SNAKE_CASE transaction: diff --git a/dangle-common/src/main/kotlin/com/dangle/common/DangleErrorCode.kt b/dangle-common/src/main/kotlin/com/dangle/common/DangleErrorCode.kt index 60e3bfe..a13e7f4 100644 --- a/dangle-common/src/main/kotlin/com/dangle/common/DangleErrorCode.kt +++ b/dangle-common/src/main/kotlin/com/dangle/common/DangleErrorCode.kt @@ -4,7 +4,12 @@ enum class DangleErrorCode( val value: Int, val message: String, ) { - // 4xxx. Not Found Error - NOT_FOUND(4000, "잘못된 요청이에요."), - NOT_FOUND_SHELTER(4001, "보호소를 찾을 수 없어요."), + // 40xx. Bad Request + BAD_REQUEST(4000, "잘못된 요청이에요."), + // 41xx. Not Found Error + NOT_FOUND(4100, "정보를 찾을 수 없어요."), + NOT_FOUND_SHELTER(4101, "보호소를 찾을 수 없어요."), + + // 5xxx. Internal Server Error + INTERNAL_SERVER_ERROR(5000,"시스템 내부 에러가 발생했어요.") } diff --git a/dangle-common/src/main/kotlin/com/dangle/common/DangleException.kt b/dangle-common/src/main/kotlin/com/dangle/common/DangleException.kt index c328bfd..1be5057 100644 --- a/dangle-common/src/main/kotlin/com/dangle/common/DangleException.kt +++ b/dangle-common/src/main/kotlin/com/dangle/common/DangleException.kt @@ -1,7 +1,7 @@ package com.dangle.common class DangleException( - errorCode: DangleErrorCode, - debug: String? = null, - throwable: Throwable? = null, + val errorCode: DangleErrorCode, + val debug: String? = null, + val throwable: Throwable? = null, ) : RuntimeException("$errorCode${debug?.let { " - $it" }}", throwable)