From c899ed7e21f90e79bed68ddfce7008a05beda988 Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 8 Oct 2026 17:39:02 +0900 Subject: [PATCH] =?UTF-8?q?feat(gift):=20=EC=84=A0=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=B0=B0=EC=86=A1=EC=A7=80=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EB=A5=BC=20=EC=9D=91=EB=8B=B5=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/v2/api/gift/dto/GiftResponse.kt | 6 + .../v2/gift/application/GiftQueryService.kt | 19 +++- .../gift/adapter/in/web/GiftControllerTest.kt | 30 ++++- .../gift/application/GiftQueryServiceTest.kt | 105 +++++++++++++++--- 4 files changed, 139 insertions(+), 21 deletions(-) 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 59496eb8..96b9bf46 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 @@ -62,6 +62,9 @@ data class GiftListResponse( counterpartMemberId = it.counterpartMemberId, counterpartNickname = it.counterpartNickname, counterpartProfileImageUrl = it.counterpartProfileImageUrl, + recipientAddressRegistered = it.recipientAddressRegistered, + recipientAddressDeadlineAt = it.recipientAddressDeadlineAt?.toUtcIso(), + canceledAt = it.canceledAt?.toUtcIso(), createdAt = it.createdAt?.toUtcIso() ) }, @@ -83,6 +86,9 @@ data class GiftListItemResponse( val counterpartMemberId: Long, val counterpartNickname: String, val counterpartProfileImageUrl: String, + val recipientAddressRegistered: Boolean, + val recipientAddressDeadlineAt: String?, + val canceledAt: String?, val createdAt: String? ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryService.kt index 8f9ae340..8d56c415 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryService.kt @@ -46,6 +46,9 @@ data class GiftListItemResult( val counterpartMemberId: Long, val counterpartNickname: String, val counterpartProfileImageUrl: String, + val recipientAddressRegistered: Boolean, + val recipientAddressDeadlineAt: LocalDateTime?, + val canceledAt: LocalDateTime?, val createdAt: LocalDateTime? ) @@ -153,10 +156,13 @@ class GiftQueryService( val membersById = memberRepository.findAllById( gifts.content.flatMap { listOf(it.senderMemberId, it.recipientMemberId) }.toSet() ).associateBy { it.id ?: throw invalidRequest() } + val deliveriesByGiftId = deliveryRepository.findByGiftIdIn( + gifts.content.mapNotNull { it.id } + ).associateBy { it.giftId } return GiftListResult( totalCount = gifts.totalElements, - items = gifts.content.map { it.toGiftListItem(memberId, membersById) }, + items = gifts.content.map { it.toGiftListItem(memberId, membersById, deliveriesByGiftId) }, page = normalizedPage, size = normalizedSize, hasNext = gifts.hasNext() @@ -203,10 +209,16 @@ class GiftQueryService( ) } - private fun Gift.toGiftListItem(memberId: Long, membersById: Map): GiftListItemResult { + private fun Gift.toGiftListItem( + memberId: Long, + membersById: Map, + deliveriesByGiftId: Map + ): GiftListItemResult { val direction = if (senderMemberId == memberId) GiftDirection.SENT else GiftDirection.RECEIVED val counterpartId = if (direction == GiftDirection.SENT) recipientMemberId else senderMemberId val counterpart = membersById[counterpartId] ?: throw invalidRequest() + val delivery = deliveriesByGiftId[id] ?: throw invalidRequest() + val recipientAddressRegistered = delivery.hasCompleteRecipientInformation() return GiftListItemResult( applicationNo = applicationNo, @@ -219,6 +231,9 @@ class GiftQueryService( counterpartMemberId = counterpartId, counterpartNickname = counterpart.nickname, counterpartProfileImageUrl = counterpart.profileImageUrl(), + recipientAddressRegistered = recipientAddressRegistered, + recipientAddressDeadlineAt = delivery.recipientAddressDeadlineAt.takeUnless { recipientAddressRegistered }, + canceledAt = delivery.canceledAt, createdAt = createdAt ) } 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 b84856da..2deec4e5 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 @@ -100,7 +100,18 @@ class GiftControllerTest @Autowired constructor( senderMemberId = member.id!!, recipientMemberId = counterpart.id!!, status = GiftStatus.RECEIVED - ) + ).also { gift -> + giftDeliveryRepository.save( + GiftDelivery( + giftId = gift.id!!, + senderName = "홍길동", + senderPhoneNumber = "01012345678", + senderZipCode = "06234", + senderAddress = "서울시 강남구", + trackingDeadlineAt = LocalDateTime.of(2026, 10, 2, 12, 0) + ) + ) + } saveGift( "G-CONTROLLER-HIDDEN", senderMemberId = counterpart.id!!, @@ -112,7 +123,19 @@ class GiftControllerTest @Autowired constructor( senderMemberId = counterpart.id!!, recipientMemberId = member.id!!, status = GiftStatus.TRACKING_REGISTERED - ) + ).also { gift -> + giftDeliveryRepository.save( + GiftDelivery( + giftId = gift.id!!, + senderName = "홍길동", + senderPhoneNumber = "01012345678", + senderZipCode = "06234", + senderAddress = "서울시 강남구", + trackingDeadlineAt = LocalDateTime.of(2026, 10, 2, 12, 0), + recipientAddressDeadlineAt = LocalDateTime.of(2026, 10, 7, 12, 0) + ) + ) + } mockMvc.perform( get("/api/v2/gifts") @@ -138,6 +161,9 @@ class GiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.data.items[0].counterpartMemberId").value(counterpart.id!!)) .andExpect(jsonPath("$.data.items[0].counterpartNickname").value("counterpart")) .andExpect(jsonPath("$.data.items[0].counterpartProfileImageUrl").value("https://cdn.test/profile/counterpart.png")) + .andExpect(jsonPath("$.data.items[0].recipientAddressRegistered").value(false)) + .andExpect(jsonPath("$.data.items[0].recipientAddressDeadlineAt").value("2026-10-07T12:00:00Z")) + .andExpect(jsonPath("$.data.items[0].canceledAt").isEmpty) .andExpect(jsonPath("$.data.items[0].createdAt").value(endsWith("Z"))) } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryServiceTest.kt index 6eef5e99..8ad5cd41 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryServiceTest.kt @@ -66,11 +66,13 @@ class GiftQueryServiceTest @Autowired constructor( fun shouldListReceivedGiftForSender() { val sender = saveMember("fan") val recipient = saveMember("creator", "profile/creator.png") - saveGift( - "G-SENT-RECEIVED", - senderMemberId = sender.id!!, - recipientMemberId = recipient.id!!, - status = GiftStatus.RECEIVED + saveDelivery( + saveGift( + "G-SENT-RECEIVED", + senderMemberId = sender.id!!, + recipientMemberId = recipient.id!!, + status = GiftStatus.RECEIVED + ) ) val result = queryService().getGifts( @@ -107,11 +109,13 @@ class GiftQueryServiceTest @Autowired constructor( recipientMemberId = recipient.id!!, status = GiftStatus.RECEIVED ) - saveGift( - "G-RECEIVED-VISIBLE", - senderMemberId = visibleSender.id!!, - recipientMemberId = recipient.id!!, - status = GiftStatus.TRACKING_REGISTERED + saveDelivery( + saveGift( + "G-RECEIVED-VISIBLE", + senderMemberId = visibleSender.id!!, + recipientMemberId = recipient.id!!, + status = GiftStatus.TRACKING_REGISTERED + ) ) val result = queryService().getGifts( @@ -136,12 +140,21 @@ class GiftQueryServiceTest @Autowired constructor( val creator = saveMember("creator") val fan = saveMember("fan") val hiddenFan = saveMember("hidden-fan") - saveGift("G-ALL-SENT", senderMemberId = member.id!!, recipientMemberId = creator.id!!, status = GiftStatus.CANCELED) - saveGift( - "G-ALL-RECEIVED", - senderMemberId = fan.id!!, - recipientMemberId = member.id!!, - status = GiftStatus.ARRIVED_AT_MAILBOX + saveDelivery( + saveGift( + "G-ALL-SENT", + senderMemberId = member.id!!, + recipientMemberId = creator.id!!, + status = GiftStatus.CANCELED + ) + ) + saveDelivery( + saveGift( + "G-ALL-RECEIVED", + senderMemberId = fan.id!!, + recipientMemberId = member.id!!, + status = GiftStatus.ARRIVED_AT_MAILBOX + ) ) saveGift("G-ALL-HIDDEN", senderMemberId = hiddenFan.id!!, recipientMemberId = member.id!!, status = GiftStatus.RECEIVED) @@ -163,12 +176,70 @@ class GiftQueryServiceTest @Autowired constructor( ) } + @Test + @DisplayName("선물함 목록은 배송지 입력 여부와 기한, 취소일을 반환한다") + fun shouldListRecipientAddressAndCancellationFields() { + val member = saveMember("creator") + val pendingSender = saveMember("pending-fan") + val completedSender = saveMember("completed-fan") + val canceledRecipient = saveMember("canceled-creator") + val pendingGift = saveGift( + "G-LIST-PENDING", + senderMemberId = pendingSender.id!!, + recipientMemberId = member.id!!, + status = GiftStatus.TRACKING_REGISTERED + ) + saveDelivery(pendingGift).apply { + trackingRegisteredAt = NOW.plusHours(1) + recipientAddressDeadlineAt = NOW.plusDays(7) + } + val completedGift = saveGift( + "G-LIST-COMPLETE", + senderMemberId = completedSender.id!!, + recipientMemberId = member.id!!, + status = GiftStatus.ARRIVED_AT_MAILBOX + ) + saveDelivery(completedGift).apply { + trackingRegisteredAt = NOW.plusHours(1) + arrivedAtMailboxAt = NOW.plusHours(2) + recipientAddressDeadlineAt = NOW.plusDays(7) + recipientName = "수신자" + recipientPhoneNumber = "01098765432" + recipientZipCode = "04524" + recipientAddress = "서울시 중구" + } + val canceledGift = saveGift( + "G-LIST-CANCELED", + senderMemberId = member.id!!, + recipientMemberId = canceledRecipient.id!!, + status = GiftStatus.CANCELED + ) + saveDelivery(canceledGift).canceledAt = NOW.plusHours(3) + + val result = queryService().getGifts( + memberId = member.id!!, + type = GiftListType.ALL, + page = 0, + size = 20 + ) + + val items = result.items.associateBy { it.applicationNo } + assertEquals(false, items.getValue("G-LIST-PENDING").recipientAddressRegistered) + assertEquals(NOW.plusDays(7), items.getValue("G-LIST-PENDING").recipientAddressDeadlineAt) + assertNull(items.getValue("G-LIST-PENDING").canceledAt) + assertEquals(true, items.getValue("G-LIST-COMPLETE").recipientAddressRegistered) + assertNull(items.getValue("G-LIST-COMPLETE").recipientAddressDeadlineAt) + assertEquals(NOW.plusHours(3), items.getValue("G-LIST-CANCELED").canceledAt) + } + @Test @DisplayName("선물함 page와 size는 음수 또는 0이면 기본값으로 보정된다") fun shouldNormalizeGiftListPageAndSize() { val sender = saveMember("fan") val recipient = saveMember("creator") - saveGift("G-PAGING", senderMemberId = sender.id!!, recipientMemberId = recipient.id!!, status = GiftStatus.RECEIVED) + saveDelivery( + saveGift("G-PAGING", senderMemberId = sender.id!!, recipientMemberId = recipient.id!!, status = GiftStatus.RECEIVED) + ) val result = queryService().getGifts( memberId = sender.id!!,