fix(channel-donation): 채널 후원 조회 기간을 월 경계 기준으로 통일한다

This commit is contained in:
2026-02-27 13:57:04 +09:00
parent 44a67f1f0f
commit a85bc67f7a
6 changed files with 165 additions and 25 deletions

View File

@@ -0,0 +1,45 @@
- [x] `getCreatorProfile`의 채널 후원 리스트 조회 경로 식별 (`ExplorerService` -> `ChannelDonationService` -> `ChannelDonationMessageRepository`)
- [x] 프로필 채널 후원 조회 시 조회 월의 1일~말일 범위만 조회되도록 기간 조건 반영
- [x] 기존 일반 채널 후원 목록 API 동작 영향 없는지 확인
- [x] 수정 파일 기준 정적 진단/테스트/빌드 검증 수행
## 검증 기록
### 1차 구현
- 무엇을: 크리에이터 프로필의 채널 후원 리스트 조회 기간을 월 단위로 제한
- 왜: 기존 기간 계산(`now - 1 month`)은 월 경계 기준 요구사항(해당 월 1일~말일)과 다름
- 어떻게:
- `lsp_diagnostics`(수정 파일 2개) 실행 시 `.kt` 확장자용 LSP 서버 미구성으로 도구 진단 불가(환경 제약 확인)
- `./gradlew test --tests "kr.co.vividnext.sodalive.explorer.profile.channelDonation.*"` 실행: 성공
- `./gradlew build` 실행: 성공 (ktlint 포함 전체 빌드 통과)
### 2차 수정
- 무엇을: 프로필 집계 응답뿐 아니라 전체 채널 후원 리스트 API도 월 단위(1일~말일) 조회로 통일
- 왜: 요구사항이 프로필 전용이 아닌 전체 채널 후원 리스트 대상까지 확장됨
- 어떻게:
- `lsp_diagnostics`(수정 파일) 실행 시 `.kt` 확장자용 LSP 서버 미구성으로 도구 진단 불가(환경 제약 확인)
- `./gradlew test --tests "kr.co.vividnext.sodalive.explorer.profile.channelDonation.*"` 실행: 성공
- `./gradlew build` 실행: 성공 (ktlint 포함 전체 빌드 통과)
### 3차 수정
- 무엇을: `endDateTime` nullable 분기와 중복 메서드를 제거하고 기존 조회 메서드 시그니처에 `endDateTime`을 포함해 단일 로직으로 정리
- 왜: `endDateTime`이 항상 존재하는 현재 요구사항에서 null 분기 로직은 불필요하며 유지보수 복잡도만 증가시킴
- 어떻게:
- `lsp_diagnostics`(수정 파일) 실행 시 `.kt` 확장자용 LSP 서버 미구성으로 도구 진단 불가(환경 제약 확인)
- `./gradlew test --tests "kr.co.vividnext.sodalive.explorer.profile.channelDonation.*"` 실행: 성공
- `./gradlew build` 실행: 성공 (ktlint 포함 전체 빌드 통과)
### 4차 수정
- 무엇을: `endDateTime` 도입 이후 테스트 의미를 월 경계 의도에 맞게 보강 (`Service`는 월 시작/종료 전달 검증, `Repository`는 월 범위 기반 필터 검증)
- 왜: 기존 테스트 일부는 단순 파라미터 통과 확인 수준이어서 월 경계 요구사항을 직접 담지 못함
- 어떻게:
- `lsp_diagnostics`(수정 테스트 파일) 실행 시 `.kt` 확장자용 LSP 서버 미구성으로 도구 진단 불가(환경 제약 확인)
- `./gradlew test --tests "kr.co.vividnext.sodalive.explorer.profile.channelDonation.*"` 실행: 성공
- `./gradlew build` 실행: 성공 (ktlint 포함 전체 빌드 통과)
### 5차 수정
- 무엇을: 채널 후원 테스트 2개 파일의 가독성 개선을 위해 `@DisplayName`(한글)과 BDD(`given/when/then`) 단락 설명을 추가
- 왜: 테스트 코드 길이가 길어지며 의도 파악이 어려워져, 시나리오/준비/실행/검증 흐름을 빠르게 읽을 수 있도록 개선 필요
- 어떻게:
- `lsp_diagnostics`(수정 테스트 파일) 실행 시 `.kt` 확장자용 LSP 서버 미구성으로 도구 진단 불가(환경 제약 확인)
- `./gradlew test --tests "kr.co.vividnext.sodalive.explorer.profile.channelDonation.*" && ./gradlew build` 실행: 성공

