test #340

Merged
klaus merged 11 commits from test into main 2025-09-14 08:51:12 +00:00
3 changed files with 69 additions and 24 deletions
Showing only changes of commit 634bf759ca - Show all commits

View File

@ -1,10 +1,11 @@
package kr.co.vividnext.sodalive.admin.chat.calculate package kr.co.vividnext.sodalive.admin.chat.calculate
import com.querydsl.core.types.Projections
import com.querydsl.core.types.dsl.CaseBuilder import com.querydsl.core.types.dsl.CaseBuilder
import com.querydsl.jpa.impl.JPAQueryFactory import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.QUseCan.useCan 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 kr.co.vividnext.sodalive.chat.character.image.QCharacterImage.characterImage
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@ -34,37 +35,60 @@ class AdminChatCalculateQueryRepository(
.then(useCan.can.add(useCan.rewardCan)) .then(useCan.can.add(useCan.rewardCan))
.otherwise(0) .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 imageSum = imageCanExpr.sum()
val messageSum = messageCanExpr.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 val query = queryFactory
.select( .select(
QChatCharacterCalculateQueryData( Projections.constructor(
chatCharacter.id, ChatCharacterCalculateQueryData::class.java,
chatCharacter.name, characterIdExpr,
chatCharacter.imagePath.prepend("/").prepend(imageHost), characterNameExpr,
characterImagePathExpr.prepend("/").prepend(imageHost),
imageSum, imageSum,
messageSum messageSum,
quotaSum
) )
) )
.from(useCan) .from(useCan)
.innerJoin(useCan.characterImage, characterImage) .leftJoin(useCan.characterImage, characterImage)
.innerJoin(characterImage.chatCharacter, chatCharacter) .leftJoin(characterImage.chatCharacter, c1)
.leftJoin(c2).on(c2.id.eq(useCan.characterId))
.where( .where(
useCan.isRefund.isFalse 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.goe(startUtc))
.and(useCan.createdAt.loe(endInclusiveUtc)) .and(useCan.createdAt.loe(endInclusiveUtc))
) )
.groupBy(chatCharacter.id, chatCharacter.name, chatCharacter.imagePath) .groupBy(characterIdExpr, characterNameExpr, characterImagePathExpr)
when (sort) { when (sort) {
ChatCharacterCalculateSort.TOTAL_SALES_DESC -> ChatCharacterCalculateSort.TOTAL_SALES_DESC ->
query.orderBy(totalSum.desc(), chatCharacter.id.desc()) query.orderBy(totalSum.desc(), characterIdExpr.desc())
ChatCharacterCalculateSort.LATEST_DESC -> ChatCharacterCalculateSort.LATEST_DESC ->
query.orderBy(chatCharacter.id.desc(), totalSum.desc()) query.orderBy(characterIdExpr.desc(), totalSum.desc())
} }
return query return query
@ -77,18 +101,29 @@ class AdminChatCalculateQueryRepository(
startUtc: LocalDateTime, startUtc: LocalDateTime,
endInclusiveUtc: LocalDateTime endInclusiveUtc: LocalDateTime
): Int { ): Int {
val c1 = QChatCharacter("c1")
val c2 = QChatCharacter("c2")
val characterIdExpr = c1.id.coalesce(c2.id)
return queryFactory return queryFactory
.select(chatCharacter.id) .select(characterIdExpr)
.from(useCan) .from(useCan)
.innerJoin(useCan.characterImage, characterImage) .leftJoin(useCan.characterImage, characterImage)
.innerJoin(characterImage.chatCharacter, chatCharacter) .leftJoin(characterImage.chatCharacter, c1)
.leftJoin(c2).on(c2.id.eq(useCan.characterId))
.where( .where(
useCan.isRefund.isFalse 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.goe(startUtc))
.and(useCan.createdAt.loe(endInclusiveUtc)) .and(useCan.createdAt.loe(endInclusiveUtc))
) )
.groupBy(chatCharacter.id) .groupBy(characterIdExpr)
.fetch() .fetch()
.size .size
} }

View File

