feat(gift): 선물함 상대 회원 정보를 추가한다

This commit is contained in:
2026-10-01 20:49:28 +09:00
parent 5494a49748
commit c073c48538
4 changed files with 117 additions and 34 deletions
@@ -59,6 +59,9 @@ data class GiftListResponse(
priceCan = it.priceCan, priceCan = it.priceCan,
categoryName = it.categoryName, categoryName = it.categoryName,
sizeName = it.sizeName, sizeName = it.sizeName,
counterpartMemberId = it.counterpartMemberId,
counterpartNickname = it.counterpartNickname,
counterpartProfileImageUrl = it.counterpartProfileImageUrl,
createdAt = it.createdAt?.toUtcIso() createdAt = it.createdAt?.toUtcIso()
) )
}, },
@@ -77,6 +80,9 @@ data class GiftListItemResponse(
val priceCan: Int, val priceCan: Int,
val categoryName: String, val categoryName: String,
val sizeName: String, val sizeName: String,
val counterpartMemberId: Long,
val counterpartNickname: String,
val counterpartProfileImageUrl: String,
val createdAt: String? val createdAt: String?
) )
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.gift.application package kr.co.vividnext.sodalive.v2.gift.application
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.Gift import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.Gift
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
@@ -11,6 +12,7 @@ import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRep
import kr.co.vividnext.sodalive.v2.gift.domain.GiftDirection import kr.co.vividnext.sodalive.v2.gift.domain.GiftDirection
import kr.co.vividnext.sodalive.v2.gift.domain.GiftListType import kr.co.vividnext.sodalive.v2.gift.domain.GiftListType
import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.domain.PageRequest import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort import org.springframework.data.domain.Sort
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
@@ -39,6 +41,9 @@ data class GiftListItemResult(
val priceCan: Int, val priceCan: Int,
val categoryName: String, val categoryName: String,
val sizeName: String, val sizeName: String,
val counterpartMemberId: Long,
val counterpartNickname: String,
val counterpartProfileImageUrl: String,
val createdAt: LocalDateTime? val createdAt: LocalDateTime?
) )
@@ -99,7 +104,9 @@ class GiftQueryService(
private val sizePriceRepository: GiftSizePriceRepository, private val sizePriceRepository: GiftSizePriceRepository,
private val giftRepository: GiftRepository, private val giftRepository: GiftRepository,
private val deliveryRepository: GiftDeliveryRepository, private val deliveryRepository: GiftDeliveryRepository,
private val memberRepository: MemberRepository private val memberRepository: MemberRepository,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getFormOptions() = GiftFormOptionsResult( fun getFormOptions() = GiftFormOptionsResult(
@@ -140,9 +147,13 @@ class GiftQueryService(
) )
} }
val membersById = memberRepository.findAllById(
gifts.content.flatMap { listOf(it.senderMemberId, it.recipientMemberId) }.toSet()
).associateBy { it.id ?: throw invalidRequest() }
return GiftListResult( return GiftListResult(
totalCount = gifts.totalElements, totalCount = gifts.totalElements,
items = gifts.content.map { it.toGiftListItem(memberId) }, items = gifts.content.map { it.toGiftListItem(memberId, membersById) },
page = normalizedPage, page = normalizedPage,
size = normalizedSize, size = normalizedSize,
hasNext = gifts.hasNext() hasNext = gifts.hasNext()
@@ -189,16 +200,31 @@ class GiftQueryService(
) )
} }
private fun Gift.toGiftListItem(memberId: Long) = GiftListItemResult( private fun Gift.toGiftListItem(memberId: Long, membersById: Map<Long, Member>): 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()
return GiftListItemResult(
applicationNo = applicationNo, applicationNo = applicationNo,
direction = if (senderMemberId == memberId) GiftDirection.SENT else GiftDirection.RECEIVED, direction = direction,
status = status, status = status,
statusName = status.displayName, statusName = status.displayName,
priceCan = salePriceCan, priceCan = salePriceCan,
categoryName = categoryNameSnapshot, categoryName = categoryNameSnapshot,
sizeName = sizeCode.displayName, sizeName = sizeCode.displayName,
counterpartMemberId = counterpartId,
counterpartNickname = counterpart.nickname,
counterpartProfileImageUrl = counterpart.profileImageUrl(),
createdAt = createdAt createdAt = createdAt
) )
}
private fun Member.profileImageUrl() = if (profileImage.isNullOrBlank()) {
"$cloudFrontHost/profile/default-profile.png"
} else {
"$cloudFrontHost/$profileImage"
}
private fun Gift.directionFor(memberId: Long) = when { private fun Gift.directionFor(memberId: Long) = when {
senderMemberId == memberId -> GiftDirection.SENT senderMemberId == memberId -> GiftDirection.SENT
@@ -46,7 +46,9 @@ import java.time.LocalDateTime
@SpringBootTest( @SpringBootTest(
properties = [ properties = [
"spring.datasource.url=jdbc:h2:mem:gift-controller;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE" "spring.datasource.url=" +
"jdbc:h2:mem:gift-controller;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
"cloud.aws.cloud-front.host=https://cdn.test"
] ]
) )
@AutoConfigureMockMvc @AutoConfigureMockMvc
@@ -86,13 +88,26 @@ class GiftControllerTest @Autowired constructor(
@Test @Test
@DisplayName("회원은 선물함 목록에서 보낸 선물과 노출 가능한 받은 선물을 페이징 응답으로 조회한다") @DisplayName("회원은 선물함 목록에서 보낸 선물과 노출 가능한 받은 선물을 페이징 응답으로 조회한다")
fun shouldListGifts() { fun shouldListGifts() {
val member = Member(password = "password", nickname = "member").apply { id = 10L } val member = memberRepository.save(Member(password = "password", nickname = "member"))
saveGift("G-CONTROLLER-SENT", senderMemberId = 10L, recipientMemberId = 20L, status = GiftStatus.RECEIVED) val counterpart = memberRepository.save(
saveGift("G-CONTROLLER-HIDDEN", senderMemberId = 20L, recipientMemberId = 10L, status = GiftStatus.RECEIVED) Member(password = "password", nickname = "counterpart", profileImage = "profile/counterpart.png")
)
saveGift(
"G-CONTROLLER-SENT",
senderMemberId = member.id!!,
recipientMemberId = counterpart.id!!,
status = GiftStatus.RECEIVED
)
saveGift(
"G-CONTROLLER-HIDDEN",
senderMemberId = counterpart.id!!,
recipientMemberId = member.id!!,
status = GiftStatus.RECEIVED
)
saveGift( saveGift(
"G-CONTROLLER-VISIBLE", "G-CONTROLLER-VISIBLE",
senderMemberId = 20L, senderMemberId = counterpart.id!!,
recipientMemberId = 10L, recipientMemberId = member.id!!,
status = GiftStatus.TRACKING_REGISTERED status = GiftStatus.TRACKING_REGISTERED
) )
@@ -117,6 +132,9 @@ class GiftControllerTest @Autowired constructor(
.andExpect(jsonPath("$.data.items[0].priceCan").value(80)) .andExpect(jsonPath("$.data.items[0].priceCan").value(80))
.andExpect(jsonPath("$.data.items[0].categoryName").value("인형")) .andExpect(jsonPath("$.data.items[0].categoryName").value("인형"))
.andExpect(jsonPath("$.data.items[0].sizeName").value("소형")) .andExpect(jsonPath("$.data.items[0].sizeName").value("소형"))
.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].createdAt").value(endsWith("Z"))) .andExpect(jsonPath("$.data.items[0].createdAt").value(endsWith("Z")))
} }
@@ -61,10 +61,17 @@ class GiftQueryServiceTest @Autowired constructor(
@Test @Test
@DisplayName("보낸 선물은 접수 직후 선물함에 노출된다") @DisplayName("보낸 선물은 접수 직후 선물함에 노출된다")
fun shouldListReceivedGiftForSender() { fun shouldListReceivedGiftForSender() {
saveGift("G-SENT-RECEIVED", senderMemberId = MEMBER_ID, recipientMemberId = 2L, status = GiftStatus.RECEIVED) val sender = saveMember("fan")
val recipient = saveMember("creator", "profile/creator.png")
saveGift(
"G-SENT-RECEIVED",
senderMemberId = sender.id!!,
recipientMemberId = recipient.id!!,
status = GiftStatus.RECEIVED
)
val result = queryService().getGifts( val result = queryService().getGifts(
memberId = MEMBER_ID, memberId = sender.id!!,
type = GiftListType.SENT, type = GiftListType.SENT,
page = 0, page = 0,
size = 20 size = 20
@@ -79,22 +86,33 @@ class GiftQueryServiceTest @Autowired constructor(
assertEquals(80, result.items.single().priceCan) assertEquals(80, result.items.single().priceCan)
assertEquals("인형", result.items.single().categoryName) assertEquals("인형", result.items.single().categoryName)
assertEquals("소형", result.items.single().sizeName) assertEquals("소형", result.items.single().sizeName)
assertEquals(recipient.id, result.items.single().counterpartMemberId)
assertEquals("creator", result.items.single().counterpartNickname)
assertEquals("https://cdn.test/profile/creator.png", result.items.single().counterpartProfileImageUrl)
assertNotNull(result.items.single().createdAt) assertNotNull(result.items.single().createdAt)
} }
@Test @Test
@DisplayName("받은 선물은 운송장 등록 이후 상태만 선물함에 노출된다") @DisplayName("받은 선물은 운송장 등록 이후 상태만 선물함에 노출된다")
fun shouldExcludeReceivedOnlyGiftAndListVisibleGiftForRecipient() { fun shouldExcludeReceivedOnlyGiftAndListVisibleGiftForRecipient() {
saveGift("G-RECEIVED-HIDDEN", senderMemberId = 2L, recipientMemberId = MEMBER_ID, status = GiftStatus.RECEIVED) val recipient = saveMember("creator")
val hiddenSender = saveMember("hidden-fan")
val visibleSender = saveMember("visible-fan")
saveGift(
"G-RECEIVED-HIDDEN",
senderMemberId = hiddenSender.id!!,
recipientMemberId = recipient.id!!,
status = GiftStatus.RECEIVED
)
saveGift( saveGift(
"G-RECEIVED-VISIBLE", "G-RECEIVED-VISIBLE",
senderMemberId = 3L, senderMemberId = visibleSender.id!!,
recipientMemberId = MEMBER_ID, recipientMemberId = recipient.id!!,
status = GiftStatus.TRACKING_REGISTERED status = GiftStatus.TRACKING_REGISTERED
) )
val result = queryService().getGifts( val result = queryService().getGifts(
memberId = MEMBER_ID, memberId = recipient.id!!,
type = GiftListType.RECEIVED, type = GiftListType.RECEIVED,
page = 0, page = 0,
size = 20 size = 20
@@ -103,22 +121,29 @@ class GiftQueryServiceTest @Autowired constructor(
assertEquals(1L, result.totalCount) assertEquals(1L, result.totalCount)
assertEquals(listOf("G-RECEIVED-VISIBLE"), result.items.map { it.applicationNo }) assertEquals(listOf("G-RECEIVED-VISIBLE"), result.items.map { it.applicationNo })
assertEquals(GiftDirection.RECEIVED, result.items.single().direction) assertEquals(GiftDirection.RECEIVED, result.items.single().direction)
assertEquals(visibleSender.id, result.items.single().counterpartMemberId)
assertEquals("visible-fan", result.items.single().counterpartNickname)
assertEquals("https://cdn.test/profile/default-profile.png", result.items.single().counterpartProfileImageUrl)
} }
@Test @Test
@DisplayName("전체 선물함은 보낸 선물과 노출 가능한 받은 선물을 방향과 함께 반환한다") @DisplayName("전체 선물함은 보낸 선물과 노출 가능한 받은 선물을 방향과 함께 반환한다")
fun shouldListSentAndVisibleReceivedGiftsWithDirectionsForAll() { fun shouldListSentAndVisibleReceivedGiftsWithDirectionsForAll() {
saveGift("G-ALL-SENT", senderMemberId = MEMBER_ID, recipientMemberId = 2L, status = GiftStatus.CANCELED) val member = saveMember("member")
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( saveGift(
"G-ALL-RECEIVED", "G-ALL-RECEIVED",
senderMemberId = 3L, senderMemberId = fan.id!!,
recipientMemberId = MEMBER_ID, recipientMemberId = member.id!!,
status = GiftStatus.ARRIVED_AT_MAILBOX status = GiftStatus.ARRIVED_AT_MAILBOX
) )
saveGift("G-ALL-HIDDEN", senderMemberId = 4L, 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(
memberId = MEMBER_ID, memberId = member.id!!,
type = GiftListType.ALL, type = GiftListType.ALL,
page = 0, page = 0,
size = 20 size = 20
@@ -129,15 +154,21 @@ class GiftQueryServiceTest @Autowired constructor(
mapOf("G-ALL-SENT" to GiftDirection.SENT, "G-ALL-RECEIVED" to GiftDirection.RECEIVED), mapOf("G-ALL-SENT" to GiftDirection.SENT, "G-ALL-RECEIVED" to GiftDirection.RECEIVED),
result.items.associate { it.applicationNo to it.direction } result.items.associate { it.applicationNo to it.direction }
) )
assertEquals(
mapOf("G-ALL-SENT" to creator.id, "G-ALL-RECEIVED" to fan.id),
result.items.associate { it.applicationNo to it.counterpartMemberId }
)
} }
@Test @Test
@DisplayName("선물함 page와 size는 음수 또는 0이면 기본값으로 보정된다") @DisplayName("선물함 page와 size는 음수 또는 0이면 기본값으로 보정된다")
fun shouldNormalizeGiftListPageAndSize() { fun shouldNormalizeGiftListPageAndSize() {
saveGift("G-PAGING", senderMemberId = MEMBER_ID, recipientMemberId = 2L, status = GiftStatus.RECEIVED) val sender = saveMember("fan")
val recipient = saveMember("creator")
saveGift("G-PAGING", senderMemberId = sender.id!!, recipientMemberId = recipient.id!!, status = GiftStatus.RECEIVED)
val result = queryService().getGifts( val result = queryService().getGifts(
memberId = MEMBER_ID, memberId = sender.id!!,
type = GiftListType.SENT, type = GiftListType.SENT,
page = -1, page = -1,
size = 0 size = 0
@@ -379,7 +410,9 @@ class GiftQueryServiceTest @Autowired constructor(
) )
) )
private fun saveMember(nickname: String) = memberRepository.save(Member(password = "password", nickname = nickname)) private fun saveMember(nickname: String, profileImage: String? = null) = memberRepository.save(
Member(password = "password", nickname = nickname, profileImage = profileImage)
)
private fun giftCategory( private fun giftCategory(
classificationNumber: String, classificationNumber: String,
@@ -416,11 +449,11 @@ class GiftQueryServiceTest @Autowired constructor(
sizePriceRepository, sizePriceRepository,
giftRepository, giftRepository,
deliveryRepository, deliveryRepository,
memberRepository memberRepository,
"https://cdn.test"
) )
private companion object { private companion object {
const val MEMBER_ID = 1L
val NOW: LocalDateTime = LocalDateTime.of(2026, 9, 29, 12, 0) val NOW: LocalDateTime = LocalDateTime.of(2026, 9, 29, 12, 0)
} }
} }