diff --git a/src/main/kotlin/com/get_offer/auction/controller/AuctionController.kt b/src/main/kotlin/com/get_offer/auction/controller/AuctionController.kt index c5526de..9ab30a7 100644 --- a/src/main/kotlin/com/get_offer/auction/controller/AuctionController.kt +++ b/src/main/kotlin/com/get_offer/auction/controller/AuctionController.kt @@ -2,9 +2,12 @@ package com.get_offer.auction.controller import ApiResponse import com.get_offer.auction.service.AuctionService -import com.get_offer.auction.service.BuyProductListDto -import com.get_offer.auction.service.SellProductListDto +import com.get_offer.auction.service.BuyAuctionDetailDto +import com.get_offer.auction.service.BuyAuctionDto +import com.get_offer.auction.service.SellAuctionDetailDto +import com.get_offer.auction.service.SellAuctionDto import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @@ -19,7 +22,7 @@ class AuctionController( * 사용자가 판매한 모든 상품을 조회 한다. */ @GetMapping("/sellHistory") - fun getUserSellHistory(@RequestParam userId: String): ApiResponse> { + fun getUserSellHistory(@RequestParam userId: String): ApiResponse> { return ApiResponse.success(auctionService.getSellHistory(userId.toLong())) } @@ -27,7 +30,23 @@ class AuctionController( * 사용자가 경매에서 낙찰된 모든 상품을 조회한다. */ @GetMapping("/buyHistory") - fun getUserBuyHistory(@RequestParam userId: String): ApiResponse> { + fun getUserBuyHistory(@RequestParam userId: String): ApiResponse> { return ApiResponse.success(auctionService.getBuyHistory(userId.toLong())) } + + @GetMapping("{auctionId}/sold") + fun getSoldAuctionDetail( + @RequestParam userId: String, + @PathVariable auctionId: Long + ): ApiResponse { + return ApiResponse.success(auctionService.getSoldAuctionDetail(userId.toLong(), auctionId)) + } + + @GetMapping("{id}/bought") + fun getBoughtAuctionDetail( + @RequestParam userId: String, + @PathVariable id: Long + ): ApiResponse { + return ApiResponse.success(auctionService.getBoughtAuctionDetail(userId.toLong(), id)) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/auction/domain/AuctionResult.kt b/src/main/kotlin/com/get_offer/auction/domain/AuctionResult.kt index 93162f3..3cdc001 100644 --- a/src/main/kotlin/com/get_offer/auction/domain/AuctionResult.kt +++ b/src/main/kotlin/com/get_offer/auction/domain/AuctionResult.kt @@ -24,5 +24,5 @@ class AuctionResult( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long? = null, + val id: Long = 0L, ) : AuditingTimeEntity() \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/auction/service/AuctionProductListDto.kt b/src/main/kotlin/com/get_offer/auction/service/AuctionProductUnitDto.kt similarity index 80% rename from src/main/kotlin/com/get_offer/auction/service/AuctionProductListDto.kt rename to src/main/kotlin/com/get_offer/auction/service/AuctionProductUnitDto.kt index f8984c0..95fc496 100644 --- a/src/main/kotlin/com/get_offer/auction/service/AuctionProductListDto.kt +++ b/src/main/kotlin/com/get_offer/auction/service/AuctionProductUnitDto.kt @@ -5,7 +5,7 @@ import com.get_offer.product.domain.Product import com.get_offer.product.domain.ProductStatus import java.time.LocalDateTime -data class AuctionProductListDto( +data class AuctionProductUnitDto( val id: Long?, val writerId: Long, val name: String, @@ -15,11 +15,10 @@ data class AuctionProductListDto( val status: ProductStatus, val startDate: LocalDateTime, val endDate: LocalDateTime, - val isMine: Boolean, ) { companion object { - fun of(product: Product, userId: Long?): AuctionProductListDto { - return AuctionProductListDto( + fun of(product: Product): AuctionProductUnitDto { + return AuctionProductUnitDto( id = product.id, writerId = product.writerId, name = product.title, @@ -29,7 +28,6 @@ data class AuctionProductListDto( status = product.status, startDate = product.startDate, endDate = product.endDate, - isMine = product.writerId == userId, ) } } diff --git a/src/main/kotlin/com/get_offer/auction/service/AuctionService.kt b/src/main/kotlin/com/get_offer/auction/service/AuctionService.kt index 79b9273..313919c 100644 --- a/src/main/kotlin/com/get_offer/auction/service/AuctionService.kt +++ b/src/main/kotlin/com/get_offer/auction/service/AuctionService.kt @@ -1,29 +1,68 @@ package com.get_offer.auction.service import com.get_offer.auction.controller.repository.AuctionResultRepository +import com.get_offer.auction.domain.AuctionResult import com.get_offer.common.exception.NotFoundException +import com.get_offer.common.exception.UnAuthorizationException +import com.get_offer.product.domain.Product import com.get_offer.product.repository.ProductRepository +import com.get_offer.user.domain.User +import com.get_offer.user.repository.UserRepository import org.springframework.stereotype.Service @Service class AuctionService( private val productRepository: ProductRepository, private val auctionRepository: AuctionResultRepository, + private val userRepository: UserRepository, ) { - fun getSellHistory(userId: Long): List { + fun getSellHistory(userId: Long): List { val productList = productRepository.findAllByWriterIdOrderByEndDateDesc(userId) - return productList.map { SellProductListDto.of(it, userId) } + return productList.map { SellAuctionDto.of(it, userId) } } - fun getBuyHistory(userId: Long): List { + fun getBuyHistory(userId: Long): List { // 옥션 최종 정보에서 buyer가 나인걸 찾음 val auctionResults = auctionRepository.findAllByBuyerIdOrderByCreatedAtDesc(userId) // 해당 상품 정보를 찾음 return auctionResults.map { val product = productRepository.findById(it.productId) .orElseThrow { NotFoundException("${it.productId} 의 상품은 존재하지 않습니다.") } - BuyProductListDto.of(it, product, userId) + BuyAuctionDto.of(it, product) } } + + fun getSoldAuctionDetail(userId: Long, auctionId: Long): SellAuctionDetailDto { + val (auction, product) = getAuctionAndProduct(auctionId) + if (userId != product.writerId) throw UnAuthorizationException() + + val buyer = getUser(auction.buyerId) + + return SellAuctionDetailDto.of(product, buyer, auction) + } + + fun getBoughtAuctionDetail(userId: Long, auctionId: Long): BuyAuctionDetailDto { + val (auction, product) = getAuctionAndProduct(auctionId) + if (userId != auction.buyerId) throw UnAuthorizationException() + + val seller = getUser(product.writerId) + + return BuyAuctionDetailDto.of(product, seller, auction) + } + + private fun getAuctionAndProduct(auctionId: Long): Pair { + val auction = auctionRepository.findById(auctionId) + .orElseThrow { NotFoundException("$auctionId 의 경매 내역은 존재하지 않습니다.") } + + val product = productRepository.findById(auction.productId) + .orElseThrow { NotFoundException("${auction.productId} 의 상품은 존재하지 않습니다.") } + + return Pair(auction, product) + } + + private fun getUser(userId: Long): User { + return userRepository.findById(userId) + .orElseThrow { NotFoundException("$userId 의 사용자는 존재하지 않습니다.") } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/auction/service/AuctionUserDto.kt b/src/main/kotlin/com/get_offer/auction/service/AuctionUserDto.kt new file mode 100644 index 0000000..00c4e06 --- /dev/null +++ b/src/main/kotlin/com/get_offer/auction/service/AuctionUserDto.kt @@ -0,0 +1,19 @@ +package com.get_offer.auction.service + +import com.get_offer.user.domain.User + +data class AuctionUserDto( + val id: Long, + val profileImage: String, + val nickname: String, +) { + companion object { + fun of(user: User): AuctionUserDto { + return AuctionUserDto( + id = user.id, + profileImage = user.image, + nickname = user.nickname + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/auction/service/BuyAuctionDetailDto.kt b/src/main/kotlin/com/get_offer/auction/service/BuyAuctionDetailDto.kt new file mode 100644 index 0000000..c3da55d --- /dev/null +++ b/src/main/kotlin/com/get_offer/auction/service/BuyAuctionDetailDto.kt @@ -0,0 +1,24 @@ +package com.get_offer.auction.service + +import com.get_offer.auction.domain.AuctionResult +import com.get_offer.auction.domain.AuctionStatus +import com.get_offer.product.domain.Product +import com.get_offer.user.domain.User + +data class BuyAuctionDetailDto( + val auctionId: Long, + val auctionStatus: AuctionStatus, + val product: AuctionProductUnitDto, + val seller: AuctionUserDto, +) { + companion object { + fun of(product: Product, seller: User, auction: AuctionResult): BuyAuctionDetailDto { + return BuyAuctionDetailDto( + auctionId = auction.id, + auctionStatus = auction.auctionStatus, + product = AuctionProductUnitDto.of(product), + seller = AuctionUserDto.of(seller), + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/auction/service/BuyProductListDto.kt b/src/main/kotlin/com/get_offer/auction/service/BuyAuctionDto.kt similarity index 87% rename from src/main/kotlin/com/get_offer/auction/service/BuyProductListDto.kt rename to src/main/kotlin/com/get_offer/auction/service/BuyAuctionDto.kt index 51aaa14..beefb6f 100644 --- a/src/main/kotlin/com/get_offer/auction/service/BuyProductListDto.kt +++ b/src/main/kotlin/com/get_offer/auction/service/BuyAuctionDto.kt @@ -6,7 +6,7 @@ import com.get_offer.product.domain.Category import com.get_offer.product.domain.Product import java.time.LocalDateTime -data class BuyProductListDto( +data class BuyAuctionDto( val productId: Long?, val writerId: Long, val buyerId: Long, @@ -20,8 +20,8 @@ data class BuyProductListDto( val endDate: LocalDateTime, ) { companion object { - fun of(auctionResult: AuctionResult, product: Product, userId: Long?): BuyProductListDto { - return BuyProductListDto( + fun of(auctionResult: AuctionResult, product: Product): BuyAuctionDto { + return BuyAuctionDto( productId = product.id, writerId = product.writerId, buyerId = auctionResult.buyerId, diff --git a/src/main/kotlin/com/get_offer/auction/service/SellAuctionDetailDto.kt b/src/main/kotlin/com/get_offer/auction/service/SellAuctionDetailDto.kt new file mode 100644 index 0000000..76ee9ef --- /dev/null +++ b/src/main/kotlin/com/get_offer/auction/service/SellAuctionDetailDto.kt @@ -0,0 +1,24 @@ +package com.get_offer.auction.service + +import com.get_offer.auction.domain.AuctionResult +import com.get_offer.auction.domain.AuctionStatus +import com.get_offer.product.domain.Product +import com.get_offer.user.domain.User + +data class SellAuctionDetailDto( + val auctionId: Long, + val auctionStatus: AuctionStatus, + val product: AuctionProductUnitDto, + val buyer: AuctionUserDto, +) { + companion object { + fun of(product: Product, buyer: User, auction: AuctionResult): SellAuctionDetailDto { + return SellAuctionDetailDto( + auctionId = auction.id, + auctionStatus = auction.auctionStatus, + product = AuctionProductUnitDto.of(product), + buyer = AuctionUserDto.of(buyer), + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/auction/service/SellProductListDto.kt b/src/main/kotlin/com/get_offer/auction/service/SellAuctionDto.kt similarity index 87% rename from src/main/kotlin/com/get_offer/auction/service/SellProductListDto.kt rename to src/main/kotlin/com/get_offer/auction/service/SellAuctionDto.kt index a257ec3..b186ce6 100644 --- a/src/main/kotlin/com/get_offer/auction/service/SellProductListDto.kt +++ b/src/main/kotlin/com/get_offer/auction/service/SellAuctionDto.kt @@ -5,7 +5,7 @@ import com.get_offer.product.domain.Product import com.get_offer.product.domain.ProductStatus import java.time.LocalDateTime -data class SellProductListDto( +data class SellAuctionDto( val id: Long?, val writerId: Long, val title: String, @@ -18,8 +18,8 @@ data class SellProductListDto( val isMine: Boolean, ) { companion object { - fun of(product: Product, userId: Long?): SellProductListDto { - return SellProductListDto( + fun of(product: Product, userId: Long?): SellAuctionDto { + return SellAuctionDto( id = product.id, writerId = product.writerId, title = product.title, diff --git a/src/main/kotlin/com/get_offer/common/exception/CustomExceptions.kt b/src/main/kotlin/com/get_offer/common/exception/CustomExceptions.kt index 5a3a340..60533e0 100644 --- a/src/main/kotlin/com/get_offer/common/exception/CustomExceptions.kt +++ b/src/main/kotlin/com/get_offer/common/exception/CustomExceptions.kt @@ -3,4 +3,5 @@ package com.get_offer.common.exception /** * custom exception 처리를 위해 남겨 놓았습니다. */ -class NotFoundException(override val message: String) : RuntimeException(message) \ No newline at end of file +class NotFoundException(override val message: String) : RuntimeException(message) +class UnAuthorizationException : RuntimeException() \ No newline at end of file diff --git a/src/main/kotlin/com/get_offer/common/exception/ExceptionControllerAdvice.kt b/src/main/kotlin/com/get_offer/common/exception/ExceptionControllerAdvice.kt index 8d0ee0c..ef1e911 100644 --- a/src/main/kotlin/com/get_offer/common/exception/ExceptionControllerAdvice.kt +++ b/src/main/kotlin/com/get_offer/common/exception/ExceptionControllerAdvice.kt @@ -17,4 +17,9 @@ class ExceptionControllerAdvice { fun handleNotFoundException(ex: NotFoundException): ResponseEntity> { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponse.error(ex.message)) } + + @ExceptionHandler + fun handleUnAuthorizationException(ex: UnAuthorizationException): ResponseEntity> { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponse.error("인가되지 않은 사용자입니다")) + } } \ No newline at end of file diff --git a/src/test/kotlin/com/get_offer/TestFixtures.kt b/src/test/kotlin/com/get_offer/TestFixtures.kt index 11c359b..e9fedab 100644 --- a/src/test/kotlin/com/get_offer/TestFixtures.kt +++ b/src/test/kotlin/com/get_offer/TestFixtures.kt @@ -6,6 +6,7 @@ import com.get_offer.product.domain.Category import com.get_offer.product.domain.Product import com.get_offer.product.domain.ProductImagesVo import com.get_offer.product.domain.ProductStatus +import com.get_offer.user.domain.User import java.time.LocalDateTime object TestFixtures { @@ -63,5 +64,13 @@ object TestFixtures { ) } + fun createBuyer(buyerId: Long?): User { + return User( + nickname = "buyer1", + image = "image.png", + id = buyerId ?: 1L, + ) + } + } \ No newline at end of file diff --git a/src/test/kotlin/com/get_offer/auction/controller/AuctionIntegrationTest.kt b/src/test/kotlin/com/get_offer/auction/controller/AuctionIntegrationTest.kt index 4e6e949..5f62e29 100644 --- a/src/test/kotlin/com/get_offer/auction/controller/AuctionIntegrationTest.kt +++ b/src/test/kotlin/com/get_offer/auction/controller/AuctionIntegrationTest.kt @@ -20,8 +20,7 @@ class AuctionIntegrationTest( @Test fun auctionSellIntegrationTest() { mockMvc.perform( - MockMvcRequestBuilders.get("/auctions/sellHistory") - .param("userId", "1") + MockMvcRequestBuilders.get("/auctions/sellHistory").param("userId", "1") ).andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk) .andExpect(MockMvcResultMatchers.jsonPath("$.data.size()").value(3)) .andExpect(MockMvcResultMatchers.jsonPath("$.data[0].id").value("1")) @@ -40,12 +39,11 @@ class AuctionIntegrationTest( .andExpect(MockMvcResultMatchers.jsonPath("$.data[2].category").value("FURNITURE")) .andExpect(MockMvcResultMatchers.jsonPath("$.data[2].thumbnail").value("https://picsum.photos/200/300")) } - + @Test fun auctionBuyIntegrationTest() { mockMvc.perform( - MockMvcRequestBuilders.get("/auctions/buyHistory") - .param("userId", "2") + MockMvcRequestBuilders.get("/auctions/buyHistory").param("userId", "2") ).andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk) .andExpect(MockMvcResultMatchers.jsonPath("$.data.size()").value(1)) .andExpect(MockMvcResultMatchers.jsonPath("$.data[0].productId").value("1")) @@ -57,4 +55,32 @@ class AuctionIntegrationTest( .andExpect(MockMvcResultMatchers.jsonPath("$.data[0].thumbnail").value("https://picsum.photos/200/300")) .andExpect(MockMvcResultMatchers.jsonPath("$.data[0].finalPrice").value("10000")) } + + @Test + fun getSoldAuctionDetailIntegrationTest() { + mockMvc.perform( + MockMvcRequestBuilders.get("/auctions/1/sold").param("userId", "1") + ).andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.auctionId").value("1")) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.auctionStatus").value("COMPLETED")) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.product.id").value("1")) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.product.writerId").value("1")) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.product.name").value("nintendo")) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.buyer.id").value("2")) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.buyer.nickname").value("test2")) + } + + @Test + fun getBoughtAuctionDetailIntegrationTest() { + mockMvc.perform( + MockMvcRequestBuilders.get("/auctions/1/bought").param("userId", "2") + ).andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.auctionId").value("1")) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.auctionStatus").value("COMPLETED")) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.product.id").value("1")) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.product.writerId").value("1")) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.product.name").value("nintendo")) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.seller.id").value("1")) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.seller.nickname").value("test")) + } } \ No newline at end of file diff --git a/src/test/kotlin/com/get_offer/auction/service/AuctionServiceTest.kt b/src/test/kotlin/com/get_offer/auction/service/AuctionServiceTest.kt index 0769252..9f6d787 100644 --- a/src/test/kotlin/com/get_offer/auction/service/AuctionServiceTest.kt +++ b/src/test/kotlin/com/get_offer/auction/service/AuctionServiceTest.kt @@ -3,6 +3,7 @@ package com.get_offer.auction.service import com.get_offer.TestFixtures import com.get_offer.auction.controller.repository.AuctionResultRepository import com.get_offer.product.repository.ProductRepository +import com.get_offer.user.repository.UserRepository import java.util.* import org.assertj.core.api.Assertions import org.junit.jupiter.api.BeforeEach @@ -15,12 +16,14 @@ class AuctionServiceTest { private lateinit var auctionService: AuctionService private lateinit var mockProductRepository: ProductRepository private lateinit var mockAuctionRepository: AuctionResultRepository + private lateinit var mockUserRepository: UserRepository @BeforeEach fun setUp() { mockProductRepository = mock(ProductRepository::class.java) mockAuctionRepository = mock(AuctionResultRepository::class.java) - auctionService = AuctionService(mockProductRepository, mockAuctionRepository) + mockUserRepository = mock(UserRepository::class.java) + auctionService = AuctionService(mockProductRepository, mockAuctionRepository, mockUserRepository) } @Test @@ -31,8 +34,11 @@ class AuctionServiceTest { val givenProduct2 = TestFixtures.createProductWait(writerId) // when - `when`(mockProductRepository.findAllByWriterIdOrderByEndDateDesc(writerId)) - .thenReturn(listOf(givenProduct, givenProduct2)) + `when`(mockProductRepository.findAllByWriterIdOrderByEndDateDesc(writerId)).thenReturn( + listOf( + givenProduct, givenProduct2 + ) + ) val result = auctionService.getSellHistory(writerId) @@ -53,11 +59,9 @@ class AuctionServiceTest { val givenAuction = TestFixtures.createAuction(buyerId) val givenProduct = TestFixtures.createProductCompleted(writerId) - `when`(mockAuctionRepository.findAllByBuyerIdOrderByCreatedAtDesc(buyerId)) - .thenReturn(listOf(givenAuction)) + `when`(mockAuctionRepository.findAllByBuyerIdOrderByCreatedAtDesc(buyerId)).thenReturn(listOf(givenAuction)) - `when`(mockProductRepository.findById(any())) - .thenReturn(Optional.of(givenProduct)) + `when`(mockProductRepository.findById(any())).thenReturn(Optional.of(givenProduct)) val result = auctionService.getBuyHistory(buyerId) @@ -67,5 +71,47 @@ class AuctionServiceTest { Assertions.assertThat(result[0].finalPrice).isEqualTo(givenAuction.finalPrice) Assertions.assertThat(result[0].writerId).isEqualTo(givenProduct.writerId) } + + @Test + fun getSoldAuctionDetailReturnsDto() { + val writerId = 1L + val buyerId = 2L + + val givenAuction = TestFixtures.createAuction(buyerId) + val givenProduct = TestFixtures.createProductCompleted(writerId) + val givenBuyer = TestFixtures.createBuyer(buyerId) + + `when`(mockAuctionRepository.findById(givenAuction.id)).thenReturn(Optional.of(givenAuction)) + `when`(mockProductRepository.findById(any())).thenReturn(Optional.of(givenProduct)) + `when`(mockUserRepository.findById(any())).thenReturn(Optional.of(givenBuyer)) + + val result = auctionService.getSoldAuctionDetail(writerId, givenAuction.id) + + Assertions.assertThat(result.auctionId).isEqualTo(givenAuction.id) + Assertions.assertThat(result.auctionStatus).isEqualTo(givenAuction.auctionStatus) + Assertions.assertThat(result.product.name).isEqualTo(givenProduct.title) + Assertions.assertThat(result.buyer.nickname).isEqualTo(givenBuyer.nickname) + } + + @Test + fun getBoughtAuctionDetailReturnsDto() { + val sellerId = 1L + val buyerId = 2L + + val givenAuction = TestFixtures.createAuction(buyerId) + val givenProduct = TestFixtures.createProductCompleted(sellerId) + val givenBuyer = TestFixtures.createBuyer(buyerId) + + `when`(mockAuctionRepository.findById(givenAuction.id)).thenReturn(Optional.of(givenAuction)) + `when`(mockProductRepository.findById(any())).thenReturn(Optional.of(givenProduct)) + `when`(mockUserRepository.findById(any())).thenReturn(Optional.of(givenBuyer)) + + val result = auctionService.getBoughtAuctionDetail(buyerId, givenAuction.id) + + Assertions.assertThat(result.auctionId).isEqualTo(givenAuction.id) + Assertions.assertThat(result.auctionStatus).isEqualTo(givenAuction.auctionStatus) + Assertions.assertThat(result.product.name).isEqualTo(givenProduct.title) + Assertions.assertThat(result.seller.nickname).isEqualTo(givenBuyer.nickname) + } }