Merge pull request #27 from beaniejoy/feature/26

애플리케이션 응답에 대한 전체 규격화 (ApplicationResponse 적용)
This commit is contained in:
Hanbin Lee
2022-12-31 19:49:57 +09:00
committed by GitHub
32 changed files with 285 additions and 144 deletions

View File

@@ -1,11 +0,0 @@
package io.beaniejoy.dongnecafe.common.security.model
import com.fasterxml.jackson.annotation.JsonInclude
import org.springframework.security.core.GrantedAuthority
data class AuthenticationResult(
val email: String,
@JsonInclude(JsonInclude.Include.NON_EMPTY)
val authorities: Collection<GrantedAuthority> = listOf(),
val msg: String
)

View File

@@ -1,5 +1,6 @@
package io.beaniejoy.dongnecafe.controller
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
import io.beaniejoy.dongnecafe.security.JwtTokenUtils
import io.beaniejoy.dongnecafe.domain.member.model.request.SignInRequest
import io.beaniejoy.dongnecafe.model.TokenResponse
@@ -16,7 +17,7 @@ class AuthController(
private val jwtTokenUtils: JwtTokenUtils
) {
@PostMapping("/authenticate")
fun signIn(@RequestBody signInRequest: SignInRequest): TokenResponse {
fun signIn(@RequestBody signInRequest: SignInRequest): ApplicationResponse<TokenResponse> {
val authentication = authService.signIn(
email = signInRequest.email,
password = signInRequest.password
@@ -24,6 +25,8 @@ class AuthController(
val accessToken = jwtTokenUtils.createToken(authentication)
return TokenResponse(accessToken)
return ApplicationResponse
.success("success authenticate")
.data(TokenResponse(accessToken))
}
}

View File

@@ -1,5 +1,6 @@
package io.beaniejoy.dongnecafe.controller
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
import io.beaniejoy.dongnecafe.domain.member.model.request.MemberRegisterRequest
import io.beaniejoy.dongnecafe.service.MemberService
import org.springframework.web.bind.annotation.PostMapping
@@ -13,7 +14,11 @@ class MemberController(
private val memberService: MemberService
) {
@PostMapping("/sign-up")
fun signUp(@RequestBody resource: MemberRegisterRequest): Long {
return memberService.registerMember(resource)
fun signUp(@RequestBody resource: MemberRegisterRequest): ApplicationResponse<Long> {
val registerMemberId = memberService.registerMember(resource)
return ApplicationResponse
.success("success sign up")
.data(registerMemberId)
}
}

View File

@@ -1,3 +0,0 @@
package io.beaniejoy.dongnecafe.error
class MemberExistedException(email: String): RuntimeException("Member[$email] is already existed")

View File

@@ -1,3 +0,0 @@
package io.beaniejoy.dongnecafe.error
class MemberDeactivatedException(email: String): RuntimeException("Member[$email] is deactivated")

View File

@@ -1,27 +0,0 @@
package io.beaniejoy.dongnecafe.error.handler
import io.beaniejoy.dongnecafe.error.ErrorResponse
import mu.KLogging
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.core.AuthenticationException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
// TODO 통합된 에러 핸들링 필요(ErrorResponse 규격화)
@RestControllerAdvice
class CommonControllerAdvice {
companion object : KLogging()
@ExceptionHandler(AuthenticationException::class)
fun handleAuthenticationException(e: AuthenticationException): ResponseEntity<ErrorResponse> {
logger.error { "AuthenticationException: ${e.message}" }
return ResponseEntity.ok().body(
ErrorResponse(
code = HttpStatus.BAD_REQUEST.value(),
message = "계정 혹은 비밀번호가 일치하지 않습니다."
)
)
}
}

View File

@@ -1,9 +1,10 @@
package io.beaniejoy.dongnecafe.common.security
package io.beaniejoy.dongnecafe.security
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import io.beaniejoy.dongnecafe.security.SecurityUser
import mu.KLogging
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.userdetails.UserDetailsService
@@ -29,7 +30,7 @@ class ApiAuthenticationProvider(
val user = userDetailsService.loadUserByUsername(email) as SecurityUser
if (!passwordEncoder.matches(password, user.password)) {
throw BadCredentialsException("Input password does not match stored password")
throw BusinessException(ErrorCode.AUTH_PASSWORD_NOT_VALID)
}
logger.info { "User password ${user.password}" }

View File

@@ -1,13 +1,13 @@
package io.beaniejoy.dongnecafe.common.security
package io.beaniejoy.dongnecafe.security
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import io.beaniejoy.dongnecafe.domain.member.entity.Member
import io.beaniejoy.dongnecafe.domain.member.repository.MemberRepository
import io.beaniejoy.dongnecafe.error.MemberDeactivatedException
import io.beaniejoy.dongnecafe.security.SecurityUser
import mu.KLogging
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional
@@ -22,12 +22,12 @@ class UserDetailsServiceImpl(
return memberRepository.findByEmail(email)?.let {
logger.info { "[LOAD MEMBER] email: ${it.email}, role: ${it.roleType}, activated: ${it.activated}" }
createSecurityUser(it)
} ?: throw UsernameNotFoundException("${email} is not found")
} ?: throw BusinessException(ErrorCode.AUTH_MEMBER_NOT_FOUND)
}
private fun createSecurityUser(member: Member): SecurityUser {
if (member.activated.not()) {
throw MemberDeactivatedException(member.email)
throw BusinessException(ErrorCode.AUTH_MEMBER_DEACTIVATED)
}
return SecurityUser(

View File

@@ -1,9 +1,10 @@
package io.beaniejoy.dongnecafe.service
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import io.beaniejoy.dongnecafe.domain.member.entity.Member
import io.beaniejoy.dongnecafe.domain.member.model.request.MemberRegisterRequest
import io.beaniejoy.dongnecafe.domain.member.repository.MemberRepository
import io.beaniejoy.dongnecafe.error.MemberExistedException
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@@ -16,7 +17,7 @@ class MemberService(
) {
fun registerMember(resource: MemberRegisterRequest): Long {
memberRepository.findByEmail(resource.email!!)?.also {
throw MemberExistedException(resource.email!!)
throw BusinessException(ErrorCode.MEMBER_EXISTED)
}
val registeredMember = memberRepository.save(

View File

@@ -0,0 +1,38 @@
package io.beaniejoy.dongnecafe.common.error.advice
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
import mu.KLogging
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestControllerAdvice
@RestControllerAdvice
class BasicControllerAdvice {
companion object : KLogging()
/**
* 예외 상황 (500 시스템 오류 처리)
* @param e Exception
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception::class)
fun handleException(e: Exception): ApplicationResponse<Nothing> {
logger.error { "[COMMON][${e.javaClass.simpleName}] $e" }
return ApplicationResponse.fail(errorCode = ErrorCode.COMMON_SERVER_ERROR).build()
}
/**
* 비즈니스 로직 상 에러 처리(예상 가능한 예외 처리)
* @param e BusinessException
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(BusinessException::class)
fun handleBusinessException(e: BusinessException): ApplicationResponse<Nothing> {
logger.error { "[${BusinessException::class.simpleName}] <ErrorCode>: ${e.errorCode.name}, <ErrorMessage>: ${e.message}" }
return ApplicationResponse.fail(errorCode = e.errorCode).build()
}
}

View File

@@ -0,0 +1,13 @@
package io.beaniejoy.dongnecafe.common.error.constant
enum class Domain {
COMMON,
AUTH,
MEMBER,
CAFE,
CAFE_MENU,
MENU_OPTION,
OPTION_DETAIL
}

View File

@@ -0,0 +1,35 @@
package io.beaniejoy.dongnecafe.common.error.constant
import io.beaniejoy.dongnecafe.common.error.constant.Domain.*
import io.beaniejoy.dongnecafe.common.error.constant.SubCategory.*
enum class ErrorCode(
val domain: Domain,
val subCategory: SubCategory
) {
// COMMON
COMMON_SERVER_ERROR(COMMON, SERVER_ERROR),
// AUTH(security 관련)
AUTH_MEMBER_NOT_FOUND(AUTH, INVALID_AUTHENTICATE_REQUEST),
AUTH_PASSWORD_NOT_VALID(AUTH, INVALID_AUTHENTICATE_REQUEST),
AUTH_MEMBER_DEACTIVATED(AUTH, DEACTIVATED),
// MEMBER
MEMBER_EXISTED(MEMBER, EXISTED),
// CAFE
CAFE_NOT_FOUND(CAFE, NOT_FOUND),
CAFE_EXISTED(CAFE, EXISTED),
// CAFE_MENU
CAFE_MENU_NOT_FOUND(CAFE_MENU, NOT_FOUND),
// MENU_OPTION
MENU_OPTION_NOT_FOUND(MENU_OPTION, NOT_FOUND),
// OPTION_DETAIL
OPTION_DETAIL_NOT_FOUND(OPTION_DETAIL, NOT_FOUND)
;
}

View File

@@ -0,0 +1,9 @@
package io.beaniejoy.dongnecafe.common.error.constant
enum class SubCategory {
SERVER_ERROR,
INVALID_AUTHENTICATE_REQUEST,
NOT_FOUND,
EXISTED,
DEACTIVATED
}

View File

@@ -0,0 +1,30 @@
package io.beaniejoy.dongnecafe.common.error.exception
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
/*
* Business Logic 상 발생 가능한 Exception
* - 로직상 개발자가 예측 가능한 예외
* - front 측면에서 해당 에러에 대해서 error code(4xx, 5xx)보다 success code(2xx)를 응답받게 설계
* - front에서 해당 예외 응답에 대해서 ErrorResponse의 Result field로 따로 구분해서 처리가능
*/
class BusinessException : RuntimeException {
var errorCode: ErrorCode
private set
constructor(errorCode: ErrorCode) : super(errorCode.name){
this.errorCode = errorCode
}
constructor(errorCode: ErrorCode, message: String): super(message) {
this.errorCode = errorCode
}
constructor(errorCode: ErrorCode, cause: Throwable) : super(cause) {
this.errorCode = errorCode
}
constructor(errorCode: ErrorCode, message: String, cause: Throwable) : super(message, cause) {
this.errorCode = errorCode
}
}

View File

@@ -0,0 +1,49 @@
package io.beaniejoy.dongnecafe.common.response
import com.fasterxml.jackson.annotation.JsonInclude
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
@JsonInclude(JsonInclude.Include.NON_NULL)
class ApplicationResponse<T>(
val result: ResultCode,
val message: String? = null,
val errorCode: String? = null,
val data: T? = null
) {
companion object {
fun success(message: String? = null): ApplicationResponseBuilder {
return ApplicationResponseBuilder(
result = ResultCode.SUCCESS,
message = message
)
}
fun fail(errorCode: ErrorCode, message: String? = null): ApplicationResponseBuilder {
return ApplicationResponseBuilder(
result = ResultCode.FAIL,
message = message,
errorCode = errorCode.name
)
}
}
}
class ApplicationResponseBuilder(
var result: ResultCode,
var message: String? = null,
var errorCode: String? = null
) {
fun build(): ApplicationResponse<Nothing> {
return data(null)
}
fun <T> data(data: T?): ApplicationResponse<T> {
return ApplicationResponse(
result = this.result,
message = this.message,
data = data,
errorCode = errorCode
)
}
}

View File

@@ -0,0 +1,6 @@
package io.beaniejoy.dongnecafe.common.response
enum class ResultCode {
SUCCESS,
FAIL;
}

View File

@@ -1,6 +0,0 @@
package io.beaniejoy.dongnecafe.error
data class ErrorResponse(
val code: Int,
val message: String?
)

View File

@@ -6,10 +6,10 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.SecurityFilterChain
@Configuration
@@ -22,9 +22,15 @@ class SecurityConfig {
fun filterChain(http: HttpSecurity): SecurityFilterChain {
return http
.csrf().disable()
.formLogin().disable()
// FIXME 임시 permitAll 설정
.authorizeRequests()
.anyRequest().authenticated()
.anyRequest().permitAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 방식(세션 불필요)
.and()
.also { jwtAuthenticationConfigurer(it) }

View File

@@ -1,5 +1,6 @@
package io.beaniejoy.dongnecafe.domain.cafe.controller
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeRegisterRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeUpdateRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.response.CafeDetailedInfo
@@ -20,14 +21,18 @@ class CafeController(
* 신규 카페 생성
*/
@PostMapping
fun createNewCafe(@RequestBody resource: CafeRegisterRequest): Long {
return cafeService.createNew(
fun createNewCafe(@RequestBody resource: CafeRegisterRequest): ApplicationResponse<Long> {
val newCafeId = cafeService.createNew(
name = resource.name!!,
address = resource.address!!,
phoneNumber = resource.phoneNumber!!,
description = resource.description!!,
cafeMenuRequestList = resource.cafeMenuList
)
return ApplicationResponse
.success("OK")
.data(newCafeId)
}
/**
@@ -36,16 +41,24 @@ class CafeController(
@GetMapping
fun searchCafeList(
@PageableDefault(sort = ["name"], direction = Sort.Direction.ASC, page = 0, size = 10) pageable: Pageable
): Page<CafeSearchInfo> {
return cafeService.searchCafeList(pageable)
): ApplicationResponse<Page<CafeSearchInfo>> {
val searchCafes = cafeService.searchCafeList(pageable)
return ApplicationResponse
.success()
.data(searchCafes)
}
/**
* 단일 카페 상세 조회
*/
@GetMapping("/{id}")
fun getDetailedInfo(@PathVariable("id") id: Long): CafeDetailedInfo {
return cafeService.getDetailedInfoByCafeId(id)
fun getDetailedInfo(@PathVariable("id") id: Long): ApplicationResponse<CafeDetailedInfo> {
val cafeDetailedInfo = cafeService.getDetailedInfoByCafeId(id)
return ApplicationResponse
.success()
.data(cafeDetailedInfo)
}
/**
@@ -56,7 +69,7 @@ class CafeController(
fun updateInfo(
@PathVariable("id") id: Long,
@RequestBody resource: CafeUpdateRequest
): String {
): ApplicationResponse<Nothing> {
cafeService.updateInfo(
id = id,
name = resource.name!!,
@@ -65,6 +78,8 @@ class CafeController(
description = resource.description!!
)
return "Successfully Cafe[$id] Info Updated"
return ApplicationResponse
.success("Successfully Cafe[$id] Info Updated")
.build()
}
}

View File

@@ -1,16 +1,11 @@
package io.beaniejoy.dongnecafe.domain.cafe.controller
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuBulkDeleteRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuUpdateRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.response.CafeMenuDetailedInfo
import io.beaniejoy.dongnecafe.domain.cafe.service.CafeMenuService
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/cafes/{cafeId}/menus")
@@ -24,11 +19,15 @@ class CafeMenuController(
fun getDetailedInfo(
@PathVariable("cafeId") cafeId: Long,
@PathVariable("menuId") menuId: Long
): CafeMenuDetailedInfo {
return cafeMenuService.getDetailedInfoByMenuId(
): ApplicationResponse<CafeMenuDetailedInfo> {
val cafeMenuDetailedInfo = cafeMenuService.getDetailedInfoByMenuId(
menuId = menuId,
cafeId = cafeId
)
return ApplicationResponse
.success()
.data(cafeMenuDetailedInfo)
}
/**
@@ -42,14 +41,16 @@ class CafeMenuController(
@PathVariable("cafeId") cafeId: Long,
@PathVariable("menuId") menuId: Long,
@RequestBody cafeMenuUpdateRequest: CafeMenuUpdateRequest
): String {
): ApplicationResponse<Nothing> {
cafeMenuService.updateInfoAndBulkUpdate(
menuId = menuId,
cafeId = cafeId,
resource = cafeMenuUpdateRequest
)
return "Success Update Cafe[$cafeId]'s CafeMenu[$menuId]"
return ApplicationResponse
.success("Success Update Cafe[$cafeId]'s CafeMenu[$menuId]")
.build()
}
/**
@@ -59,13 +60,15 @@ class CafeMenuController(
fun delete(
@PathVariable("cafeId") cafeId: Long,
@PathVariable("menuId") menuId: Long
): String {
): ApplicationResponse<Nothing> {
cafeMenuService.deleteByCafeMenuId(
menuId = menuId,
cafeId = cafeId
)
return "Success Delete Cafe[$cafeId]'s CafeMenu[$menuId]"
return ApplicationResponse
.success("Success Delete Cafe[$cafeId]'s CafeMenu[$menuId]")
.build()
}
/**
@@ -75,9 +78,11 @@ class CafeMenuController(
fun bulkDelete(
@PathVariable("cafeId") cafeId: Long,
@RequestBody resource: CafeMenuBulkDeleteRequest
): String {
): ApplicationResponse<Nothing> {
cafeMenuService.bulkDelete(cafeId, resource.cafeMenuIdList)
return "Success Delete Cafe[$cafeId]'s CafeMenu List"
return ApplicationResponse
.success("Success Delete Cafe[$cafeId]'s CafeMenu List")
.build()
}
}

View File

@@ -1,8 +1,9 @@
package io.beaniejoy.dongnecafe.domain.cafe.service
import io.beaniejoy.dongnecafe.domain.cafe.model.response.CafeMenuDetailedInfo
import io.beaniejoy.dongnecafe.error.exception.CafeMenuNotFoundException
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuUpdateRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.response.CafeMenuDetailedInfo
import io.beaniejoy.dongnecafe.domain.cafe.repository.CafeMenuRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
@@ -20,14 +21,14 @@ class CafeMenuService(
@Transactional(readOnly = true)
fun getDetailedInfoByMenuId(menuId: Long, cafeId: Long): CafeMenuDetailedInfo {
val cafeMenu = cafeMenuRepository.findByIdOrNull(menuId)
?: throw CafeMenuNotFoundException(menuId = menuId, cafeId = cafeId)
?: throw BusinessException(ErrorCode.CAFE_MENU_NOT_FOUND)
return CafeMenuDetailedInfo.of(cafeMenu)
}
fun updateInfoAndBulkUpdate(menuId: Long, cafeId: Long, resource: CafeMenuUpdateRequest) {
val cafeMenu = cafeMenuRepository.findByIdOrNull(menuId)
?: throw CafeMenuNotFoundException(menuId = menuId, cafeId = cafeId)
?: throw BusinessException(ErrorCode.CAFE_MENU_NOT_FOUND)
cafeMenu.updateInfo(name = resource.name!!, price = resource.price)
@@ -36,7 +37,7 @@ class CafeMenuService(
fun deleteByCafeMenuId(menuId: Long, cafeId: Long) {
val cafeMenu = cafeMenuRepository.findByIdOrNull(menuId)
?: throw CafeMenuNotFoundException(menuId = menuId, cafeId = cafeId)
?: throw BusinessException(ErrorCode.CAFE_MENU_NOT_FOUND)
cafeMenuRepository.delete(cafeMenu)
}

View File

@@ -1,12 +1,12 @@
package io.beaniejoy.dongnecafe.domain.cafe.service
import io.beaniejoy.dongnecafe.domain.cafe.entity.Cafe
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuRegisterRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.response.CafeDetailedInfo
import io.beaniejoy.dongnecafe.domain.cafe.model.response.CafeSearchInfo
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuRegisterRequest
import io.beaniejoy.dongnecafe.domain.cafe.entity.Cafe
import io.beaniejoy.dongnecafe.error.exception.CafeExistedException
import io.beaniejoy.dongnecafe.error.exception.CafeNotFoundException
import io.beaniejoy.dongnecafe.domain.cafe.repository.CafeRepository
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import mu.KLogging
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
@@ -55,7 +55,7 @@ class CafeService(
private fun checkCafeExistedByName(name: String) {
val findCafe = cafeRepository.findByName(name)
if (findCafe != null) {
throw CafeExistedException(name)
throw BusinessException(ErrorCode.CAFE_EXISTED)
}
}
@@ -67,7 +67,7 @@ class CafeService(
fun getDetailedInfoByCafeId(id: Long): CafeDetailedInfo {
val cafe = cafeRepository.findByIdOrNull(id)
?: throw CafeNotFoundException(id)
?: throw BusinessException(ErrorCode.CAFE_NOT_FOUND)
return CafeDetailedInfo.of(cafe)
}
@@ -85,7 +85,7 @@ class CafeService(
description: String,
) {
val cafe = cafeRepository.findByIdOrNull(id)
?: throw CafeNotFoundException(id)
?: throw BusinessException(ErrorCode.CAFE_NOT_FOUND)
cafe.updateInfo(
name = name,

View File

@@ -1,6 +1,7 @@
package io.beaniejoy.dongnecafe.domain.cafe.service
import io.beaniejoy.dongnecafe.error.exception.MenuOptionNotFoundException
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import io.beaniejoy.dongnecafe.domain.cafe.model.request.MenuOptionUpdateRequest
import io.beaniejoy.dongnecafe.domain.cafe.repository.MenuOptionRepository
import org.springframework.data.repository.findByIdOrNull
@@ -16,7 +17,7 @@ class MenuOptionService(
fun bulkUpdate(resources: List<MenuOptionUpdateRequest>) {
resources.forEach {
val menuOption = menuOptionRepository.findByIdOrNull(it.menuOptionId)
?: throw MenuOptionNotFoundException(it.menuOptionId)
?: throw BusinessException(ErrorCode.MENU_OPTION_NOT_FOUND)
if (it.isDelete) {
menuOptionRepository.delete(menuOption)

View File

@@ -1,6 +1,7 @@
package io.beaniejoy.dongnecafe.domain.cafe.service
import io.beaniejoy.dongnecafe.error.exception.OptionDetailNotFoundException
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import io.beaniejoy.dongnecafe.domain.cafe.model.request.OptionDetailUpdateRequest
import io.beaniejoy.dongnecafe.domain.cafe.repository.OptionDetailRepository
import org.springframework.data.repository.findByIdOrNull
@@ -15,7 +16,7 @@ class OptionDetailService(
fun bulkUpdate(resources: List<OptionDetailUpdateRequest>) {
resources.forEach {
val optionDetail = optionDetailRepository.findByIdOrNull(it.optionDetailId)
?: throw OptionDetailNotFoundException(it.optionDetailId)
?: throw BusinessException(ErrorCode.OPTION_DETAIL_NOT_FOUND)
if (it.isDelete) {
optionDetailRepository.delete(optionDetail)

View File

@@ -1,15 +0,0 @@
package io.beaniejoy.dongnecafe.error
import io.beaniejoy.dongnecafe.error.exception.CafeNotFoundException
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
@RestControllerAdvice
class CafeExceptionHandler {
// TODO: error 규격화
@ExceptionHandler(CafeNotFoundException::class)
fun handleNotFound(exception: CafeNotFoundException): ResponseEntity<String> {
return ResponseEntity.badRequest().body(exception.message)
}
}

View File

@@ -1,3 +0,0 @@
package io.beaniejoy.dongnecafe.error.exception
class CafeExistedException(name: String) : RuntimeException("Cafe[$name] is already existed")

View File

@@ -1,4 +0,0 @@
package io.beaniejoy.dongnecafe.error.exception
class CafeMenuNotFoundException(menuId: Long, cafeId: Long) :
RuntimeException("Cafe[${cafeId}]의 Menu[${menuId}]는 존재하지 않는 메뉴입니다.")

View File

@@ -1,3 +0,0 @@
package io.beaniejoy.dongnecafe.error.exception
class CafeNotFoundException(cafeId: Long) : RuntimeException("Cafe[$cafeId] is not found")

View File

@@ -1,3 +0,0 @@
package io.beaniejoy.dongnecafe.error.exception
class MenuOptionNotFoundException(menuOptionId: Long) : RuntimeException("MenuOption[$menuOptionId] is not found")

View File

@@ -1,3 +0,0 @@
package io.beaniejoy.dongnecafe.error.exception
class OptionDetailNotFoundException(optionDetailId: Long) : RuntimeException("OptionDetail[$optionDetailId] is not found")

View File

@@ -1,7 +1,8 @@
package io.beaniejoy.dongnecafe.domain.cafe.service
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import io.beaniejoy.dongnecafe.domain.cafe.entity.CafeMenu
import io.beaniejoy.dongnecafe.error.exception.CafeMenuNotFoundException
import io.beaniejoy.dongnecafe.domain.cafe.repository.CafeMenuRepository
import io.beaniejoy.dongnecafe.domain.cafe.repository.MenuOptionRepository
import io.beaniejoy.dongnecafe.domain.cafe.repository.OptionDetailRepository
@@ -81,12 +82,12 @@ internal class CafeMenuServiceTest {
`when`(mockCafeMenuRepository.findById(findCafeMenuId)).thenReturn(Optional.empty())
// then
val exception = assertThrows<CafeMenuNotFoundException> {
val exception = assertThrows<BusinessException> {
// when
mockCafeMenuService.getDetailedInfoByMenuId(findCafeMenuId, findCafeId)
}
assertEquals("Cafe[${findCafeId}]의 Menu[${findCafeMenuId}]는 존재하지 않는 메뉴입니다.", exception.message)
assertEquals(ErrorCode.CAFE_MENU_NOT_FOUND, exception.errorCode)
verify(mockCafeMenuRepository).findById(findCafeMenuId)
}

View File

@@ -1,10 +1,10 @@
package io.beaniejoy.dongnecafe.domain.cafe.service
import io.beaniejoy.dongnecafe.domain.cafe.entity.Cafe
import io.beaniejoy.dongnecafe.error.exception.CafeExistedException
import io.beaniejoy.dongnecafe.error.exception.CafeNotFoundException
import io.beaniejoy.dongnecafe.domain.cafe.repository.CafeRepository
import io.beaniejoy.dongnecafe.domain.cafe.utils.CafeTestUtils
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.extension.ExtendWith
@@ -68,7 +68,7 @@ internal class CafeServiceTest {
`when`(mockCafeRepository.findByName(name)).thenReturn(cafe)
// then
assertThrows<CafeExistedException> {
val exception = assertThrows<BusinessException> {
// when
mockCafeService.createNew(
name = name,
@@ -78,8 +78,8 @@ internal class CafeServiceTest {
cafeMenuRequestList = cafeMenuList
)
}
verify(mockCafeRepository).findByName(name)
assertEquals(ErrorCode.CAFE_EXISTED, exception.errorCode)
}
@Test
@@ -134,7 +134,7 @@ internal class CafeServiceTest {
`when`(mockCafeRepository.findById(cafeId)).thenReturn(Optional.empty())
assertThrows<CafeNotFoundException> {
val exception = assertThrows<BusinessException> {
mockCafeService.updateInfo(
id = cafeId,
name = "",
@@ -143,5 +143,7 @@ internal class CafeServiceTest {
description = "",
)
}
assertEquals(ErrorCode.CAFE_NOT_FOUND, exception.errorCode)
}
}