feat(admin-chat-calculate): 캐릭터별 정산 조회 API 추가
This commit is contained in:
		| @@ -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() | ||||
|     ) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user