From 22c6cd2d38fd7dd6c6e39a52c90943ff26370e3a Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 30 Sep 2026 17:38:51 +0900 Subject: [PATCH] =?UTF-8?q?feat(gift):=20=EC=84=A0=EB=AC=BC=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EC=8B=9C=EA=B0=81=EC=9D=84=20UTC=20=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EC=97=B4=EB=A1=9C=20=EB=B3=80=ED=99=98=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/admin/gift/dto/AdminGiftResponse.kt | 12 +++-- .../sodalive/v2/api/gift/dto/GiftResponse.kt | 54 +++++++++---------- .../adapter/in/web/AdminGiftControllerTest.kt | 31 ++++++++++- .../gift/adapter/in/web/GiftControllerTest.kt | 50 ++++++++++++----- 4 files changed, 103 insertions(+), 44 deletions(-) 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 58f698a1..6754d5be 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,23 +1,29 @@ 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.GiftCategoryResult import kr.co.vividnext.sodalive.v2.gift.application.GiftOperationStatusResult import kr.co.vividnext.sodalive.v2.gift.application.GiftSizePriceResult import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus -import java.time.LocalDateTime data class AdminGiftCategoryResponse( val categoryId: Long, + val classificationNumber: String, val categoryCode: String, val name: String, + val receiptCode: String, + val representativeItem: String, val requiresDamageWaiver: Boolean, val isActive: Boolean ) { companion object { fun from(result: GiftCategoryResult) = AdminGiftCategoryResponse( categoryId = result.categoryId, + classificationNumber = result.classificationNumber, categoryCode = result.categoryCode, name = result.name, + receiptCode = result.receiptCode, + representativeItem = result.representativeItem, requiresDamageWaiver = result.requiresDamageWaiver, isActive = result.isActive ) @@ -46,14 +52,14 @@ data class AdminGiftOperationStatusResponse( val applicationNo: String, val status: GiftStatus, val statusName: String, - val occurredAt: LocalDateTime + val occurredAt: String ) { companion object { fun from(result: GiftOperationStatusResult) = AdminGiftOperationStatusResponse( applicationNo = result.applicationNo, status = result.status, statusName = result.statusName, - occurredAt = result.occurredAt + occurredAt = result.occurredAt.toUtcIso() ) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/dto/GiftResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/dto/GiftResponse.kt index c3e15825..5c9883fc 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/dto/GiftResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/dto/GiftResponse.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.v2.api.gift.dto +import kr.co.vividnext.sodalive.extensions.toUtcIso import kr.co.vividnext.sodalive.v2.gift.application.GiftApplicationResult import kr.co.vividnext.sodalive.v2.gift.application.GiftCancellationResult import kr.co.vividnext.sodalive.v2.gift.application.GiftDeliveryConfirmationResult @@ -9,7 +10,6 @@ import kr.co.vividnext.sodalive.v2.gift.application.GiftListResult import kr.co.vividnext.sodalive.v2.gift.application.GiftRecipientAddressRegistrationResult import kr.co.vividnext.sodalive.v2.gift.application.GiftReviewResult import kr.co.vividnext.sodalive.v2.gift.application.GiftTrackingRegistrationResult -import java.time.LocalDateTime data class GiftFormOptionsResponse( val sizes: List, @@ -59,7 +59,7 @@ data class GiftListResponse( priceCan = it.priceCan, categoryName = it.categoryName, sizeName = it.sizeName, - createdAt = it.createdAt + createdAt = it.createdAt?.toUtcIso() ) }, page = result.page, @@ -77,7 +77,7 @@ data class GiftListItemResponse( val priceCan: Int, val categoryName: String, val sizeName: String, - val createdAt: LocalDateTime? + val createdAt: String? ) data class GiftDetailResponse( @@ -91,7 +91,7 @@ data class GiftDetailResponse( val mailbox: GiftMailboxResponse?, val trackingRequired: Boolean, val recipientAddressRequired: Boolean, - val recipientAddressDeadlineAt: LocalDateTime?, + val recipientAddressDeadlineAt: String?, val delivery: GiftDeliveryInfoResponse, val statusTimeline: List ) { @@ -109,21 +109,21 @@ data class GiftDetailResponse( applicationNo = result.giftInfo.applicationNo, paidCan = result.giftInfo.paidCan, tracking = result.giftInfo.tracking, - shippingRequestedAt = result.giftInfo.shippingRequestedAt + shippingRequestedAt = result.giftInfo.shippingRequestedAt?.toUtcIso() ), senderInfo = result.senderInfo?.let { GiftAddressResponse(it.name, it.phoneNumber, it.address) }, recipientAddress = result.recipientAddress?.let { GiftAddressResponse(it.name, it.phoneNumber, it.address) }, mailbox = result.mailbox?.let { GiftMailboxResponse(it.name, it.address, it.phoneNumber) }, trackingRequired = result.trackingRequired, recipientAddressRequired = result.recipientAddressRequired, - recipientAddressDeadlineAt = result.recipientAddressDeadlineAt, + recipientAddressDeadlineAt = result.recipientAddressDeadlineAt?.toUtcIso(), delivery = GiftDeliveryInfoResponse( - canceledAt = result.delivery.canceledAt, - undeliverableAt = result.delivery.undeliverableAt, + canceledAt = result.delivery.canceledAt?.toUtcIso(), + undeliverableAt = result.delivery.undeliverableAt?.toUtcIso(), undeliverableReason = result.delivery.undeliverableReason ), statusTimeline = result.statusTimeline.map { - GiftStatusTimelineResponse(it.status.name, it.statusName, it.occurredAt) + GiftStatusTimelineResponse(it.status.name, it.statusName, it.occurredAt?.toUtcIso()) } ) } @@ -137,7 +137,7 @@ data class GiftDetailInfoResponse( val applicationNo: String?, val paidCan: Int?, val tracking: String?, - val shippingRequestedAt: LocalDateTime? + val shippingRequestedAt: String? ) data class GiftAddressResponse( @@ -153,15 +153,15 @@ data class GiftMailboxResponse( ) data class GiftDeliveryInfoResponse( - val canceledAt: LocalDateTime?, - val undeliverableAt: LocalDateTime?, + val canceledAt: String?, + val undeliverableAt: String?, val undeliverableReason: String? ) data class GiftStatusTimelineResponse( val status: String, val statusName: String, - val occurredAt: LocalDateTime? + val occurredAt: String? ) data class GiftApplicationResponse( @@ -169,7 +169,7 @@ data class GiftApplicationResponse( val status: String, val statusName: String, val priceCan: Int, - val trackingDeadlineAt: LocalDateTime + val trackingDeadlineAt: String ) { companion object { fun from(result: GiftApplicationResult) = GiftApplicationResponse( @@ -177,7 +177,7 @@ data class GiftApplicationResponse( status = result.status.name, statusName = result.statusName, priceCan = result.priceCan, - trackingDeadlineAt = result.trackingDeadlineAt + trackingDeadlineAt = result.trackingDeadlineAt.toUtcIso() ) } } @@ -187,7 +187,7 @@ data class GiftCancellationResponse( val status: String, val statusName: String, val priceCan: Int, - val canceledAt: LocalDateTime + val canceledAt: String ) { companion object { fun from(result: GiftCancellationResult) = GiftCancellationResponse( @@ -195,7 +195,7 @@ data class GiftCancellationResponse( status = result.status.name, statusName = result.statusName, priceCan = result.priceCan, - canceledAt = result.canceledAt + canceledAt = result.canceledAt.toUtcIso() ) } } @@ -206,8 +206,8 @@ data class GiftTrackingRegistrationResponse( val statusName: String, val courierCompanyName: String, val trackingNumber: String, - val trackingRegisteredAt: LocalDateTime, - val recipientAddressDeadlineAt: LocalDateTime + val trackingRegisteredAt: String, + val recipientAddressDeadlineAt: String ) { companion object { fun from(result: GiftTrackingRegistrationResult) = GiftTrackingRegistrationResponse( @@ -216,8 +216,8 @@ data class GiftTrackingRegistrationResponse( statusName = result.statusName, courierCompanyName = result.courierCompanyName, trackingNumber = result.trackingNumber, - trackingRegisteredAt = result.trackingRegisteredAt, - recipientAddressDeadlineAt = result.recipientAddressDeadlineAt + trackingRegisteredAt = result.trackingRegisteredAt.toUtcIso(), + recipientAddressDeadlineAt = result.recipientAddressDeadlineAt.toUtcIso() ) } } @@ -226,14 +226,14 @@ data class GiftRecipientAddressRegistrationResponse( val applicationNo: String, val status: String, val statusName: String, - val recipientAddressRegisteredAt: LocalDateTime + val recipientAddressRegisteredAt: String ) { companion object { fun from(result: GiftRecipientAddressRegistrationResult) = GiftRecipientAddressRegistrationResponse( applicationNo = result.applicationNo, status = result.status.name, statusName = result.statusName, - recipientAddressRegisteredAt = result.recipientAddressRegisteredAt + recipientAddressRegisteredAt = result.recipientAddressRegisteredAt.toUtcIso() ) } } @@ -242,14 +242,14 @@ data class GiftDeliveryConfirmationResponse( val applicationNo: String, val status: String, val statusName: String, - val deliveredAt: LocalDateTime + val deliveredAt: String ) { companion object { fun from(result: GiftDeliveryConfirmationResult) = GiftDeliveryConfirmationResponse( applicationNo = result.applicationNo, status = result.status.name, statusName = result.statusName, - deliveredAt = result.deliveredAt + deliveredAt = result.deliveredAt.toUtcIso() ) } } @@ -260,7 +260,7 @@ data class GiftReviewResponse( val rating: Int, val keywords: List, val comment: String?, - val createdAt: LocalDateTime? + val createdAt: String? ) { companion object { fun from(result: GiftReviewResult) = GiftReviewResponse( @@ -269,7 +269,7 @@ data class GiftReviewResponse( rating = result.rating, keywords = result.keywords, comment = result.comment, - createdAt = result.createdAt + createdAt = result.createdAt?.toUtcIso() ) } } 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 38bd8172..369ac8a2 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 @@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePrice import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository import kr.co.vividnext.sodalive.v2.gift.domain.GiftSize import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus +import org.hamcrest.Matchers.endsWith import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -62,13 +63,24 @@ class AdminGiftControllerTest @Autowired constructor( .contentType(MediaType.APPLICATION_JSON) .content( """ - {"categoryCode":"DOLL","name":"인형","requiresDamageWaiver":true,"isActive":true} + { + "classificationNumber":"100", + "categoryCode":"DOLL", + "name":"인형", + "receiptCode":"A", + "representativeItem":"피규어", + "requiresDamageWaiver":true, + "isActive":true + } """.trimIndent() ) ) .andExpect(status().isOk) .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.classificationNumber").value("100")) .andExpect(jsonPath("$.data.categoryCode").value("DOLL")) + .andExpect(jsonPath("$.data.receiptCode").value("A")) + .andExpect(jsonPath("$.data.representativeItem").value("피규어")) val categoryId = categoryRepository.findByCategoryCode("DOLL")!!.id!! @@ -78,12 +90,23 @@ class AdminGiftControllerTest @Autowired constructor( .contentType(MediaType.APPLICATION_JSON) .content( """ - {"categoryCode":"DOLL","name":"큰 인형","requiresDamageWaiver":false,"isActive":true} + { + "classificationNumber":"101", + "categoryCode":"DOLL", + "name":"큰 인형", + "receiptCode":"B", + "representativeItem":"대형 피규어", + "requiresDamageWaiver":false, + "isActive":true + } """.trimIndent() ) ) .andExpect(status().isOk) + .andExpect(jsonPath("$.data.classificationNumber").value("101")) .andExpect(jsonPath("$.data.name").value("큰 인형")) + .andExpect(jsonPath("$.data.receiptCode").value("B")) + .andExpect(jsonPath("$.data.representativeItem").value("대형 피규어")) mockMvc.perform(get("/api/v2/admin/gift-categories").with(user("admin").roles("ADMIN"))) .andExpect(status().isOk) @@ -132,11 +155,13 @@ class AdminGiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data.applicationNo").value(APPLICATION_NO)) .andExpect(jsonPath("$.data.status").value("ARRIVED_AT_MAILBOX")) + .andExpect(jsonPath("$.data.occurredAt").value(endsWith("Z"))) mockMvc.perform(post("/api/v2/admin/gifts/$APPLICATION_NO/complete-inspection").with(user("admin").roles("ADMIN"))) .andExpect(status().isOk) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data.status").value("INSPECTION_COMPLETED")) + .andExpect(jsonPath("$.data.occurredAt").value(endsWith("Z"))) } @Test @@ -154,6 +179,7 @@ class AdminGiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data.applicationNo").value(APPLICATION_NO)) .andExpect(jsonPath("$.data.status").value("UNDELIVERABLE")) + .andExpect(jsonPath("$.data.occurredAt").value(endsWith("Z"))) } @Test @@ -181,6 +207,7 @@ class AdminGiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data.applicationNo").value(APPLICATION_NO)) .andExpect(jsonPath("$.data.status").value("DELIVERED")) + .andExpect(jsonPath("$.data.occurredAt").value(endsWith("Z"))) } private fun saveGift(status: GiftStatus) { diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftControllerTest.kt index effe8e04..8a195ba9 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftControllerTest.kt @@ -26,6 +26,7 @@ import kr.co.vividnext.sodalive.v2.gift.application.GiftReviewResult import kr.co.vividnext.sodalive.v2.gift.application.GiftTrackingRegistrationResult import kr.co.vividnext.sodalive.v2.gift.domain.GiftSize import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus +import org.hamcrest.Matchers.endsWith import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito @@ -64,8 +65,8 @@ class GiftControllerTest @Autowired constructor( @Test fun `팬은 활성 폼 옵션을 조회하고 categoryCode를 받지 않는다`() { - categoryRepository.save(GiftCategory("DOLL", "인형", true, true)) - categoryRepository.save(GiftCategory("FOOD", "간식", false, false)) + categoryRepository.save(giftCategory("100", "DOLL", "A", "인형", "피규어", true, true)) + categoryRepository.save(giftCategory("200", "FOOD", "B", "간식", "쿠키", false, false)) sizePriceRepository.save(GiftSizePrice(GiftSize.SMALL, 100, 80, true)) mockMvc.perform(get("/api/v2/gifts/form-options").with(user("fan").roles("USER"))) @@ -75,8 +76,11 @@ class GiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.data.sizes[0].name").value("소형")) .andExpect(jsonPath("$.data.sizes[0].basePriceCan").value(100)) .andExpect(jsonPath("$.data.sizes[0].salePriceCan").value(80)) - .andExpect(jsonPath("$.data.categories[0].name").value("인형")) + .andExpect(jsonPath("$.data.categories[0].name").value("인형/피규어")) .andExpect(jsonPath("$.data.categories[0].categoryCode").doesNotExist()) + .andExpect(jsonPath("$.data.categories[0].classificationNumber").doesNotExist()) + .andExpect(jsonPath("$.data.categories[0].receiptCode").doesNotExist()) + .andExpect(jsonPath("$.data.categories[0].representativeItem").doesNotExist()) } @Test @@ -113,7 +117,7 @@ class GiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.data.items[0].priceCan").value(80)) .andExpect(jsonPath("$.data.items[0].categoryName").value("인형")) .andExpect(jsonPath("$.data.items[0].sizeName").value("소형")) - .andExpect(jsonPath("$.data.items[0].createdAt").exists()) + .andExpect(jsonPath("$.data.items[0].createdAt").value(endsWith("Z"))) } @Test @@ -161,7 +165,11 @@ class GiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.data.recipientAddress").isEmpty) .andExpect(jsonPath("$.data.trackingRequired").value(false)) .andExpect(jsonPath("$.data.statusTimeline[0].status").value("RECEIVED")) - .andExpect(jsonPath("$.data.statusTimeline[1].occurredAt").value("2026-09-30T12:00:00")) + .andExpect(jsonPath("$.data.recipientAddressDeadlineAt").isEmpty) + .andExpect(jsonPath("$.data.giftInfo.shippingRequestedAt").isEmpty) + .andExpect(jsonPath("$.data.delivery.canceledAt").isEmpty) + .andExpect(jsonPath("$.data.delivery.undeliverableAt").isEmpty) + .andExpect(jsonPath("$.data.statusTimeline[1].occurredAt").value("2026-09-30T12:00:00Z")) } @Test @@ -209,7 +217,7 @@ class GiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.data.status").value("RECEIVED")) .andExpect(jsonPath("$.data.statusName").value("접수 완료")) .andExpect(jsonPath("$.data.priceCan").value(80)) - .andExpect(jsonPath("$.data.trackingDeadlineAt").value("2026-10-02T12:00:00")) + .andExpect(jsonPath("$.data.trackingDeadlineAt").value("2026-10-02T12:00:00Z")) } @Test @@ -240,7 +248,7 @@ class GiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.data.status").value("CANCELED")) .andExpect(jsonPath("$.data.statusName").value("신청 취소")) .andExpect(jsonPath("$.data.priceCan").value(80)) - .andExpect(jsonPath("$.data.canceledAt").value("2026-09-29T12:00:00")) + .andExpect(jsonPath("$.data.canceledAt").value("2026-09-29T12:00:00Z")) } @Test @@ -281,8 +289,8 @@ class GiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.data.statusName").value("발송 확인")) .andExpect(jsonPath("$.data.courierCompanyName").value("CJ대한통운")) .andExpect(jsonPath("$.data.trackingNumber").value("1234567890")) - .andExpect(jsonPath("$.data.trackingRegisteredAt").value("2026-09-29T12:00:00")) - .andExpect(jsonPath("$.data.recipientAddressDeadlineAt").value("2026-10-06T12:00:00")) + .andExpect(jsonPath("$.data.trackingRegisteredAt").value("2026-09-29T12:00:00Z")) + .andExpect(jsonPath("$.data.recipientAddressDeadlineAt").value("2026-10-06T12:00:00Z")) } @Test @@ -324,7 +332,7 @@ class GiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.data.applicationNo").value(APPLICATION_NO)) .andExpect(jsonPath("$.data.status").value("TRACKING_REGISTERED")) .andExpect(jsonPath("$.data.statusName").value("발송 확인")) - .andExpect(jsonPath("$.data.recipientAddressRegisteredAt").value("2026-09-29T12:00:00")) + .andExpect(jsonPath("$.data.recipientAddressRegisteredAt").value("2026-09-29T12:00:00Z")) } @Test @@ -350,7 +358,7 @@ class GiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.data.applicationNo").value(APPLICATION_NO)) .andExpect(jsonPath("$.data.status").value("DELIVERED")) .andExpect(jsonPath("$.data.statusName").value("전달완료")) - .andExpect(jsonPath("$.data.deliveredAt").value("2026-09-29T12:00:00")) + .andExpect(jsonPath("$.data.deliveredAt").value("2026-09-29T12:00:00Z")) Mockito.verify(commandService).confirmDelivery(member, APPLICATION_NO) } @@ -391,7 +399,7 @@ class GiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.data.keywords[1]").value("안내가 친절해요")) .andExpect(jsonPath("$.data.keyword").doesNotExist()) .andExpect(jsonPath("$.data.comment").value("선물 전달 과정이 만족스러웠습니다.")) - .andExpect(jsonPath("$.data.createdAt").value("2026-10-11T12:00:00")) + .andExpect(jsonPath("$.data.createdAt").value("2026-10-11T12:00:00Z")) Mockito.verify(commandService).writeReview(member, APPLICATION_NO, request.toCommand()) } @@ -419,6 +427,24 @@ class GiftControllerTest @Autowired constructor( ) ) + private fun giftCategory( + classificationNumber: String, + categoryCode: String, + receiptCode: String, + name: String, + representativeItem: String, + requiresDamageWaiver: Boolean, + isActive: Boolean + ) = GiftCategory( + classificationNumber = classificationNumber, + categoryCode = categoryCode, + name = name, + receiptCode = receiptCode, + representativeItem = representativeItem, + requiresDamageWaiver = requiresDamageWaiver, + isActive = isActive + ) + private companion object { const val SENDER_ID = 1L const val RECIPIENT_ID = 2L