generated from pricelees/issue-pr-template
[#44] 매장 기능 도입 #45
134
src/main/kotlin/roomescape/store/business/StoreService.kt
Normal file
134
src/main/kotlin/roomescape/store/business/StoreService.kt
Normal file
@ -0,0 +1,134 @@
|
||||
package roomescape.store.business
|
||||
|
||||
import com.github.f4b6a3.tsid.TsidFactory
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.admin.business.AdminService
|
||||
import roomescape.common.config.next
|
||||
import roomescape.common.dto.AuditInfo
|
||||
import roomescape.region.business.RegionService
|
||||
import roomescape.store.exception.StoreErrorCode
|
||||
import roomescape.store.exception.StoreException
|
||||
import roomescape.store.infrastructure.persistence.StoreEntity
|
||||
import roomescape.store.infrastructure.persistence.StoreRepository
|
||||
import roomescape.store.infrastructure.persistence.StoreStatus
|
||||
import roomescape.store.web.*
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class StoreService(
|
||||
private val storeRepository: StoreRepository,
|
||||
private val adminService: AdminService,
|
||||
private val regionService: RegionService,
|
||||
private val tsidFactory: TsidFactory,
|
||||
) {
|
||||
@Transactional(readOnly = true)
|
||||
fun getDetail(id: Long): DetailStoreResponse {
|
||||
log.info { "[StoreService.getDetail] 매장 상세 조회 시작: id=${id}" }
|
||||
|
||||
val store: StoreEntity = findOrThrow(id)
|
||||
val region = regionService.findRegionInfo(store.regionCode)
|
||||
val audit = getAuditInfo(store)
|
||||
|
||||
return store.toDetailResponse(region, audit)
|
||||
.also { log.info { "[StoreService.getDetail] 매장 상세 조회 완료: id=${id}" } }
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun register(request: StoreRegisterRequest): StoreRegisterResponse {
|
||||
log.info { "[StoreService.register] 매장 등록 시작: name=${request.name}" }
|
||||
|
||||
val store = StoreEntity(
|
||||
id = tsidFactory.next(),
|
||||
name = request.name,
|
||||
address = request.address,
|
||||
contact = request.contact,
|
||||
businessRegNum = request.businessRegNum,
|
||||
regionCode = request.regionCode,
|
||||
status = StoreStatus.ACTIVE,
|
||||
).also {
|
||||
storeRepository.save(it)
|
||||
}
|
||||
|
||||
return StoreRegisterResponse(store.id).also {
|
||||
log.info { "[StoreService.register] 매장 등록 완료: id=${store.id}, name=${request.name}" }
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun update(id: Long, request: StoreUpdateRequest) {
|
||||
log.info { "[StoreService.update] 매장 수정 시작: id=${id}, request=${request}" }
|
||||
|
||||
findOrThrow(id).apply {
|
||||
this.modifyIfNotNull(request.name, request.address, request.contact)
|
||||
}.also {
|
||||
log.info { "[StoreService.update] 매장 수정 완료: id=${id}" }
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun disableById(id: Long) {
|
||||
log.info { "[StoreService.inactive] 매장 비활성화 시작: id=${id}" }
|
||||
|
||||
findOrThrow(id).apply {
|
||||
this.disable()
|
||||
}.also {
|
||||
log.info { "[StoreService.inactive] 매장 비활성화 완료: id=${id}" }
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getAllActiveStores(sidoCode: String?, sigunguCode: String?): SimpleStoreListResponse {
|
||||
log.info { "[StoreService.getAllActiveStores] 전체 매장 조회 시작" }
|
||||
|
||||
val regionCode: String? = when {
|
||||
sidoCode == null && sigunguCode != null -> throw StoreException(StoreErrorCode.SIDO_CODE_REQUIRED)
|
||||
sidoCode != null -> "${sidoCode}${sigunguCode ?: ""}"
|
||||
else -> null
|
||||
}
|
||||
|
||||
return storeRepository.findAllActiveStoresByRegion(regionCode).toSimpleListResponse()
|
||||
.also { log.info { "[StoreService.getAllActiveStores] 전체 매장 조회 완료: total=${it.stores.size}" } }
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findStoreInfo(id: Long): StoreInfoResponse {
|
||||
log.info { "[StoreService.findStoreInfo] 매장 정보 조회 시작: id=${id}" }
|
||||
|
||||
val store: StoreEntity = findOrThrow(id)
|
||||
|
||||
return store.toInfoResponse()
|
||||
.also { log.info { "[StoreService.findStoreInfo] 매장 정보 조회 완료: id=${id}" } }
|
||||
}
|
||||
|
||||
private fun getAuditInfo(store: StoreEntity): AuditInfo {
|
||||
log.info { "[StoreService.getAuditInfo] 감사 정보 조회 시작: storeId=${store.id}" }
|
||||
val createdBy = adminService.findOperatorOrUnknown(store.createdBy)
|
||||
val updatedBy = adminService.findOperatorOrUnknown(store.updatedBy)
|
||||
|
||||
return AuditInfo(
|
||||
createdAt = store.createdAt,
|
||||
createdBy = createdBy,
|
||||
updatedAt = store.updatedAt,
|
||||
updatedBy = updatedBy
|
||||
).also {
|
||||
log.info { "[StoreService.getAuditInfo] 감사 정보 조회 완료: storeId=${store.id}" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun findOrThrow(id: Long): StoreEntity {
|
||||
log.info { "[StoreService.findOrThrow] 매장 조회 시작: id=${id}" }
|
||||
|
||||
return storeRepository.findActiveStoreById(id)
|
||||
?.also {
|
||||
log.info { "[StoreService.findOrThrow] 매장 조회 완료: id=${id}" }
|
||||
}
|
||||
?: run {
|
||||
log.warn { "[StoreService.findOrThrow] 매장 조회 실패: id=${id}" }
|
||||
throw StoreException(StoreErrorCode.STORE_NOT_FOUND)
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/main/kotlin/roomescape/store/docs/StoreAPI.kt
Normal file
64
src/main/kotlin/roomescape/store/docs/StoreAPI.kt
Normal file
@ -0,0 +1,64 @@
|
||||
package roomescape.store.docs
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import roomescape.admin.infrastructure.persistence.AdminType
|
||||
import roomescape.admin.infrastructure.persistence.Privilege
|
||||
import roomescape.auth.web.support.AdminOnly
|
||||
import roomescape.auth.web.support.Public
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.store.web.*
|
||||
|
||||
interface AdminStoreAPI {
|
||||
@AdminOnly(type = AdminType.HQ, privilege = Privilege.READ_DETAIL)
|
||||
@Operation(summary = "특정 매장의 상세 정보 조회")
|
||||
@ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true))
|
||||
fun findStoreDetail(
|
||||
@PathVariable id: Long
|
||||
): ResponseEntity<CommonApiResponse<DetailStoreResponse>>
|
||||
|
||||
@AdminOnly(type = AdminType.HQ, privilege = Privilege.CREATE)
|
||||
@Operation(summary = "매장 등록")
|
||||
@ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true))
|
||||
fun registerStore(
|
||||
@Valid @RequestBody request: StoreRegisterRequest
|
||||
): ResponseEntity<CommonApiResponse<StoreRegisterResponse>>
|
||||
|
||||
@AdminOnly(type = AdminType.STORE, privilege = Privilege.UPDATE)
|
||||
@Operation(summary = "매장 정보 수정")
|
||||
@ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true))
|
||||
fun updateStore(
|
||||
@PathVariable id: Long,
|
||||
@Valid @RequestBody request: StoreUpdateRequest
|
||||
): ResponseEntity<CommonApiResponse<Unit>>
|
||||
|
||||
@AdminOnly(type = AdminType.HQ, privilege = Privilege.DELETE)
|
||||
@Operation(summary = "매장 비활성화")
|
||||
@ApiResponses(ApiResponse(responseCode = "204", useReturnTypeSchema = true))
|
||||
fun disableStore(
|
||||
@PathVariable id: Long
|
||||
): ResponseEntity<CommonApiResponse<Unit>>
|
||||
}
|
||||
|
||||
interface PublicStoreAPI {
|
||||
@Public
|
||||
@Operation(summary = "모든 매장의 id / 이름 조회")
|
||||
@ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true))
|
||||
fun getStores(
|
||||
@RequestParam(value = "sido", required = false) sidoCode: String?,
|
||||
@RequestParam(value = "sigungu", required = false) sigunguCode: String?
|
||||
): ResponseEntity<CommonApiResponse<SimpleStoreListResponse>>
|
||||
|
||||
@Public
|
||||
@Operation(summary = "특정 매장의 정보 조회")
|
||||
@ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true))
|
||||
fun getStoreInfo(
|
||||
@PathVariable id: Long
|
||||
): ResponseEntity<CommonApiResponse<StoreInfoResponse>>
|
||||
}
|
||||
19
src/main/kotlin/roomescape/store/exception/StoreException.kt
Normal file
19
src/main/kotlin/roomescape/store/exception/StoreException.kt
Normal file
@ -0,0 +1,19 @@
|
||||
package roomescape.store.exception
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import roomescape.common.exception.ErrorCode
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
|
||||
class StoreException(
|
||||
override val errorCode: StoreErrorCode,
|
||||
override val message: String = errorCode.message
|
||||
) : RoomescapeException(errorCode, message)
|
||||
|
||||
enum class StoreErrorCode(
|
||||
override val httpStatus: HttpStatus,
|
||||
override val errorCode: String,
|
||||
override val message: String
|
||||
) : ErrorCode {
|
||||
STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "ST001", "매장을 찾을 수 없어요."),
|
||||
SIDO_CODE_REQUIRED(HttpStatus.BAD_REQUEST, "ST002", "시/도 정보를 찾을 수 없어요. 다시 시도해주세요.")
|
||||
}
|
||||
@ -1,11 +1,6 @@
|
||||
package roomescape.store.infrastructure.persistence
|
||||
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.EntityListeners
|
||||
import jakarta.persistence.EnumType
|
||||
import jakarta.persistence.Enumerated
|
||||
import jakarta.persistence.Table
|
||||
import jakarta.persistence.*
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener
|
||||
import roomescape.common.entity.AuditingBaseEntity
|
||||
|
||||
@ -31,9 +26,23 @@ class StoreEntity(
|
||||
|
||||
@Enumerated(value = EnumType.STRING)
|
||||
var status: StoreStatus
|
||||
) : AuditingBaseEntity(id)
|
||||
) : AuditingBaseEntity(id) {
|
||||
fun modifyIfNotNull(
|
||||
name: String?,
|
||||
address: String?,
|
||||
contact: String?,
|
||||
) {
|
||||
name?.let { this.name = it }
|
||||
address?.let { this.address = it }
|
||||
contact?.let { this.contact = it }
|
||||
}
|
||||
|
||||
fun disable() {
|
||||
this.status = StoreStatus.DISABLED
|
||||
}
|
||||
}
|
||||
|
||||
enum class StoreStatus {
|
||||
ACTIVE,
|
||||
INACTIVE
|
||||
DISABLED
|
||||
}
|
||||
|
||||
@ -1,5 +1,33 @@
|
||||
package roomescape.store.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
|
||||
interface StoreRepository : JpaRepository<StoreEntity, Long>
|
||||
interface StoreRepository : JpaRepository<StoreEntity, Long> {
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT
|
||||
s
|
||||
FROM
|
||||
StoreEntity s
|
||||
WHERE
|
||||
s._id = :id
|
||||
AND s.status = roomescape.store.infrastructure.persistence.StoreStatus.ACTIVE
|
||||
"""
|
||||
)
|
||||
fun findActiveStoreById(id: Long): StoreEntity?
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT
|
||||
s
|
||||
FROM
|
||||
StoreEntity s
|
||||
WHERE
|
||||
s.status = roomescape.store.infrastructure.persistence.StoreStatus.ACTIVE
|
||||
AND (:regionCode IS NULL OR s.regionCode LIKE ':regionCode%')
|
||||
"""
|
||||
)
|
||||
fun findAllActiveStoresByRegion(regionCode: String?): List<StoreEntity>
|
||||
}
|
||||
|
||||
52
src/main/kotlin/roomescape/store/web/AdminStoreController.kt
Normal file
52
src/main/kotlin/roomescape/store/web/AdminStoreController.kt
Normal file
@ -0,0 +1,52 @@
|
||||
package roomescape.store.web
|
||||
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.store.business.StoreService
|
||||
import roomescape.store.docs.AdminStoreAPI
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/admin/stores")
|
||||
class AdminStoreController(
|
||||
private val storeService: StoreService
|
||||
) : AdminStoreAPI {
|
||||
|
||||
@GetMapping("/{id}/detail")
|
||||
override fun findStoreDetail(
|
||||
@PathVariable id: Long
|
||||
): ResponseEntity<CommonApiResponse<DetailStoreResponse>> {
|
||||
val response: DetailStoreResponse = storeService.getDetail(id)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
override fun registerStore(
|
||||
@Valid @RequestBody request: StoreRegisterRequest
|
||||
): ResponseEntity<CommonApiResponse<StoreRegisterResponse>> {
|
||||
val response: StoreRegisterResponse = storeService.register(request)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}")
|
||||
override fun updateStore(
|
||||
@PathVariable id: Long,
|
||||
@Valid @RequestBody request: StoreUpdateRequest
|
||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||
storeService.update(id, request)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse())
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/disable")
|
||||
override fun disableStore(
|
||||
@PathVariable id: Long,
|
||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||
storeService.disableById(id)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse())
|
||||
}
|
||||
}
|
||||
46
src/main/kotlin/roomescape/store/web/AdminStoreDto.kt
Normal file
46
src/main/kotlin/roomescape/store/web/AdminStoreDto.kt
Normal file
@ -0,0 +1,46 @@
|
||||
package roomescape.store.web
|
||||
|
||||
import roomescape.common.dto.AuditInfo
|
||||
import roomescape.region.web.RegionInfoResponse
|
||||
import roomescape.store.infrastructure.persistence.StoreEntity
|
||||
|
||||
data class StoreRegisterRequest(
|
||||
val name: String,
|
||||
val address: String,
|
||||
val contact: String,
|
||||
val businessRegNum: String,
|
||||
val regionCode: String
|
||||
)
|
||||
|
||||
data class StoreRegisterResponse(
|
||||
val id: Long
|
||||
)
|
||||
|
||||
data class StoreUpdateRequest(
|
||||
val name: String?,
|
||||
val address: String?,
|
||||
val contact: String?,
|
||||
)
|
||||
|
||||
data class DetailStoreResponse(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val address: String,
|
||||
val contact: String,
|
||||
val businessRegNum: String,
|
||||
val region: RegionInfoResponse,
|
||||
val audit: AuditInfo
|
||||
)
|
||||
|
||||
fun StoreEntity.toDetailResponse(
|
||||
region: RegionInfoResponse,
|
||||
audit: AuditInfo
|
||||
) = DetailStoreResponse(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
address = this.address,
|
||||
contact = this.contact,
|
||||
businessRegNum = this.businessRegNum,
|
||||
region = region,
|
||||
audit = audit,
|
||||
)
|
||||
35
src/main/kotlin/roomescape/store/web/StoreController.kt
Normal file
35
src/main/kotlin/roomescape/store/web/StoreController.kt
Normal file
@ -0,0 +1,35 @@
|
||||
package roomescape.store.web
|
||||
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.store.business.StoreService
|
||||
import roomescape.store.docs.PublicStoreAPI
|
||||
|
||||
@RestController
|
||||
class StoreController(
|
||||
private val storeService: StoreService
|
||||
) : PublicStoreAPI {
|
||||
|
||||
@GetMapping("/stores")
|
||||
override fun getStores(
|
||||
@RequestParam(value = "sido", required = false) sidoCode: String?,
|
||||
@RequestParam(value = "sigungu", required = false) sigunguCode: String?
|
||||
): ResponseEntity<CommonApiResponse<SimpleStoreListResponse>> {
|
||||
val response = storeService.getAllActiveStores(sidoCode, sigunguCode)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@GetMapping("/stores/{id}")
|
||||
override fun getStoreInfo(
|
||||
@PathVariable id: Long
|
||||
): ResponseEntity<CommonApiResponse<StoreInfoResponse>> {
|
||||
val response = storeService.findStoreInfo(id)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,5 @@
|
||||
package roomescape.store.web
|
||||
|
||||
import roomescape.common.dto.AuditInfo
|
||||
import roomescape.region.web.RegionInfoResponse
|
||||
import roomescape.store.infrastructure.persistence.StoreEntity
|
||||
|
||||
data class SimpleStoreResponse(
|
||||
@ -40,13 +38,3 @@ data class StoreInfoListResponse(
|
||||
fun List<StoreEntity>.toInfoListResponse() = StoreInfoListResponse(
|
||||
stores = this.map { it.toInfoResponse() }
|
||||
)
|
||||
|
||||
data class StoreDetailResponse(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val address: String,
|
||||
val contact: String,
|
||||
val businessRegNum: String,
|
||||
val region: RegionInfoResponse,
|
||||
val audit: AuditInfo
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user