diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftController.kt index 3de06d87..564b91d5 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftController.kt @@ -3,12 +3,16 @@ package kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.`in`.web import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftCategoryRequest import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftCategoryResponse +import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftDetailResponse +import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftListResponse import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftMarkUndeliverableRequest import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftOperationStatusResponse import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftSizePriceRequest import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftSizePriceResponse +import kr.co.vividnext.sodalive.v2.gift.application.GiftAdminQueryService import kr.co.vividnext.sodalive.v2.gift.application.GiftAdminService import kr.co.vividnext.sodalive.v2.gift.domain.GiftSize +import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping @@ -17,12 +21,30 @@ import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v2/admin") @PreAuthorize("hasRole('ADMIN')") -class AdminGiftController(private val service: GiftAdminService) { +class AdminGiftController( + private val service: GiftAdminService, + private val queryService: GiftAdminQueryService +) { + @GetMapping("/gifts") + fun getGifts( + @RequestParam(required = false) status: GiftStatus?, + @RequestParam(required = false) applicationNo: String?, + @RequestParam(required = false) nickname: String?, + @RequestParam(defaultValue = "0") page: Int, + @RequestParam(defaultValue = "20") size: Int + ) = ApiResponse.ok(AdminGiftListResponse.from(queryService.getGifts(status, applicationNo, nickname, page, size))) + + @GetMapping("/gifts/{applicationNo}") + fun getGiftDetail(@PathVariable applicationNo: String) = ApiResponse.ok( + AdminGiftDetailResponse.from(queryService.getGiftDetail(applicationNo)) + ) + @GetMapping("/gift-categories") fun getCategories() = ApiResponse.ok(service.getCategories().map { AdminGiftCategoryResponse.from(it) }) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/dto/AdminGiftResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/dto/AdminGiftResponse.kt index 6754d5be..32c9a606 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/dto/AdminGiftResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/dto/AdminGiftResponse.kt @@ -1,6 +1,12 @@ package kr.co.vividnext.sodalive.v2.api.admin.gift.dto import kr.co.vividnext.sodalive.extensions.toUtcIso +import kr.co.vividnext.sodalive.v2.gift.application.AdminGiftDetailResult +import kr.co.vividnext.sodalive.v2.gift.application.AdminGiftInboundDeliveryInfoResult +import kr.co.vividnext.sodalive.v2.gift.application.AdminGiftListItemResult +import kr.co.vividnext.sodalive.v2.gift.application.AdminGiftListResult +import kr.co.vividnext.sodalive.v2.gift.application.AdminGiftMemberInfoResult +import kr.co.vividnext.sodalive.v2.gift.application.AdminGiftProductInfoResult import kr.co.vividnext.sodalive.v2.gift.application.GiftCategoryResult import kr.co.vividnext.sodalive.v2.gift.application.GiftOperationStatusResult import kr.co.vividnext.sodalive.v2.gift.application.GiftSizePriceResult @@ -63,3 +69,121 @@ data class AdminGiftOperationStatusResponse( ) } } + +data class AdminGiftListResponse( + val totalCount: Long, + val items: List, + val page: Int, + val size: Int, + val hasNext: Boolean +) { + companion object { + fun from(result: AdminGiftListResult) = AdminGiftListResponse( + totalCount = result.totalCount, + items = result.items.map { AdminGiftListItemResponse.from(it) }, + page = result.page, + size = result.size, + hasNext = result.hasNext + ) + } +} + +data class AdminGiftListItemResponse( + val applicationNo: String, + val senderNickname: String, + val recipientNickname: String, + val sizeCode: String, + val sizeName: String, + val categoryName: String, + val classificationNumber: String, + val courierCompanyName: String?, + val trackingNumber: String?, + val status: GiftStatus, + val statusName: String, + val availableActions: List +) { + companion object { + fun from(result: AdminGiftListItemResult) = AdminGiftListItemResponse( + applicationNo = result.applicationNo, + senderNickname = result.senderNickname, + recipientNickname = result.recipientNickname, + sizeCode = result.sizeCode, + sizeName = result.sizeName, + categoryName = result.categoryName, + classificationNumber = result.classificationNumber, + courierCompanyName = result.courierCompanyName, + trackingNumber = result.trackingNumber, + status = result.status, + statusName = result.statusName, + availableActions = result.availableActions + ) + } +} + +data class AdminGiftDetailResponse( + val applicationNo: String, + val senderInfo: AdminGiftMemberInfoResponse, + val recipientInfo: AdminGiftMemberInfoResponse, + val productInfo: AdminGiftProductInfoResponse, + val inboundDeliveryInfo: AdminGiftInboundDeliveryInfoResponse, + val status: GiftStatus, + val statusName: String, + val availableActions: List +) { + companion object { + fun from(result: AdminGiftDetailResult) = AdminGiftDetailResponse( + applicationNo = result.applicationNo, + senderInfo = AdminGiftMemberInfoResponse.from(result.senderInfo), + recipientInfo = AdminGiftMemberInfoResponse.from(result.recipientInfo), + productInfo = AdminGiftProductInfoResponse.from(result.productInfo), + inboundDeliveryInfo = AdminGiftInboundDeliveryInfoResponse.from(result.inboundDeliveryInfo), + status = result.status, + statusName = result.statusName, + availableActions = result.availableActions + ) + } +} + +data class AdminGiftMemberInfoResponse( + val nickname: String, + val name: String, + val phoneNumber: String, + val address: String +) { + companion object { + fun from(result: AdminGiftMemberInfoResult) = AdminGiftMemberInfoResponse( + nickname = result.nickname, + name = result.name, + phoneNumber = result.phoneNumber, + address = result.address + ) + } +} + +data class AdminGiftProductInfoResponse( + val sizeCode: String, + val sizeName: String, + val categoryName: String, + val classificationNumber: String +) { + companion object { + fun from(result: AdminGiftProductInfoResult) = AdminGiftProductInfoResponse( + sizeCode = result.sizeCode, + sizeName = result.sizeName, + categoryName = result.categoryName, + classificationNumber = result.classificationNumber + ) + } +} + +data class AdminGiftInboundDeliveryInfoResponse( + val courierCompanyName: String?, + val trackingNumber: String? +) { + companion object { + fun from(result: AdminGiftInboundDeliveryInfoResult) = AdminGiftInboundDeliveryInfoResponse( + courierCompanyName = result.courierCompanyName, + trackingNumber = result.trackingNumber + ) + } +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt index 369ac8a2..db7dcb0e 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt @@ -1,7 +1,10 @@ package kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.`in`.web +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.Gift +import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategory import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDelivery import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDeliveryRepository @@ -46,12 +49,93 @@ class AdminGiftControllerTest @Autowired constructor( private val categoryRepository: GiftCategoryRepository, private val sizePriceRepository: GiftSizePriceRepository, private val giftRepository: GiftRepository, - private val giftDeliveryRepository: GiftDeliveryRepository + private val giftDeliveryRepository: GiftDeliveryRepository, + private val memberRepository: MemberRepository ) { @BeforeEach fun deleteGiftFixtures() { giftDeliveryRepository.deleteAll() giftRepository.deleteAll() + categoryRepository.deleteAll() + } + + @Test + @DisplayName("관리자는 전체 선물함 목록을 필터링해 조회한다") + fun shouldListAdminGifts() { + val (gift, _) = saveAdminGift( + applicationNo = "A-1002609300001", + status = GiftStatus.TRACKING_REGISTERED, + senderNickname = "admin-fan-alpha", + recipientNickname = "admin-creator-alpha", + withTracking = true + ) + + mockMvc.perform( + get("/api/v2/admin/gifts") + .with(user("admin").roles("ADMIN")) + .param("status", "TRACKING_REGISTERED") + .param("applicationNo", "0001") + .param("nickname", "alpha") + .param("page", "0") + .param("size", "20") + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.totalCount").value(1)) + .andExpect(jsonPath("$.data.items[0].applicationNo").value(gift.applicationNo)) + .andExpect(jsonPath("$.data.items[0].senderNickname").value("admin-fan-alpha")) + .andExpect(jsonPath("$.data.items[0].recipientNickname").value("admin-creator-alpha")) + .andExpect(jsonPath("$.data.items[0].sizeCode").value("SMALL")) + .andExpect(jsonPath("$.data.items[0].sizeName").value("소형")) + .andExpect(jsonPath("$.data.items[0].categoryName").value("인형")) + .andExpect(jsonPath("$.data.items[0].classificationNumber").value("100")) + .andExpect(jsonPath("$.data.items[0].courierCompanyName").value("CJ대한통운")) + .andExpect(jsonPath("$.data.items[0].trackingNumber").value("1234567890")) + .andExpect(jsonPath("$.data.items[0].status").value("TRACKING_REGISTERED")) + .andExpect(jsonPath("$.data.items[0].statusName").value("발송 확인")) + .andExpect(jsonPath("$.data.items[0].availableActions[0]").value("ARRIVE_MAILBOX")) + .andExpect(jsonPath("$.data.items[0].availableActions[1]").value("MARK_UNDELIVERABLE")) + .andExpect(jsonPath("$.data.page").value(0)) + .andExpect(jsonPath("$.data.size").value(20)) + .andExpect(jsonPath("$.data.hasNext").value(false)) + } + + @Test + @DisplayName("관리자는 선물 상세를 조회하고 미입력 수취인 정보는 빈 문자열로 받는다") + fun shouldGetAdminGiftDetail() { + saveAdminGift( + applicationNo = "A-1002609300002", + status = GiftStatus.ARRIVED_AT_MAILBOX, + senderNickname = "admin-fan-beta", + recipientNickname = "admin-creator-beta", + withTracking = true + ) + + mockMvc.perform( + get("/api/v2/admin/gifts/A-1002609300002") + .with(user("admin").roles("ADMIN")) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.applicationNo").value("A-1002609300002")) + .andExpect(jsonPath("$.data.senderInfo.nickname").value("admin-fan-beta")) + .andExpect(jsonPath("$.data.senderInfo.name").value("홍길동")) + .andExpect(jsonPath("$.data.senderInfo.phoneNumber").value("01012345678")) + .andExpect(jsonPath("$.data.senderInfo.address").value("(06234) 서울시 강남구, 101동 1001호")) + .andExpect(jsonPath("$.data.recipientInfo.nickname").value("admin-creator-beta")) + .andExpect(jsonPath("$.data.recipientInfo.name").value("")) + .andExpect(jsonPath("$.data.recipientInfo.phoneNumber").value("")) + .andExpect(jsonPath("$.data.recipientInfo.address").value("")) + .andExpect(jsonPath("$.data.productInfo.sizeCode").value("SMALL")) + .andExpect(jsonPath("$.data.productInfo.sizeName").value("소형")) + .andExpect(jsonPath("$.data.productInfo.categoryName").value("인형")) + .andExpect(jsonPath("$.data.productInfo.classificationNumber").value("100")) + .andExpect(jsonPath("$.data.inboundDeliveryInfo.courierCompanyName").value("CJ대한통운")) + .andExpect(jsonPath("$.data.inboundDeliveryInfo.trackingNumber").value("1234567890")) + .andExpect(jsonPath("$.data.status").value("ARRIVED_AT_MAILBOX")) + .andExpect(jsonPath("$.data.statusName").value("사서함 도착")) + .andExpect(jsonPath("$.data.availableActions[0]").value("COMPLETE_INSPECTION")) + .andExpect(jsonPath("$.data.availableActions[1]").value("MARK_UNDELIVERABLE")) } @Test @@ -242,6 +326,62 @@ class AdminGiftControllerTest @Autowired constructor( ) } + private fun saveAdminGift( + applicationNo: String, + status: GiftStatus, + senderNickname: String, + recipientNickname: String, + withTracking: Boolean + ): Pair { + val sender = memberRepository.save(Member(password = "password", nickname = senderNickname)) + val recipient = memberRepository.save(Member(password = "password", nickname = recipientNickname)) + val category = categoryRepository.save( + GiftCategory( + classificationNumber = "100", + categoryCode = "ADMIN_${applicationNo.takeLast(4)}", + name = "인형", + receiptCode = "A", + representativeItem = "피규어" + ) + ) + val gift = giftRepository.save( + Gift( + applicationNo = applicationNo, + senderMemberId = sender.id!!, + recipientMemberId = recipient.id!!, + status = status, + sizeCode = GiftSize.SMALL, + categoryId = category.id!!, + categoryNameSnapshot = category.name, + salePriceCan = 80, + canUsageId = 10L, + senderTermsAgreed = true, + senderPrivacyAgreed = true, + senderTermsAgreedAt = NOW.minusDays(1), + senderPrivacyAgreedAt = NOW.minusDays(1), + damageWaiverAgreed = true + ) + ) + val delivery = giftDeliveryRepository.save( + GiftDelivery( + giftId = requireNotNull(gift.id), + senderName = "홍길동", + senderPhoneNumber = "01012345678", + senderZipCode = "06234", + senderAddress = "서울시 강남구", + senderAddressDetail = "101동 1001호", + trackingDeadlineAt = NOW.minusDays(1) + ).apply { + if (withTracking) { + courierCompanyName = "CJ대한통운" + trackingNumber = "1234567890" + trackingRegisteredAt = NOW.minusHours(1) + } + } + ) + return gift to delivery + } + private companion object { const val APPLICATION_NO = "G20260929000001" const val SENDER_MEMBER_ID = 1L