14
docs/20260227_채널후원후원랭킹반영.md
Normal file
14
docs/20260227_채널후원후원랭킹반영.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
- [x] Explorer 후원랭킹 집계 경로에서 후원 타입 필터 조건을 확인한다.
|
||||||
|
- [x] 크리에이터 프로필 후원랭킹 집계에 `CanUsage.CHANNEL_DONATION`을 반영하도록 쿼리를 수정한다.
|
||||||
|
- [x] 변경 범위와 연관된 테스트/검증(컴파일/테스트)을 실행한다.
|
||||||
|
- [x] 구현 완료 후 체크박스를 갱신하고 검증 기록(무엇을/왜/어떻게)을 남긴다.
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
|
||||||
|
- 1차 구현
|
||||||
|
- 무엇을: `CreatorDonationRankingQueryRepository`의 후원랭킹 조회/총원 집계 조건에 `CanUsage.CHANNEL_DONATION`을 추가했다.
|
||||||
|
- 왜: `ExplorerService.getCreatorProfile`의 후원랭킹이 기존 `DONATION`, `SPIN_ROULETTE`, `LIVE`만 포함해 채널 후원이 누락되고 있었기 때문이다.
|
||||||
|
- 어떻게:
|
||||||
|
- `lsp_diagnostics`로 Kotlin 파일 진단을 시도했지만, 현재 환경에 `.kt` LSP 서버가 없어 도구 기반 진단은 불가했다.
|
||||||
|
- `./gradlew test` 실행 결과: 성공
|
||||||
|
- `./gradlew build -x test` 실행 결과: 성공
|
||||||
17
docs/20260227_최근종료라이브최적화.md
Normal file
17
docs/20260227_최근종료라이브최적화.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# 최근 종료 라이브(getLatestFinishedLive) 최적화
|
||||||
|
|
||||||
|
- [x] `getLatestFinishedLive` 조회를 DB 단계에서 차단 관계(`left join`)로 필터링하도록 변경
|
||||||
|
- [x] 조회 결과를 `GetLatestFinishedLiveResponse`로 QueryProjection 하여 서비스 단 추가 `map` 제거
|
||||||
|
- [x] 회원 차단(`memberBlock`) / 차단해제(`memberUnBlock`) 시 최근 종료 라이브 캐시 무효화 적용
|
||||||
|
- [x] 정적 진단 및 테스트/빌드 검증 수행
|
||||||
|
- [x] 검증 기록 작성
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
|
||||||
|
### 1차 구현
|
||||||
|
- 무엇을: `getLatestFinishedLive`를 서비스 후처리(`filter`/`map`)에서 제거하고, `LiveRoomRepository`에서 `leftJoin(blockMember)` + `blockMember.id.isNull` 조건으로 차단 관계를 DB 단계에서 제외하도록 변경했다. 또한 `GetLatestFinishedLiveResponse`에 `@QueryProjection` 생성자를 추가해 쿼리 결과를 응답 DTO로 바로 생성했다. 마지막으로 `memberBlock`/`memberUnBlock`에서 `getLatestFinishedLive:{memberId}` 캐시를 즉시 evict 하도록 반영했다.
|
||||||
|
- 왜: 기존 로직은 조회 후 애플리케이션 레벨에서 차단 여부를 반복 조회하고 별도 `map`을 수행해 비용이 컸고, 차단/차단해제 직후 최근 종료 라이브 캐시가 TTL 만료 전까지 stale 상태가 될 수 있어 DB 레벨 필터링 및 이벤트성 캐시 무효화가 필요했다.
|
||||||
|
- 어떻게:
|
||||||
|
- `lsp_diagnostics` (대상: `GetLatestFinishedLiveResponse.kt`, `LiveRoomRepository.kt`, `LiveRoomService.kt`, `MemberService.kt`) 실행 결과: **환경상 Kotlin LSP 미구성으로 진단 불가**
|
||||||
|
- `./gradlew test && ./gradlew build` 실행 결과: **성공 (BUILD SUCCESSFUL)**
|
||||||
|
- `./gradlew tasks --all` 실행 결과: **성공 (BUILD SUCCESSFUL)**
|
||||||
45
docs/20260227_크리에이터프로필채널후원조회월범위수정.md
Normal file
45
docs/20260227_크리에이터프로필채널후원조회월범위수정.md
Normal 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` 실행: 성공
|
||||||
@@ -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,
|
||||||
@@ -405,23 +406,6 @@ class ExplorerService(
|
|||||||
listOf()
|
listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 차단한 크리에이터 인지 체크
|
|
||||||
val activitySummary = if (isCreator) {
|
|
||||||
// 활동요약 (라이브 횟수, 라이브 시간, 라이브 참여자, 콘텐츠 수)
|
|
||||||
val liveCount = queryRepository.getLiveCount(creatorId) ?: 0
|
|
||||||
val liveTime = queryRepository.getLiveTime(creatorId)
|
|
||||||
val liveContributorCount = queryRepository.getLiveContributorCount(creatorId) ?: 0
|
|
||||||
val contentCount = queryRepository.getContentCount(creatorId) ?: 0
|
|
||||||
GetCreatorActivitySummary(
|
|
||||||
liveCount = liveCount,
|
|
||||||
liveTime = liveTime,
|
|
||||||
liveContributorCount = liveContributorCount,
|
|
||||||
contentCount = contentCount
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
GetCreatorActivitySummary(0, 0, 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
val seriesList = if (isCreator && !isBlock) {
|
val seriesList = if (isCreator && !isBlock) {
|
||||||
seriesService
|
seriesService
|
||||||
.getSeriesList(
|
.getSeriesList(
|
||||||
@@ -467,7 +451,6 @@ class ExplorerService(
|
|||||||
communityPostList = communityPostList,
|
communityPostList = communityPostList,
|
||||||
channelDonationList = channelDonationList,
|
channelDonationList = channelDonationList,
|
||||||
cheers = cheers,
|
cheers = cheers,
|
||||||
activitySummary = activitySummary,
|
|
||||||
seriesList = seriesList,
|
seriesList = seriesList,
|
||||||
isBlock = isBlock,
|
isBlock = isBlock,
|
||||||
isCreatorRole = isCreator
|
isCreatorRole = isCreator
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ data class GetCreatorProfileResponse(
|
|||||||
val communityPostList: List<GetCommunityPostListResponse>,
|
val communityPostList: List<GetCommunityPostListResponse>,
|
||||||
val channelDonationList: List<GetChannelDonationListItem>,
|
val channelDonationList: List<GetChannelDonationListItem>,
|
||||||
val cheers: GetCheersResponse,
|
val cheers: GetCheersResponse,
|
||||||
val activitySummary: GetCreatorActivitySummary,
|
|
||||||
val seriesList: List<GetSeriesListResponse.SeriesListItem>,
|
val seriesList: List<GetSeriesListResponse.SeriesListItem>,
|
||||||
val isBlock: Boolean,
|
val isBlock: Boolean,
|
||||||
val isCreatorRole: Boolean
|
val isCreatorRole: Boolean
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class CreatorDonationRankingQueryRepository(private val queryFactory: JPAQueryFa
|
|||||||
useCan.canUsage.eq(CanUsage.DONATION)
|
useCan.canUsage.eq(CanUsage.DONATION)
|
||||||
.or(useCan.canUsage.eq(CanUsage.SPIN_ROULETTE))
|
.or(useCan.canUsage.eq(CanUsage.SPIN_ROULETTE))
|
||||||
.or(useCan.canUsage.eq(CanUsage.LIVE))
|
.or(useCan.canUsage.eq(CanUsage.LIVE))
|
||||||
|
.or(useCan.canUsage.eq(CanUsage.CHANNEL_DONATION))
|
||||||
)
|
)
|
||||||
.and(buildDateRangeCondition(startDate, endDate))
|
.and(buildDateRangeCondition(startDate, endDate))
|
||||||
)
|
)
|
||||||
@@ -70,6 +71,7 @@ class CreatorDonationRankingQueryRepository(private val queryFactory: JPAQueryFa
|
|||||||
useCan.canUsage.eq(CanUsage.DONATION)
|
useCan.canUsage.eq(CanUsage.DONATION)
|
||||||
.or(useCan.canUsage.eq(CanUsage.SPIN_ROULETTE))
|
.or(useCan.canUsage.eq(CanUsage.SPIN_ROULETTE))
|
||||||
.or(useCan.canUsage.eq(CanUsage.LIVE))
|
.or(useCan.canUsage.eq(CanUsage.LIVE))
|
||||||
|
.or(useCan.canUsage.eq(CanUsage.CHANNEL_DONATION))
|
||||||
)
|
)
|
||||||
.and(buildDateRangeCondition(startDate, endDate))
|
.and(buildDateRangeCondition(startDate, endDate))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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!!,
|
||||||
|
|||||||
@@ -6,13 +6,6 @@ import kr.co.vividnext.sodalive.extensions.getTimeAgoString
|
|||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
|
|
||||||
data class GetLatestFinishedLiveQueryResponse @QueryProjection constructor(
|
|
||||||
val memberId: Long,
|
|
||||||
val nickname: String,
|
|
||||||
val profileImageUrl: String,
|
|
||||||
val updatedAt: LocalDateTime
|
|
||||||
)
|
|
||||||
|
|
||||||
data class GetLatestFinishedLiveResponse(
|
data class GetLatestFinishedLiveResponse(
|
||||||
@JsonProperty("memberId") val memberId: Long,
|
@JsonProperty("memberId") val memberId: Long,
|
||||||
@JsonProperty("nickname") val nickname: String,
|
@JsonProperty("nickname") val nickname: String,
|
||||||
@@ -20,12 +13,18 @@ data class GetLatestFinishedLiveResponse(
|
|||||||
@JsonProperty("timeAgo") val timeAgo: String,
|
@JsonProperty("timeAgo") val timeAgo: String,
|
||||||
@JsonProperty("dateUtc") val dateUtc: String
|
@JsonProperty("dateUtc") val dateUtc: String
|
||||||
) {
|
) {
|
||||||
constructor(response: GetLatestFinishedLiveQueryResponse) : this(
|
@QueryProjection
|
||||||
response.memberId,
|
constructor(
|
||||||
response.nickname,
|
memberId: Long,
|
||||||
response.profileImageUrl,
|
nickname: String,
|
||||||
response.updatedAt.getTimeAgoString(),
|
profileImageUrl: String,
|
||||||
response.updatedAt
|
updatedAt: LocalDateTime
|
||||||
|
) : this(
|
||||||
|
memberId,
|
||||||
|
nickname,
|
||||||
|
profileImageUrl,
|
||||||
|
updatedAt.getTimeAgoString(),
|
||||||
|
updatedAt
|
||||||
.atZone(ZoneId.of("UTC"))
|
.atZone(ZoneId.of("UTC"))
|
||||||
.toInstant()
|
.toInstant()
|
||||||
.toString()
|
.toString()
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ interface LiveRoomQueryRepository {
|
|||||||
fun getTotalHeartCount(roomId: Long): Int?
|
fun getTotalHeartCount(roomId: Long): Int?
|
||||||
fun getLiveRoomCreatorId(roomId: Long): Long?
|
fun getLiveRoomCreatorId(roomId: Long): Long?
|
||||||
fun getHeartList(roomId: Long): List<GetLiveRoomHeartListItem>
|
fun getHeartList(roomId: Long): List<GetLiveRoomHeartListItem>
|
||||||
fun getLatestFinishedLive(): List<GetLatestFinishedLiveQueryResponse>
|
fun getLatestFinishedLive(memberId: Long?): List<GetLatestFinishedLiveResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
class LiveRoomQueryRepositoryImpl(
|
class LiveRoomQueryRepositoryImpl(
|
||||||
@@ -486,10 +486,16 @@ class LiveRoomQueryRepositoryImpl(
|
|||||||
.fetch()
|
.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLatestFinishedLive(): List<GetLatestFinishedLiveQueryResponse> {
|
override fun getLatestFinishedLive(memberId: Long?): List<GetLatestFinishedLiveResponse> {
|
||||||
return queryFactory
|
var where = liveRoom.isActive.isFalse
|
||||||
|
.and(liveRoom.channelName.isNotNull)
|
||||||
|
.and(liveRoom.updatedAt.goe(LocalDateTime.now().minusWeeks(1)))
|
||||||
|
.and(member.isActive.isTrue)
|
||||||
|
.and(member.role.eq(MemberRole.CREATOR))
|
||||||
|
|
||||||
|
var select = queryFactory
|
||||||
.select(
|
.select(
|
||||||
QGetLatestFinishedLiveQueryResponse(
|
QGetLatestFinishedLiveResponse(
|
||||||
member.id,
|
member.id,
|
||||||
member.nickname,
|
member.nickname,
|
||||||
member.profileImage.prepend("/").prepend(cloudFrontHost),
|
member.profileImage.prepend("/").prepend(cloudFrontHost),
|
||||||
@@ -498,13 +504,24 @@ class LiveRoomQueryRepositoryImpl(
|
|||||||
)
|
)
|
||||||
.from(liveRoom)
|
.from(liveRoom)
|
||||||
.innerJoin(liveRoom.member, member)
|
.innerJoin(liveRoom.member, member)
|
||||||
.where(
|
|
||||||
liveRoom.isActive.isFalse
|
if (memberId != null) {
|
||||||
.and(liveRoom.channelName.isNotNull)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(liveRoom.updatedAt.goe(LocalDateTime.now().minusWeeks(1)))
|
.and(
|
||||||
.and(member.isActive.isTrue)
|
blockMember.member.id.eq(member.id)
|
||||||
.and(member.role.eq(MemberRole.CREATOR))
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
)
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
||||||
|
where = where.and(blockMember.id.isNull)
|
||||||
|
}
|
||||||
|
|
||||||
|
return select
|
||||||
|
.where(where)
|
||||||
.groupBy(member.id)
|
.groupBy(member.id)
|
||||||
.orderBy(liveRoom.updatedAt.max().desc())
|
.orderBy(liveRoom.updatedAt.max().desc())
|
||||||
.limit(20)
|
.limit(20)
|
||||||
|
|||||||
@@ -1444,20 +1444,9 @@ class LiveRoomService(
|
|||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@Cacheable(
|
@Cacheable(
|
||||||
cacheNames = ["cache_ttl_10_minutes"],
|
cacheNames = ["cache_ttl_10_minutes"],
|
||||||
key = "'getLatestFinishedLive:' + (#member ?: 'guest')"
|
key = "'getLatestFinishedLive:' + (#member?.id ?: 'guest')"
|
||||||
)
|
)
|
||||||
fun getLatestFinishedLive(member: Member?): List<GetLatestFinishedLiveResponse> {
|
fun getLatestFinishedLive(member: Member?): List<GetLatestFinishedLiveResponse> {
|
||||||
return repository.getLatestFinishedLive()
|
return repository.getLatestFinishedLive(memberId = member?.id)
|
||||||
.filter {
|
|
||||||
if (member?.id != null) {
|
|
||||||
!blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.memberId) &&
|
|
||||||
!blockMemberRepository.isBlocked(blockedMemberId = it.memberId, memberId = member.id!!)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.map {
|
|
||||||
GetLatestFinishedLiveResponse(response = it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ class MemberService(
|
|||||||
private val tokenLocks: MutableMap<Long, ReentrantReadWriteLock> = mutableMapOf()
|
private val tokenLocks: MutableMap<Long, ReentrantReadWriteLock> = mutableMapOf()
|
||||||
|
|
||||||
private val recommendLiveCacheKeyPrefix = "getRecommendLive:"
|
private val recommendLiveCacheKeyPrefix = "getRecommendLive:"
|
||||||
|
private val latestFinishedLiveCacheKeyPrefix = "getLatestFinishedLive:"
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun signUpV2(request: SignUpRequestV2): SignUpResponse {
|
fun signUpV2(request: SignUpRequestV2): SignUpResponse {
|
||||||
@@ -564,7 +565,11 @@ class MemberService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
evictRecommendLiveCache(memberId)
|
evictRecommendLiveCache(memberId)
|
||||||
blockTargetMemberIds.forEach { evictRecommendLiveCache(it) }
|
evictLatestFinishedLiveCache(memberId)
|
||||||
|
blockTargetMemberIds.forEach {
|
||||||
|
evictRecommendLiveCache(it)
|
||||||
|
evictLatestFinishedLiveCache(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -580,6 +585,8 @@ class MemberService(
|
|||||||
|
|
||||||
evictRecommendLiveCache(memberId)
|
evictRecommendLiveCache(memberId)
|
||||||
evictRecommendLiveCache(request.blockMemberId)
|
evictRecommendLiveCache(request.blockMemberId)
|
||||||
|
evictLatestFinishedLiveCache(memberId)
|
||||||
|
evictLatestFinishedLiveCache(request.blockMemberId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isBlocked(blockedMemberId: Long, memberId: Long) = blockMemberRepository.isBlocked(blockedMemberId, memberId)
|
fun isBlocked(blockedMemberId: Long, memberId: Long) = blockMemberRepository.isBlocked(blockedMemberId, memberId)
|
||||||
@@ -843,6 +850,10 @@ class MemberService(
|
|||||||
cacheManager.getCache("cache_ttl_3_hours")?.evict(recommendLiveCacheKeyPrefix + memberId)
|
cacheManager.getCache("cache_ttl_3_hours")?.evict(recommendLiveCacheKeyPrefix + memberId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun evictLatestFinishedLiveCache(memberId: Long) {
|
||||||
|
cacheManager.getCache("cache_ttl_10_minutes")?.evict(latestFinishedLiveCacheKeyPrefix + memberId)
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun updateMarketingInfo(memberId: Long, adid: String, pid: String): String? {
|
fun updateMarketingInfo(memberId: Long, adid: String, pid: String): String? {
|
||||||
val member = repository.findByIdOrNull(id = memberId)
|
val member = repository.findByIdOrNull(id = memberId)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user