@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.admin.chat.calculate package kr.co.vividnext.sodalive.admin.chat.calculate
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@ -27,11 +28,16 @@ class AdminChatCalculateService(
val endDate = LocalDate.parse(endDateStr, dateFormatter) val endDate = LocalDate.parse(endDateStr, dateFormatter)
val todayKst = LocalDate.now(kstZone) val todayKst = LocalDate.now(kstZone)
require(!endDate.isAfter(todayKst)) { "끝 날짜는 오늘 날짜까지만 입력 가능합니다." } if (endDate.isAfter(todayKst)) {
require(!startDate.isAfter(endDate)) { "시작 날짜는 끝 날짜보다 이후일 수 없습니다." } throw SodaException("끝 날짜는 오늘 날짜까지만 입력 가능합니다.")
require(!endDate.isAfter(startDate.plusMonths(6))) { "조회 가능 기간은 최대 6개월입니다." } }
if (startDate.isAfter(endDate)) {
throw SodaException("시작 날짜는 끝 날짜보다 이후일 수 없습니다.")
}
if (endDate.isAfter(startDate.plusMonths(6))) {
throw SodaException("조회 가능 기간은 최대 6개월입니다.")
}
// 입력은 KST, UTC로 변환해 [start, end(미포함)] 조회
val startUtc = startDateStr.convertLocalDateTime() val startUtc = startDateStr.convertLocalDateTime()
val endInclusiveUtc = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59) val endInclusiveUtc = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)

View File

@ -17,7 +17,8 @@ data class ChatCharacterCalculateQueryData @QueryProjection constructor(
val characterName: String, val characterName: String,
val characterImagePath: String?, val characterImagePath: String?,
val imagePurchaseCan: Int?, val imagePurchaseCan: Int?,
val messagePurchaseCan: Int? val messagePurchaseCan: Int?,
val quotaPurchaseCan: Int?
) )
// 응답 DTO (아이템) // 응답 DTO (아이템)
@ -27,6 +28,7 @@ data class ChatCharacterCalculateItem(
@JsonProperty("name") val name: String, @JsonProperty("name") val name: String,
@JsonProperty("imagePurchaseCan") val imagePurchaseCan: Int, @JsonProperty("imagePurchaseCan") val imagePurchaseCan: Int,
@JsonProperty("messagePurchaseCan") val messagePurchaseCan: Int, @JsonProperty("messagePurchaseCan") val messagePurchaseCan: Int,
@JsonProperty("quotaPurchaseCan") val quotaPurchaseCan: Int,
@JsonProperty("totalCan") val totalCan: Int, @JsonProperty("totalCan") val totalCan: Int,
@JsonProperty("totalKrw") val totalKrw: Int, @JsonProperty("totalKrw") val totalKrw: Int,
@JsonProperty("settlementKrw") val settlementKrw: Int @JsonProperty("settlementKrw") val settlementKrw: Int
@ -41,7 +43,8 @@ data class ChatCharacterCalculateResponse(
fun ChatCharacterCalculateQueryData.toItem(): ChatCharacterCalculateItem { fun ChatCharacterCalculateQueryData.toItem(): ChatCharacterCalculateItem {
val image = imagePurchaseCan ?: 0 val image = imagePurchaseCan ?: 0
val message = messagePurchaseCan ?: 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 totalKrw = BigDecimal(total).multiply(BigDecimal(100))
val settlement = totalKrw.multiply(BigDecimal("0.10")).setScale(0, RoundingMode.HALF_UP) val settlement = totalKrw.multiply(BigDecimal("0.10")).setScale(0, RoundingMode.HALF_UP)
@ -51,6 +54,7 @@ fun ChatCharacterCalculateQueryData.toItem(): ChatCharacterCalculateItem {
name = characterName, name = characterName,
imagePurchaseCan = image, imagePurchaseCan = image,
messagePurchaseCan = message, messagePurchaseCan = message,
quotaPurchaseCan = quota,
totalCan = total, totalCan = total,
totalKrw = totalKrw.toInt(), totalKrw = totalKrw.toInt(),
settlementKrw = settlement.toInt() settlementKrw = settlement.toInt()