test #340
|
@ -0,0 +1,32 @@
|
||||||
|
package kr.co.vividnext.sodalive.admin.chat.calculate
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
|
import org.springframework.data.domain.Pageable
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
@RequestMapping("/admin/chat/calculate")
|
||||||
|
class AdminChatCalculateController(
|
||||||
|
private val service: AdminChatCalculateService
|
||||||
|
) {
|
||||||
|
@GetMapping("/characters")
|
||||||
|
fun getCharacterCalculate(
|
||||||
|
@RequestParam startDateStr: String,
|
||||||
|
@RequestParam endDateStr: String,
|
||||||
|
@RequestParam(required = false, defaultValue = "TOTAL_SALES_DESC") sort: ChatCharacterCalculateSort,
|
||||||
|
pageable: Pageable
|
||||||
|
) = ApiResponse.ok(
|
||||||
|
service.getCharacterCalculate(
|
||||||
|
startDateStr,
|
||||||
|
endDateStr,
|
||||||
|
sort,
|
||||||
|
pageable.offset,
|
||||||
|
pageable.pageSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package kr.co.vividnext.sodalive.admin.chat.calculate
|
||||||
|
|
||||||
|
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.image.QCharacterImage.characterImage
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class AdminChatCalculateQueryRepository(
|
||||||
|
private val queryFactory: JPAQueryFactory
|
||||||
|
) {
|
||||||
|
fun getCharacterCalculate(
|
||||||
|
startUtc: LocalDateTime,
|
||||||
|
endInclusiveUtc: LocalDateTime,
|
||||||
|
sort: ChatCharacterCalculateSort,
|
||||||
|
offset: Long,
|
||||||
|
limit: Long
|
||||||
|
): List<ChatCharacterCalculateQueryData> {
|
||||||
|
val imageCanExpr = CaseBuilder()
|
||||||
|
.`when`(useCan.canUsage.eq(CanUsage.CHARACTER_IMAGE_PURCHASE))
|
||||||
|
.then(useCan.can.add(useCan.rewardCan))
|
||||||
|
.otherwise(0)
|
||||||
|
|
||||||
|
val messageCanExpr = CaseBuilder()
|
||||||
|
.`when`(useCan.canUsage.eq(CanUsage.CHAT_MESSAGE_PURCHASE))
|
||||||
|
.then(useCan.can.add(useCan.rewardCan))
|
||||||
|
.otherwise(0)
|
||||||
|
|
||||||
|
val imageSum = imageCanExpr.sum()
|
||||||
|
val messageSum = messageCanExpr.sum()
|
||||||
|
val totalSum = imageSum.add(messageSum)
|
||||||
|
|
||||||
|
val query = queryFactory
|
||||||
|
.select(
|
||||||
|
QChatCharacterCalculateQueryData(
|
||||||
|
chatCharacter.id,
|
||||||
|
chatCharacter.name,
|
||||||
|
chatCharacter.imagePath,
|
||||||
|
imageSum,
|
||||||
|
messageSum
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.from(useCan)
|
||||||
|
.innerJoin(useCan.characterImage, characterImage)
|
||||||
|
.innerJoin(characterImage.chatCharacter, chatCharacter)
|
||||||
|
.where(
|
||||||
|
useCan.isRefund.isFalse
|
||||||
|
.and(useCan.canUsage.`in`(CanUsage.CHARACTER_IMAGE_PURCHASE, CanUsage.CHAT_MESSAGE_PURCHASE))
|
||||||
|
.and(useCan.createdAt.goe(startUtc))
|
||||||
|
.and(useCan.createdAt.loe(endInclusiveUtc))
|
||||||
|
)
|
||||||
|
.groupBy(chatCharacter.id, chatCharacter.name, chatCharacter.imagePath)
|
||||||
|
|
||||||
|
when (sort) {
|
||||||
|
ChatCharacterCalculateSort.TOTAL_SALES_DESC ->
|
||||||
|
query.orderBy(totalSum.desc(), chatCharacter.id.desc())
|
||||||
|
|
||||||
|
ChatCharacterCalculateSort.LATEST_DESC ->
|
||||||
|
query.orderBy(chatCharacter.id.desc(), totalSum.desc())
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package kr.co.vividnext.sodalive.admin.chat.calculate
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class AdminChatCalculateService(
|
||||||
|
private val repository: AdminChatCalculateQueryRepository
|
||||||
|
) {
|
||||||
|
private val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||||
|
private val kstZone: ZoneId = ZoneId.of("Asia/Seoul")
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun getCharacterCalculate(
|
||||||
|
startDateStr: String,
|
||||||
|
endDateStr: String,
|
||||||
|
sort: ChatCharacterCalculateSort,
|
||||||
|
offset: Long,
|
||||||
|
pageSize: Int
|
||||||
|
): List<ChatCharacterCalculateItem> {
|
||||||
|
// 날짜 유효성 검증 (KST 기준)
|
||||||
|
val startDate = LocalDate.parse(startDateStr, dateFormatter)
|
||||||
|
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개월입니다." }
|
||||||
|
|
||||||
|
// 입력은 KST, UTC로 변환해 [start, end(미포함)] 조회
|
||||||
|
val startUtc = startDateStr.convertLocalDateTime()
|
||||||
|
val endInclusiveUtc = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
|
||||||
|
|
||||||
|
val rows = repository.getCharacterCalculate(startUtc, endInclusiveUtc, sort, offset, pageSize.toLong())
|
||||||
|
return rows.map { it.toItem() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package kr.co.vividnext.sodalive.admin.chat.calculate
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.querydsl.core.annotations.QueryProjection
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.math.RoundingMode
|
||||||
|
|
||||||
|
// 정렬 옵션
|
||||||
|
enum class ChatCharacterCalculateSort {
|
||||||
|
TOTAL_SALES_DESC,
|
||||||
|
LATEST_DESC
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryDSL 프로젝션용 DTO
|
||||||
|
data class ChatCharacterCalculateQueryData @QueryProjection constructor(
|
||||||
|
val characterId: Long,
|
||||||
|
val characterName: String,
|
||||||
|
val characterImagePath: String?,
|
||||||
|
val imagePurchaseCan: Int?,
|
||||||
|
val messagePurchaseCan: Int?
|
||||||
|
)
|
||||||
|
|
||||||
|
// 응답 DTO
|
||||||
|
data class ChatCharacterCalculateItem(
|
||||||
|
@JsonProperty("characterId") val characterId: Long,
|
||||||
|
@JsonProperty("characterImage") val characterImage: String?,
|
||||||
|
@JsonProperty("name") val name: String,
|
||||||
|
@JsonProperty("imagePurchaseCan") val imagePurchaseCan: Int,
|
||||||
|
@JsonProperty("messagePurchaseCan") val messagePurchaseCan: Int,
|
||||||
|
@JsonProperty("totalCan") val totalCan: Int,
|
||||||
|
@JsonProperty("totalKrw") val totalKrw: Int,
|
||||||
|
@JsonProperty("settlementKrw") val settlementKrw: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ChatCharacterCalculateQueryData.toItem(): ChatCharacterCalculateItem {
|
||||||
|
val image = imagePurchaseCan ?: 0
|
||||||
|
val message = messagePurchaseCan ?: 0
|
||||||
|
val total = image + message
|
||||||
|
val totalKrw = BigDecimal(total).multiply(BigDecimal(100))
|
||||||
|
val settlement = totalKrw.multiply(BigDecimal("0.10")).setScale(0, RoundingMode.HALF_UP)
|
||||||
|
|
||||||
|
return ChatCharacterCalculateItem(
|
||||||
|
characterId = characterId,
|
||||||
|
characterImage = characterImagePath,
|
||||||
|
name = characterName,
|
||||||
|
imagePurchaseCan = image,
|
||||||
|
messagePurchaseCan = message,
|
||||||
|
totalCan = total,
|
||||||
|
totalKrw = totalKrw.toInt(),
|
||||||
|
settlementKrw = settlement.toInt()
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue