diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/calculate/AdminChatCalculateQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/calculate/AdminChatCalculateQueryRepository.kt index d285902..3eecc5e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/calculate/AdminChatCalculateQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/calculate/AdminChatCalculateQueryRepository.kt @@ -1,10 +1,11 @@ package kr.co.vividnext.sodalive.admin.chat.calculate +import com.querydsl.core.types.Projections import com.querydsl.core.types.dsl.CaseBuilder import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.can.use.QUseCan.useCan -import kr.co.vividnext.sodalive.chat.character.QChatCharacter.chatCharacter +import kr.co.vividnext.sodalive.chat.character.QChatCharacter import kr.co.vividnext.sodalive.chat.character.image.QCharacterImage.characterImage import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Repository @@ -34,37 +35,60 @@ class AdminChatCalculateQueryRepository( .then(useCan.can.add(useCan.rewardCan)) .otherwise(0) + val quotaCanExpr = CaseBuilder() + .`when`(useCan.canUsage.eq(CanUsage.CHAT_QUOTA_PURCHASE)) + .then(useCan.can.add(useCan.rewardCan)) + .otherwise(0) + val imageSum = imageCanExpr.sum() val messageSum = messageCanExpr.sum() - val totalSum = imageSum.add(messageSum) + val quotaSum = quotaCanExpr.sum() + val totalSum = imageSum.add(messageSum).add(quotaSum) + + // 캐릭터 조인: 이미지 경로를 통한 캐릭터(c1) + characterId 직접 지정(c2) + val c1 = QChatCharacter("c1") + val c2 = QChatCharacter("c2") + + val characterIdExpr = c1.id.coalesce(c2.id) + val characterNameExpr = c1.name.coalesce(c2.name) + val characterImagePathExpr = c1.imagePath.coalesce(c2.imagePath) val query = queryFactory .select( - QChatCharacterCalculateQueryData( - chatCharacter.id, - chatCharacter.name, - chatCharacter.imagePath.prepend("/").prepend(imageHost), + Projections.constructor( + ChatCharacterCalculateQueryData::class.java, + characterIdExpr, + characterNameExpr, + characterImagePathExpr.prepend("/").prepend(imageHost), imageSum, - messageSum + messageSum, + quotaSum ) ) .from(useCan) - .innerJoin(useCan.characterImage, characterImage) - .innerJoin(characterImage.chatCharacter, chatCharacter) + .leftJoin(useCan.characterImage, characterImage) + .leftJoin(characterImage.chatCharacter, c1) + .leftJoin(c2).on(c2.id.eq(useCan.characterId)) .where( useCan.isRefund.isFalse - .and(useCan.canUsage.`in`(CanUsage.CHARACTER_IMAGE_PURCHASE, CanUsage.CHAT_MESSAGE_PURCHASE)) + .and( + useCan.canUsage.`in`( + CanUsage.CHARACTER_IMAGE_PURCHASE, + CanUsage.CHAT_MESSAGE_PURCHASE, + CanUsage.CHAT_QUOTA_PURCHASE + ) + ) .and(useCan.createdAt.goe(startUtc)) .and(useCan.createdAt.loe(endInclusiveUtc)) ) - .groupBy(chatCharacter.id, chatCharacter.name, chatCharacter.imagePath) + .groupBy(characterIdExpr, characterNameExpr, characterImagePathExpr) when (sort) { ChatCharacterCalculateSort.TOTAL_SALES_DESC -> - query.orderBy(totalSum.desc(), chatCharacter.id.desc()) + query.orderBy(totalSum.desc(), characterIdExpr.desc()) ChatCharacterCalculateSort.LATEST_DESC -> - query.orderBy(chatCharacter.id.desc(), totalSum.desc()) + query.orderBy(characterIdExpr.desc(), totalSum.desc()) } return query @@ -77,18 +101,29 @@ class AdminChatCalculateQueryRepository( startUtc: LocalDateTime, endInclusiveUtc: LocalDateTime ): Int { + val c1 = QChatCharacter("c1") + val c2 = QChatCharacter("c2") + val characterIdExpr = c1.id.coalesce(c2.id) + return queryFactory - .select(chatCharacter.id) + .select(characterIdExpr) .from(useCan) - .innerJoin(useCan.characterImage, characterImage) - .innerJoin(characterImage.chatCharacter, chatCharacter) + .leftJoin(useCan.characterImage, characterImage) + .leftJoin(characterImage.chatCharacter, c1) + .leftJoin(c2).on(c2.id.eq(useCan.characterId)) .where( useCan.isRefund.isFalse - .and(useCan.canUsage.`in`(CanUsage.CHARACTER_IMAGE_PURCHASE, CanUsage.CHAT_MESSAGE_PURCHASE)) + .and( + useCan.canUsage.`in`( + CanUsage.CHARACTER_IMAGE_PURCHASE, + CanUsage.CHAT_MESSAGE_PURCHASE, + CanUsage.CHAT_QUOTA_PURCHASE + ) + ) .and(useCan.createdAt.goe(startUtc)) .and(useCan.createdAt.loe(endInclusiveUtc)) ) - .groupBy(chatCharacter.id) + .groupBy(characterIdExpr) .fetch() .size } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/calculate/AdminChatCalculateService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/calculate/AdminChatCalculateService.kt index e9b95c4..34593e8 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/calculate/AdminChatCalculateService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/calculate/AdminChatCalculateService.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.admin.chat.calculate +import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.extensions.convertLocalDateTime import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -27,11 +28,16 @@ class AdminChatCalculateService( val endDate = LocalDate.parse(endDateStr, dateFormatter) val todayKst = LocalDate.now(kstZone) - require(!endDate.isAfter(todayKst)) { "끝 날짜는 오늘 날짜까지만 입력 가능합니다." } - require(!startDate.isAfter(endDate)) { "시작 날짜는 끝 날짜보다 이후일 수 없습니다." } - require(!endDate.isAfter(startDate.plusMonths(6))) { "조회 가능 기간은 최대 6개월입니다." } + if (endDate.isAfter(todayKst)) { + throw SodaException("끝 날짜는 오늘 날짜까지만 입력 가능합니다.") + } + if (startDate.isAfter(endDate)) { + throw SodaException("시작 날짜는 끝 날짜보다 이후일 수 없습니다.") + } + if (endDate.isAfter(startDate.plusMonths(6))) { + throw SodaException("조회 가능 기간은 최대 6개월입니다.") + } - // 입력은 KST, UTC로 변환해 [start, end(미포함)] 조회 val startUtc = startDateStr.convertLocalDateTime() val endInclusiveUtc = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/calculate/ChatCharacterCalculateDtos.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/calculate/ChatCharacterCalculateDtos.kt index 8fa38b7..28218f9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/calculate/ChatCharacterCalculateDtos.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/calculate/ChatCharacterCalculateDtos.kt @@ -17,7 +17,8 @@ data class ChatCharacterCalculateQueryData @QueryProjection constructor( val characterName: String, val characterImagePath: String?, val imagePurchaseCan: Int?, - val messagePurchaseCan: Int? + val messagePurchaseCan: Int?, + val quotaPurchaseCan: Int? ) // 응답 DTO (아이템) @@ -27,6 +28,7 @@ data class ChatCharacterCalculateItem( @JsonProperty("name") val name: String, @JsonProperty("imagePurchaseCan") val imagePurchaseCan: Int, @JsonProperty("messagePurchaseCan") val messagePurchaseCan: Int, + @JsonProperty("quotaPurchaseCan") val quotaPurchaseCan: Int, @JsonProperty("totalCan") val totalCan: Int, @JsonProperty("totalKrw") val totalKrw: Int, @JsonProperty("settlementKrw") val settlementKrw: Int @@ -41,7 +43,8 @@ data class ChatCharacterCalculateResponse( fun ChatCharacterCalculateQueryData.toItem(): ChatCharacterCalculateItem { val image = imagePurchaseCan ?: 0 val message = messagePurchaseCan ?: 0 - val total = image + message + val quota = quotaPurchaseCan ?: 0 + val total = image + message + quota val totalKrw = BigDecimal(total).multiply(BigDecimal(100)) val settlement = totalKrw.multiply(BigDecimal("0.10")).setScale(0, RoundingMode.HALF_UP) @@ -51,6 +54,7 @@ fun ChatCharacterCalculateQueryData.toItem(): ChatCharacterCalculateItem { name = characterName, imagePurchaseCan = image, messagePurchaseCan = message, + quotaPurchaseCan = quota, totalCan = total, totalKrw = totalKrw.toInt(), settlementKrw = settlement.toInt()