feat(gift): 선물함 리스트 배송지 상태를 응답한다

This commit is contained in:
2026-10-08 17:39:02 +09:00
parent f5f58e44f8
commit c899ed7e21
4 changed files with 139 additions and 21 deletions
@@ -62,6 +62,9 @@ data class GiftListResponse(
counterpartMemberId = it.counterpartMemberId, counterpartMemberId = it.counterpartMemberId,
counterpartNickname = it.counterpartNickname, counterpartNickname = it.counterpartNickname,
counterpartProfileImageUrl = it.counterpartProfileImageUrl, counterpartProfileImageUrl = it.counterpartProfileImageUrl,
recipientAddressRegistered = it.recipientAddressRegistered,
recipientAddressDeadlineAt = it.recipientAddressDeadlineAt?.toUtcIso(),
canceledAt = it.canceledAt?.toUtcIso(),
createdAt = it.createdAt?.toUtcIso() createdAt = it.createdAt?.toUtcIso()
) )
}, },
@@ -83,6 +86,9 @@ data class GiftListItemResponse(
val counterpartMemberId: Long, val counterpartMemberId: Long,
val counterpartNickname: String, val counterpartNickname: String,
val counterpartProfileImageUrl: String, val counterpartProfileImageUrl: String,
val recipientAddressRegistered: Boolean,
val recipientAddressDeadlineAt: String?,
val canceledAt: String?,
val createdAt: String? val createdAt: String?
) )
@@ -46,6 +46,9 @@ data class GiftListItemResult(
val counterpartMemberId: Long, val counterpartMemberId: Long,
val counterpartNickname: String, val counterpartNickname: String,
val counterpartProfileImageUrl: String, val counterpartProfileImageUrl: String,
val recipientAddressRegistered: Boolean,
val recipientAddressDeadlineAt: LocalDateTime?,
val canceledAt: LocalDateTime?,
val createdAt: LocalDateTime? val createdAt: LocalDateTime?
) )
@@ -153,10 +156,13 @@ class GiftQueryService(
val membersById = memberRepository.findAllById( val membersById = memberRepository.findAllById(
gifts.content.flatMap { listOf(it.senderMemberId, it.recipientMemberId) }.toSet() gifts.content.flatMap { listOf(it.senderMemberId, it.recipientMemberId) }.toSet()
).associateBy { it.id ?: throw invalidRequest() } ).associateBy { it.id ?: throw invalidRequest() }
val deliveriesByGiftId = deliveryRepository.findByGiftIdIn(
gifts.content.mapNotNull { it.id }
).associateBy { it.giftId }
return GiftListResult( return GiftListResult(
totalCount = gifts.totalElements, totalCount = gifts.totalElements,
items = gifts.content.map { it.toGiftListItem(memberId, membersById) }, items = gifts.content.map { it.toGiftListItem(memberId, membersById, deliveriesByGiftId) },
page = normalizedPage, page = normalizedPage,
size = normalizedSize, size = normalizedSize,
hasNext = gifts.hasNext() hasNext = gifts.hasNext()
@@ -203,10 +209,16 @@ class GiftQueryService(
) )
} }
private fun Gift.toGiftListItem(memberId: Long, membersById: Map<Long, Member>): GiftListItemResult { private fun Gift.toGiftListItem(
memberId: Long,
membersById: Map<Long, Member>,
deliveriesByGiftId: Map<Long, GiftDelivery>
): GiftListItemResult {
val direction = if (senderMemberId == memberId) GiftDirection.SENT else GiftDirection.RECEIVED val direction = if (senderMemberId == memberId) GiftDirection.SENT else GiftDirection.RECEIVED
val counterpartId = if (direction == GiftDirection.SENT) recipientMemberId else senderMemberId val counterpartId = if (direction == GiftDirection.SENT) recipientMemberId else senderMemberId
val counterpart = membersById[counterpartId] ?: throw invalidRequest() val counterpart = membersById[counterpartId] ?: throw invalidRequest()
val delivery = deliveriesByGiftId[id] ?: throw invalidRequest()
val recipientAddressRegistered = delivery.hasCompleteRecipientInformation()
return GiftListItemResult( return GiftListItemResult(
applicationNo = applicationNo, applicationNo = applicationNo,
@@ -219,6 +231,9 @@ class GiftQueryService(
counterpartMemberId = counterpartId, counterpartMemberId = counterpartId,
counterpartNickname = counterpart.nickname, counterpartNickname = counterpart.nickname,
counterpartProfileImageUrl = counterpart.profileImageUrl(), counterpartProfileImageUrl = counterpart.profileImageUrl(),
recipientAddressRegistered = recipientAddressRegistered,
recipientAddressDeadlineAt = delivery.recipientAddressDeadlineAt.takeUnless { recipientAddressRegistered },
canceledAt = delivery.canceledAt,
createdAt = createdAt createdAt = createdAt
) )
} }
@@ -100,7 +100,18 @@ class GiftControllerTest @Autowired constructor(
senderMemberId = member.id!!, senderMemberId = member.id!!,
recipientMemberId = counterpart.id!!, recipientMemberId = counterpart.id!!,
status = GiftStatus.RECEIVED 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( saveGift(
"G-CONTROLLER-HIDDEN", "G-CONTROLLER-HIDDEN",
senderMemberId = counterpart.id!!, senderMemberId = counterpart.id!!,
@@ -112,7 +123,19 @@ class GiftControllerTest @Autowired constructor(
senderMemberId = counterpart.id!!, senderMemberId = counterpart.id!!,
recipientMemberId = member.id!!, recipientMemberId = member.id!!,
status = GiftStatus.TRACKING_REGISTERED 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( mockMvc.perform(
get("/api/v2/gifts") 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].counterpartMemberId").value(counterpart.id!!))
.andExpect(jsonPath("$.data.items[0].counterpartNickname").value("counterpart")) .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].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"))) .andExpect(jsonPath("$.data.items[0].createdAt").value(endsWith("Z")))
} }
@@ -66,12 +66,14 @@ class GiftQueryServiceTest @Autowired constructor(
fun shouldListReceivedGiftForSender() { fun shouldListReceivedGiftForSender() {
val sender = saveMember("fan") val sender = saveMember("fan")
val recipient = saveMember("creator", "profile/creator.png") val recipient = saveMember("creator", "profile/creator.png")
saveDelivery(
saveGift( saveGift(
"G-SENT-RECEIVED", "G-SENT-RECEIVED",
senderMemberId = sender.id!!, senderMemberId = sender.id!!,
recipientMemberId = recipient.id!!, recipientMemberId = recipient.id!!,
status = GiftStatus.RECEIVED status = GiftStatus.RECEIVED
) )
)
val result = queryService().getGifts( val result = queryService().getGifts(
memberId = sender.id!!, memberId = sender.id!!,
@@ -107,12 +109,14 @@ class GiftQueryServiceTest @Autowired constructor(
recipientMemberId = recipient.id!!, recipientMemberId = recipient.id!!,
status = GiftStatus.RECEIVED status = GiftStatus.RECEIVED
) )
saveDelivery(
saveGift( saveGift(
"G-RECEIVED-VISIBLE", "G-RECEIVED-VISIBLE",
senderMemberId = visibleSender.id!!, senderMemberId = visibleSender.id!!,
recipientMemberId = recipient.id!!, recipientMemberId = recipient.id!!,
status = GiftStatus.TRACKING_REGISTERED status = GiftStatus.TRACKING_REGISTERED
) )
)
val result = queryService().getGifts( val result = queryService().getGifts(
memberId = recipient.id!!, memberId = recipient.id!!,
@@ -136,13 +140,22 @@ class GiftQueryServiceTest @Autowired constructor(
val creator = saveMember("creator") val creator = saveMember("creator")
val fan = saveMember("fan") val fan = saveMember("fan")
val hiddenFan = saveMember("hidden-fan") val hiddenFan = saveMember("hidden-fan")
saveGift("G-ALL-SENT", senderMemberId = member.id!!, recipientMemberId = creator.id!!, status = GiftStatus.CANCELED) saveDelivery(
saveGift(
"G-ALL-SENT",
senderMemberId = member.id!!,
recipientMemberId = creator.id!!,
status = GiftStatus.CANCELED
)
)
saveDelivery(
saveGift( saveGift(
"G-ALL-RECEIVED", "G-ALL-RECEIVED",
senderMemberId = fan.id!!, senderMemberId = fan.id!!,
recipientMemberId = member.id!!, recipientMemberId = member.id!!,
status = GiftStatus.ARRIVED_AT_MAILBOX status = GiftStatus.ARRIVED_AT_MAILBOX
) )
)
saveGift("G-ALL-HIDDEN", senderMemberId = hiddenFan.id!!, recipientMemberId = member.id!!, status = GiftStatus.RECEIVED) saveGift("G-ALL-HIDDEN", senderMemberId = hiddenFan.id!!, recipientMemberId = member.id!!, status = GiftStatus.RECEIVED)
val result = queryService().getGifts( val result = queryService().getGifts(
@@ -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 @Test
@DisplayName("선물함 page와 size는 음수 또는 0이면 기본값으로 보정된다") @DisplayName("선물함 page와 size는 음수 또는 0이면 기본값으로 보정된다")
fun shouldNormalizeGiftListPageAndSize() { fun shouldNormalizeGiftListPageAndSize() {
val sender = saveMember("fan") val sender = saveMember("fan")
val recipient = saveMember("creator") val recipient = saveMember("creator")
saveDelivery(
saveGift("G-PAGING", senderMemberId = sender.id!!, recipientMemberId = recipient.id!!, status = GiftStatus.RECEIVED) saveGift("G-PAGING", senderMemberId = sender.id!!, recipientMemberId = recipient.id!!, status = GiftStatus.RECEIVED)
)
val result = queryService().getGifts( val result = queryService().getGifts(
memberId = sender.id!!, memberId = sender.id!!,