View File

@@ -395,6 +395,7 @@ class ExplorerService(
limit = 4 limit = 4
) )
// 채널 후원
val channelDonationList = if (isCreator && !isBlock) { val channelDonationList = if (isCreator && !isBlock) {
channelDonationService.getChannelDonationListForProfile( channelDonationService.getChannelDonationListForProfile(
creatorId = creatorId, creatorId = creatorId,

View File

@@ -17,14 +17,16 @@ interface ChannelDonationMessageQueryRepository {
isCreator: Boolean, isCreator: Boolean,
offset: Long, offset: Long,
limit: Long, limit: Long,
startDateTime: LocalDateTime startDateTime: LocalDateTime,
endDateTime: LocalDateTime
): List<ChannelDonationMessage> ): List<ChannelDonationMessage>
fun getChannelDonationMessageTotalCount( fun getChannelDonationMessageTotalCount(
creatorId: Long, creatorId: Long,
memberId: Long, memberId: Long,
isCreator: Boolean, isCreator: Boolean,
startDateTime: LocalDateTime startDateTime: LocalDateTime,
endDateTime: LocalDateTime
): Int ): Int
} }
@@ -37,13 +39,15 @@ class ChannelDonationMessageQueryRepositoryImpl(
isCreator: Boolean, isCreator: Boolean,
offset: Long, offset: Long,
limit: Long, limit: Long,
startDateTime: LocalDateTime startDateTime: LocalDateTime,
endDateTime: LocalDateTime
): List<ChannelDonationMessage> { ): List<ChannelDonationMessage> {
val where = whereCondition( val where = whereCondition(
creatorId = creatorId, creatorId = creatorId,
memberId = memberId, memberId = memberId,
isCreator = isCreator, isCreator = isCreator,
startDateTime = startDateTime startDateTime = startDateTime,
endDateTime = endDateTime
) )
return queryFactory return queryFactory
@@ -62,13 +66,15 @@ class ChannelDonationMessageQueryRepositoryImpl(
creatorId: Long, creatorId: Long,
memberId: Long, memberId: Long,
isCreator: Boolean, isCreator: Boolean,
startDateTime: LocalDateTime startDateTime: LocalDateTime,
endDateTime: LocalDateTime
): Int { ): Int {
val where = whereCondition( val where = whereCondition(
creatorId = creatorId, creatorId = creatorId,
memberId = memberId, memberId = memberId,
isCreator = isCreator, isCreator = isCreator,
startDateTime = startDateTime startDateTime = startDateTime,
endDateTime = endDateTime
) )
return queryFactory return queryFactory
@@ -83,9 +89,11 @@ class ChannelDonationMessageQueryRepositoryImpl(
creatorId: Long, creatorId: Long,
memberId: Long, memberId: Long,
isCreator: Boolean, isCreator: Boolean,
startDateTime: LocalDateTime startDateTime: LocalDateTime,
endDateTime: LocalDateTime
) = channelDonationMessage.creator.id.eq(creatorId) ) = channelDonationMessage.creator.id.eq(creatorId)
.and(channelDonationMessage.createdAt.goe(startDateTime)) .and(channelDonationMessage.createdAt.goe(startDateTime))
.and(channelDonationMessage.createdAt.lt(endDateTime))
.let { .let {
if (isCreator) { if (isCreator) {
it it

View File

@@ -11,8 +11,9 @@ import kr.co.vividnext.sodalive.member.MemberRole
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime import java.time.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.temporal.TemporalAdjusters
@Service @Service
class ChannelDonationService( class ChannelDonationService(
@@ -61,14 +62,18 @@ class ChannelDonationService(
memberRepository.findCreatorByIdOrNull(creatorId) memberRepository.findCreatorByIdOrNull(creatorId)
?: throw SodaException(messageKey = "member.validation.creator_not_found") ?: throw SodaException(messageKey = "member.validation.creator_not_found")
val startDateTime = LocalDateTime.now().minusMonths(1) val startDateTime = LocalDate.now()
.with(TemporalAdjusters.firstDayOfMonth())
.atStartOfDay()
val endDateTime = startDateTime.plusMonths(1)
val isCreator = member.role == MemberRole.CREATOR && creatorId == member.id val isCreator = member.role == MemberRole.CREATOR && creatorId == member.id
val totalCount = channelDonationMessageRepository.getChannelDonationMessageTotalCount( val totalCount = channelDonationMessageRepository.getChannelDonationMessageTotalCount(
creatorId = creatorId, creatorId = creatorId,
memberId = member.id!!, memberId = member.id!!,
isCreator = isCreator, isCreator = isCreator,
startDateTime = startDateTime startDateTime = startDateTime,
endDateTime = endDateTime
) )
val items = channelDonationMessageRepository.getChannelDonationMessageList( val items = channelDonationMessageRepository.getChannelDonationMessageList(
@@ -77,7 +82,8 @@ class ChannelDonationService(
isCreator = isCreator, isCreator = isCreator,
offset = offset, offset = offset,
limit = limit, limit = limit,
startDateTime = startDateTime startDateTime = startDateTime,
endDateTime = endDateTime
).map { ).map {
GetChannelDonationListItem( GetChannelDonationListItem(
id = it.id!!, id = it.id!!,

View File

@@ -5,6 +5,7 @@ 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.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
@@ -20,44 +21,56 @@ class ChannelDonationMessageRepositoryTest @Autowired constructor(
private val entityManager: EntityManager private val entityManager: EntityManager
) { ) {
@Test @Test
@DisplayName("일반 사용자 조회 시 월 범위와 공개/비공개 규칙을 적용하고 최신순으로 정렬한다")
fun shouldFilterByDateAndSortByCreatedAtAndIdDescForViewer() { fun shouldFilterByDateAndSortByCreatedAtAndIdDescForViewer() {
// given: 크리에이터/조회자/타 사용자 데이터를 준비한다.
val creator = saveMember(nickname = "creator", role = MemberRole.CREATOR) val creator = saveMember(nickname = "creator", role = MemberRole.CREATOR)
val viewer = saveMember(nickname = "viewer", role = MemberRole.USER) val viewer = saveMember(nickname = "viewer", role = MemberRole.USER)
val otherUser = saveMember(nickname = "other", role = MemberRole.USER) val otherUser = saveMember(nickname = "other", role = MemberRole.USER)
// given: 조회 기준 월의 시작/종료 시점을 계산한다.
val now = LocalDateTime.now() val now = LocalDateTime.now()
val tieTime = now.minusDays(2) val monthStart = now.withDayOfMonth(1).toLocalDate().atStartOfDay()
val nextMonthStart = monthStart.plusMonths(1)
val tieTime = monthStart.plusDays(2)
// given: 공개/비공개 및 월 범위 경계 확인용 메시지를 저장한다.
val oldPublic = saveMessage(member = viewer, creator = creator, can = 1, isSecret = false) val oldPublic = saveMessage(member = viewer, creator = creator, can = 1, isSecret = false)
val publicTieFirst = saveMessage(member = otherUser, creator = creator, can = 2, isSecret = false) val publicTieFirst = saveMessage(member = otherUser, creator = creator, can = 2, isSecret = false)
val publicTieSecond = saveMessage(member = viewer, creator = creator, can = 3, isSecret = false) val publicTieSecond = saveMessage(member = viewer, creator = creator, can = 3, isSecret = false)
val secretMine = saveMessage(member = viewer, creator = creator, can = 4, isSecret = true) val secretMine = saveMessage(member = viewer, creator = creator, can = 4, isSecret = true)
val secretOther = saveMessage(member = otherUser, creator = creator, can = 5, isSecret = true) val secretOther = saveMessage(member = otherUser, creator = creator, can = 5, isSecret = true)
updateCreatedAt(oldPublic.id!!, now.minusMonths(2)) // given: createdAt을 직접 조정해 정렬/필터 조건을 명확히 만든다.
updateCreatedAt(oldPublic.id!!, monthStart.minusDays(1))
updateCreatedAt(publicTieFirst.id!!, tieTime) updateCreatedAt(publicTieFirst.id!!, tieTime)
updateCreatedAt(publicTieSecond.id!!, tieTime) updateCreatedAt(publicTieSecond.id!!, tieTime)
updateCreatedAt(secretMine.id!!, now.minusDays(1)) updateCreatedAt(secretMine.id!!, monthStart.plusDays(4))
updateCreatedAt(secretOther.id!!, now.minusHours(12)) updateCreatedAt(secretOther.id!!, monthStart.plusDays(5))
entityManager.flush() entityManager.flush()
entityManager.clear() entityManager.clear()
// when: 일반 사용자 기준으로 목록/총건수 조회를 실행한다.
val list = channelDonationMessageRepository.getChannelDonationMessageList( val list = channelDonationMessageRepository.getChannelDonationMessageList(
creatorId = creator.id!!, creatorId = creator.id!!,
memberId = viewer.id!!, memberId = viewer.id!!,
isCreator = false, isCreator = false,
offset = 0, offset = 0,
limit = 10, limit = 10,
startDateTime = now.minusMonths(1) startDateTime = monthStart,
endDateTime = nextMonthStart
) )
// when: 같은 조건으로 총 건수 조회를 실행한다.
val totalCount = channelDonationMessageRepository.getChannelDonationMessageTotalCount( val totalCount = channelDonationMessageRepository.getChannelDonationMessageTotalCount(
creatorId = creator.id!!, creatorId = creator.id!!,
memberId = viewer.id!!, memberId = viewer.id!!,
isCreator = false, isCreator = false,
startDateTime = now.minusMonths(1) startDateTime = monthStart,
endDateTime = nextMonthStart
) )
// then: 비공개 타인 메시지는 제외되고, createdAt desc/id desc 정렬이 적용돼야 한다.
assertEquals(3, list.size) assertEquals(3, list.size)
assertEquals(secretMine.id, list[0].id) assertEquals(secretMine.id, list[0].id)
assertEquals(publicTieSecond.id, list[1].id) assertEquals(publicTieSecond.id, list[1].id)
@@ -66,40 +79,53 @@ class ChannelDonationMessageRepositoryTest @Autowired constructor(
} }
@Test @Test
@DisplayName("크리에이터 본인 조회 시 월 범위 내 비공개 메시지를 모두 조회한다")
fun shouldIncludeAllRecentSecretMessagesForCreator() { fun shouldIncludeAllRecentSecretMessagesForCreator() {
// given: 크리에이터/조회자/타 사용자 데이터를 준비한다.
val creator = saveMember(nickname = "creator2", role = MemberRole.CREATOR) val creator = saveMember(nickname = "creator2", role = MemberRole.CREATOR)
val viewer = saveMember(nickname = "viewer2", role = MemberRole.USER) val viewer = saveMember(nickname = "viewer2", role = MemberRole.USER)
val otherUser = saveMember(nickname = "other2", role = MemberRole.USER) val otherUser = saveMember(nickname = "other2", role = MemberRole.USER)
// given: 조회 기준 월의 시작/종료 시점을 계산한다.
val now = LocalDateTime.now() val now = LocalDateTime.now()
val monthStart = now.withDayOfMonth(1).toLocalDate().atStartOfDay()
val nextMonthStart = monthStart.plusMonths(1)
// given: 월 경계/비공개 노출 검증용 메시지를 저장한다.
val oldPublic = saveMessage(member = viewer, creator = creator, can = 1, isSecret = false) val oldPublic = saveMessage(member = viewer, creator = creator, can = 1, isSecret = false)
val recentPublic = saveMessage(member = viewer, creator = creator, can = 2, isSecret = false) val recentPublic = saveMessage(member = viewer, creator = creator, can = 2, isSecret = false)
val recentSecretMine = saveMessage(member = viewer, creator = creator, can = 3, isSecret = true) val recentSecretMine = saveMessage(member = viewer, creator = creator, can = 3, isSecret = true)
val recentSecretOther = saveMessage(member = otherUser, creator = creator, can = 4, isSecret = true) val recentSecretOther = saveMessage(member = otherUser, creator = creator, can = 4, isSecret = true)
updateCreatedAt(oldPublic.id!!, now.minusMonths(2)) // given: createdAt을 직접 조정해 월 범위 안/밖 데이터를 분리한다.
updateCreatedAt(recentPublic.id!!, now.minusDays(3)) updateCreatedAt(oldPublic.id!!, monthStart.minusDays(1))
updateCreatedAt(recentSecretMine.id!!, now.minusDays(2)) updateCreatedAt(recentPublic.id!!, monthStart.plusDays(2))
updateCreatedAt(recentSecretOther.id!!, now.minusDays(1)) updateCreatedAt(recentSecretMine.id!!, monthStart.plusDays(3))
updateCreatedAt(recentSecretOther.id!!, monthStart.plusDays(4))
entityManager.flush() entityManager.flush()
entityManager.clear() entityManager.clear()
// when: 크리에이터 본인 기준으로 목록/총건수 조회를 실행한다.
val list = channelDonationMessageRepository.getChannelDonationMessageList( val list = channelDonationMessageRepository.getChannelDonationMessageList(
creatorId = creator.id!!, creatorId = creator.id!!,
memberId = creator.id!!, memberId = creator.id!!,
isCreator = true, isCreator = true,
offset = 0, offset = 0,
limit = 10, limit = 10,
startDateTime = now.minusMonths(1) startDateTime = monthStart,
endDateTime = nextMonthStart
) )
// when: 같은 조건으로 총 건수 조회를 실행한다.
val totalCount = channelDonationMessageRepository.getChannelDonationMessageTotalCount( val totalCount = channelDonationMessageRepository.getChannelDonationMessageTotalCount(
creatorId = creator.id!!, creatorId = creator.id!!,
memberId = creator.id!!, memberId = creator.id!!,
isCreator = true, isCreator = true,
startDateTime = now.minusMonths(1) startDateTime = monthStart,
endDateTime = nextMonthStart
) )
// then: 본인 조회는 비공개 메시지를 포함하고 최신순으로 반환해야 한다.
assertEquals(3, list.size) assertEquals(3, list.size)
assertEquals(recentSecretOther.id, list[0].id) assertEquals(recentSecretOther.id, list[0].id)
assertEquals(recentSecretMine.id, list[1].id) assertEquals(recentSecretMine.id, list[1].id)

View File

@@ -10,9 +10,11 @@ import kr.co.vividnext.sodalive.member.MemberRole
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.Mockito import org.mockito.Mockito
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime
class ChannelDonationServiceTest { class ChannelDonationServiceTest {
private lateinit var canPaymentService: CanPaymentService private lateinit var canPaymentService: CanPaymentService
@@ -36,7 +38,9 @@ class ChannelDonationServiceTest {
} }
@Test @Test
@DisplayName("후원 캔 수가 1 미만이면 예외를 던진다")
fun shouldThrowWhenDonateCanIsLessThanOne() { fun shouldThrowWhenDonateCanIsLessThanOne() {
// given: 유효하지 않은 후원 요청(캔 0)을 준비한다.
val member = createMember(id = 10L, role = MemberRole.USER, nickname = "viewer") val member = createMember(id = 10L, role = MemberRole.USER, nickname = "viewer")
val request = PostChannelDonationRequest( val request = PostChannelDonationRequest(
creatorId = 1L, creatorId = 1L,
@@ -46,15 +50,19 @@ class ChannelDonationServiceTest {
container = "aos" container = "aos"
) )
// when: 후원 로직을 실행한다.
val exception = assertThrows(SodaException::class.java) { val exception = assertThrows(SodaException::class.java) {
service.donate(request, member) service.donate(request, member)
} }
// then: 최소 캔 수 검증 메시지 키를 반환해야 한다.
assertEquals("content.donation.error.minimum_can", exception.messageKey) assertEquals("content.donation.error.minimum_can", exception.messageKey)
} }
@Test @Test
@DisplayName("일반 사용자 조회 시 비공개 노출 규칙과 월 범위를 적용한다")
fun shouldPassUserVisibilityFlagToRepositoryWhenRequesterIsNotCreator() { fun shouldPassUserVisibilityFlagToRepositoryWhenRequesterIsNotCreator() {
// given: 크리에이터/조회자/후원 메시지 데이터를 준비한다.
val creator = createMember(id = 1L, role = MemberRole.CREATOR, nickname = "creator") val creator = createMember(id = 1L, role = MemberRole.CREATOR, nickname = "creator")
val viewer = createMember(id = 2L, role = MemberRole.USER, nickname = "viewer") val viewer = createMember(id = 2L, role = MemberRole.USER, nickname = "viewer")
val message = ChannelDonationMessage(can = 3, isSecret = true, additionalMessage = "응원합니다") val message = ChannelDonationMessage(can = 3, isSecret = true, additionalMessage = "응원합니다")
@@ -62,16 +70,24 @@ class ChannelDonationServiceTest {
message.member = viewer message.member = viewer
message.creator = creator message.creator = creator
message.createdAt = LocalDateTime.of(2026, 2, 20, 12, 0, 0) message.createdAt = LocalDateTime.of(2026, 2, 20, 12, 0, 0)
var capturedStartDateTime: LocalDateTime? = null
var capturedEndDateTime: LocalDateTime? = null
// given: repository 응답과 전달 파라미터 캡처를 설정한다.
Mockito.`when`(memberRepository.findCreatorByIdOrNull(creator.id!!)).thenReturn(creator) Mockito.`when`(memberRepository.findCreatorByIdOrNull(creator.id!!)).thenReturn(creator)
Mockito.`when`( Mockito.`when`(
channelDonationMessageRepository.getChannelDonationMessageTotalCount( channelDonationMessageRepository.getChannelDonationMessageTotalCount(
Mockito.eq(creator.id!!), Mockito.eq(creator.id!!),
Mockito.eq(viewer.id!!), Mockito.eq(viewer.id!!),
Mockito.eq(false), Mockito.eq(false),
anyLocalDateTime(),
anyLocalDateTime() anyLocalDateTime()
) )
).thenReturn(1) ).thenAnswer {
capturedStartDateTime = it.getArgument(3)
capturedEndDateTime = it.getArgument(4)
1
}
Mockito.`when`( Mockito.`when`(
channelDonationMessageRepository.getChannelDonationMessageList( channelDonationMessageRepository.getChannelDonationMessageList(
Mockito.eq(creator.id!!), Mockito.eq(creator.id!!),
@@ -79,10 +95,12 @@ class ChannelDonationServiceTest {
Mockito.eq(false), Mockito.eq(false),
Mockito.eq(0L), Mockito.eq(0L),
Mockito.eq(5L), Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime() anyLocalDateTime()
) )
).thenReturn(listOf(message)) ).thenReturn(listOf(message))
// when: 채널 후원 목록 조회를 실행한다.
val result = service.getChannelDonationList( val result = service.getChannelDonationList(
creatorId = creator.id!!, creatorId = creator.id!!,
member = viewer, member = viewer,
@@ -90,14 +108,17 @@ class ChannelDonationServiceTest {
limit = 5 limit = 5
) )
// then: 응답 총 건수/메시지 포맷이 기대값과 일치해야 한다.
assertEquals(1, result.totalCount) assertEquals(1, result.totalCount)
assertEquals(1, result.items.size) assertEquals(1, result.items.size)
assertEquals("3캔을 비밀후원하셨습니다.\n\"응원합니다\"", result.items[0].message) assertEquals("3캔을 비밀후원하셨습니다.\n\"응원합니다\"", result.items[0].message)
// then: 일반 사용자 조회(false) 조건으로 repository를 호출해야 한다.
Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageTotalCount( Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageTotalCount(
Mockito.eq(creator.id!!), Mockito.eq(creator.id!!),
Mockito.eq(viewer.id!!), Mockito.eq(viewer.id!!),
Mockito.eq(false), Mockito.eq(false),
anyLocalDateTime(),
anyLocalDateTime() anyLocalDateTime()
) )
Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageList( Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageList(
@@ -106,12 +127,20 @@ class ChannelDonationServiceTest {
Mockito.eq(false), Mockito.eq(false),
Mockito.eq(0L), Mockito.eq(0L),
Mockito.eq(5L), Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime() anyLocalDateTime()
) )
// then: 월 시작/다음 달 시작 범위를 사용해야 한다.
assertEquals(1, capturedStartDateTime!!.dayOfMonth)
assertEquals(LocalTime.MIDNIGHT, capturedStartDateTime!!.toLocalTime())
assertEquals(capturedStartDateTime!!.plusMonths(1), capturedEndDateTime)
} }
@Test @Test
@DisplayName("후원 캔 수는 천 단위 콤마가 포함된 메시지로 포맷된다")
fun shouldFormatCanWithCommaInDonationMessage() { fun shouldFormatCanWithCommaInDonationMessage() {
// given: 1,000캔 후원 메시지 데이터를 준비한다.
val creator = createMember(id = 1L, role = MemberRole.CREATOR, nickname = "creator") val creator = createMember(id = 1L, role = MemberRole.CREATOR, nickname = "creator")
val viewer = createMember(id = 2L, role = MemberRole.USER, nickname = "viewer") val viewer = createMember(id = 2L, role = MemberRole.USER, nickname = "viewer")
val message = ChannelDonationMessage(can = 1000, isSecret = true, additionalMessage = "응원합니다") val message = ChannelDonationMessage(can = 1000, isSecret = true, additionalMessage = "응원합니다")
@@ -120,12 +149,14 @@ class ChannelDonationServiceTest {
message.creator = creator message.creator = creator
message.createdAt = LocalDateTime.of(2026, 2, 20, 12, 0, 0) message.createdAt = LocalDateTime.of(2026, 2, 20, 12, 0, 0)
// given: repository 응답을 설정한다.
Mockito.`when`(memberRepository.findCreatorByIdOrNull(creator.id!!)).thenReturn(creator) Mockito.`when`(memberRepository.findCreatorByIdOrNull(creator.id!!)).thenReturn(creator)
Mockito.`when`( Mockito.`when`(
channelDonationMessageRepository.getChannelDonationMessageTotalCount( channelDonationMessageRepository.getChannelDonationMessageTotalCount(
Mockito.eq(creator.id!!), Mockito.eq(creator.id!!),
Mockito.eq(viewer.id!!), Mockito.eq(viewer.id!!),
Mockito.eq(false), Mockito.eq(false),
anyLocalDateTime(),
anyLocalDateTime() anyLocalDateTime()
) )
).thenReturn(1) ).thenReturn(1)
@@ -136,10 +167,12 @@ class ChannelDonationServiceTest {
Mockito.eq(false), Mockito.eq(false),
Mockito.eq(0L), Mockito.eq(0L),
Mockito.eq(5L), Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime() anyLocalDateTime()
) )
).thenReturn(listOf(message)) ).thenReturn(listOf(message))
// when: 목록 조회를 실행한다.
val result = service.getChannelDonationList( val result = service.getChannelDonationList(
creatorId = creator.id!!, creatorId = creator.id!!,
member = viewer, member = viewer,
@@ -147,22 +180,33 @@ class ChannelDonationServiceTest {
limit = 5 limit = 5
) )
// then: 후원 메시지에 천 단위 콤마가 포함되어야 한다.
assertEquals("1,000캔을 비밀후원하셨습니다.\n\"응원합니다\"", result.items[0].message) assertEquals("1,000캔을 비밀후원하셨습니다.\n\"응원합니다\"", result.items[0].message)
} }
@Test @Test
@DisplayName("크리에이터 본인 조회 시 creator 플래그와 월 범위를 적용한다")
fun shouldPassCreatorVisibilityFlagToRepositoryWhenRequesterIsCreatorSelf() { fun shouldPassCreatorVisibilityFlagToRepositoryWhenRequesterIsCreatorSelf() {
// given: 조회자와 크리에이터가 동일한 상황을 준비한다.
val creator = createMember(id = 1L, role = MemberRole.CREATOR, nickname = "creator") val creator = createMember(id = 1L, role = MemberRole.CREATOR, nickname = "creator")
var capturedStartDateTime: LocalDateTime? = null
var capturedEndDateTime: LocalDateTime? = null
// given: repository 응답과 전달 파라미터 캡처를 설정한다.
Mockito.`when`(memberRepository.findCreatorByIdOrNull(creator.id!!)).thenReturn(creator) Mockito.`when`(memberRepository.findCreatorByIdOrNull(creator.id!!)).thenReturn(creator)
Mockito.`when`( Mockito.`when`(
channelDonationMessageRepository.getChannelDonationMessageTotalCount( channelDonationMessageRepository.getChannelDonationMessageTotalCount(
Mockito.eq(creator.id!!), Mockito.eq(creator.id!!),
Mockito.eq(creator.id!!), Mockito.eq(creator.id!!),
Mockito.eq(true), Mockito.eq(true),
anyLocalDateTime(),
anyLocalDateTime() anyLocalDateTime()
) )
).thenReturn(0) ).thenAnswer {
capturedStartDateTime = it.getArgument(3)
capturedEndDateTime = it.getArgument(4)
0
}
Mockito.`when`( Mockito.`when`(
channelDonationMessageRepository.getChannelDonationMessageList( channelDonationMessageRepository.getChannelDonationMessageList(
Mockito.eq(creator.id!!), Mockito.eq(creator.id!!),
@@ -170,10 +214,12 @@ class ChannelDonationServiceTest {
Mockito.eq(true), Mockito.eq(true),
Mockito.eq(0L), Mockito.eq(0L),
Mockito.eq(5L), Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime() anyLocalDateTime()
) )
).thenReturn(emptyList()) ).thenReturn(emptyList())
// when: 크리에이터 본인으로 목록 조회를 실행한다.
service.getChannelDonationList( service.getChannelDonationList(
creatorId = creator.id!!, creatorId = creator.id!!,
member = creator, member = creator,
@@ -181,10 +227,12 @@ class ChannelDonationServiceTest {
limit = 5 limit = 5
) )
// then: creator(true) 조건으로 repository를 호출해야 한다.
Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageTotalCount( Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageTotalCount(
Mockito.eq(creator.id!!), Mockito.eq(creator.id!!),
Mockito.eq(creator.id!!), Mockito.eq(creator.id!!),
Mockito.eq(true), Mockito.eq(true),
anyLocalDateTime(),
anyLocalDateTime() anyLocalDateTime()
) )
Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageList( Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageList(
@@ -193,8 +241,14 @@ class ChannelDonationServiceTest {
Mockito.eq(true), Mockito.eq(true),
Mockito.eq(0L), Mockito.eq(0L),
Mockito.eq(5L), Mockito.eq(5L),
anyLocalDateTime(),
anyLocalDateTime() anyLocalDateTime()
) )
// then: 월 시작/다음 달 시작 범위를 사용해야 한다.
assertEquals(1, capturedStartDateTime!!.dayOfMonth)
assertEquals(LocalTime.MIDNIGHT, capturedStartDateTime!!.toLocalTime())
assertEquals(capturedStartDateTime!!.plusMonths(1), capturedEndDateTime)
} }
private fun createMember(id: Long, role: MemberRole, nickname: String): Member { private fun createMember(id: Long, role: MemberRole, nickname: String): Member {