fix(channel-donation): 관리자 채널후원 정산 조회를 날짜별과 크리에이터별로 분리하고 엑셀 다운로드를 추가한다

This commit is contained in:
2026-03-03 14:42:42 +09:00
parent ad872923ee
commit 1fbad0f2bb
11 changed files with 540 additions and 20 deletions

View File

@@ -1,12 +1,18 @@
package kr.co.vividnext.sodalive.admin.calculate.channelDonation
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.core.io.InputStreamResource
import org.springframework.data.domain.Pageable
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
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
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
@RestController
@PreAuthorize("hasRole('ADMIN')")
@@ -14,6 +20,20 @@ import org.springframework.web.bind.annotation.RestController
class AdminChannelDonationCalculateController(
private val service: AdminChannelDonationCalculateService
) {
@GetMapping("/channel-donation-by-date")
fun getChannelDonationByDate(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
pageable: Pageable
) = ApiResponse.ok(
service.getChannelDonationByDate(
startDateStr = startDateStr,
endDateStr = endDateStr,
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
)
@GetMapping("/channel-donation-by-creator")
fun getChannelDonationByCreator(
@RequestParam startDateStr: String,
@@ -27,4 +47,28 @@ class AdminChannelDonationCalculateController(
limit = pageable.pageSize.toLong()
)
)
@GetMapping("/channel-donation-by-creator/excel")
fun downloadChannelDonationByCreatorExcel(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String
): ResponseEntity<InputStreamResource> {
val encodedFileName = URLEncoder.encode(
"channel-donation-by-creator.xlsx",
StandardCharsets.UTF_8.toString()
).replace("+", "%20")
val headers = HttpHeaders().apply {
add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''$encodedFileName")
}
val response = service.downloadChannelDonationByCreatorExcel(
startDateStr = startDateStr,
endDateStr = endDateStr
)
return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
.body(InputStreamResource(response))
}
}

View File

@@ -16,9 +16,23 @@ import java.time.LocalDateTime
class AdminChannelDonationCalculateQueryRepository(
private val queryFactory: JPAQueryFactory
) {
fun getChannelDonationByDateTotal(
startDate: LocalDateTime,
endDate: LocalDateTime
): GetAdminChannelDonationSettlementTotalQueryData {
return getChannelDonationSettlementTotal(startDate, endDate)
}
fun getChannelDonationByCreatorTotal(
startDate: LocalDateTime,
endDate: LocalDateTime
): GetAdminChannelDonationSettlementTotalQueryData {
return getChannelDonationSettlementTotal(startDate, endDate)
}
private fun getChannelDonationSettlementTotal(
startDate: LocalDateTime,
endDate: LocalDateTime
): GetAdminChannelDonationSettlementTotalQueryData {
return queryFactory
.select(
@@ -39,7 +53,7 @@ class AdminChannelDonationCalculateQueryRepository(
)
}
fun getChannelDonationByCreatorTotalCount(startDate: LocalDateTime, endDate: LocalDateTime): Int {
fun getChannelDonationByDateTotalCount(startDate: LocalDateTime, endDate: LocalDateTime): Int {
val formattedDate = getFormattedDate(useCan.createdAt)
val distinctGroupKey = Expressions.stringTemplate(
"CONCAT({0}, '-', {1})",
@@ -59,7 +73,20 @@ class AdminChannelDonationCalculateQueryRepository(
?: 0
}
fun getChannelDonationByCreator(
fun getChannelDonationByCreatorTotalCount(startDate: LocalDateTime, endDate: LocalDateTime): Int {
return queryFactory
.select(member.id.countDistinct())
.from(useCanCalculate)
.innerJoin(useCanCalculate.useCan, useCan)
.innerJoin(member)
.on(member.id.eq(useCanCalculate.recipientCreatorId))
.where(baseWhereCondition(startDate, endDate))
.fetchOne()
?.toInt()
?: 0
}
fun getChannelDonationByDate(
startDate: LocalDateTime,
endDate: LocalDateTime,
offset: Long,
@@ -88,6 +115,54 @@ class AdminChannelDonationCalculateQueryRepository(
.fetch()
}
fun getChannelDonationByCreator(
startDate: LocalDateTime,
endDate: LocalDateTime,
offset: Long,
limit: Long
): List<GetAdminChannelDonationSettlementByCreatorQueryData> {
return queryFactory
.select(
QGetAdminChannelDonationSettlementByCreatorQueryData(
member.nickname,
useCan.id.countDistinct(),
useCanCalculate.can.sum()
)
)
.from(useCanCalculate)
.innerJoin(useCanCalculate.useCan, useCan)
.innerJoin(member)
.on(member.id.eq(useCanCalculate.recipientCreatorId))
.where(baseWhereCondition(startDate, endDate))
.groupBy(member.id)
.orderBy(member.id.desc())
.offset(offset)
.limit(limit)
.fetch()
}
fun getChannelDonationByCreatorForExcel(
startDate: LocalDateTime,
endDate: LocalDateTime
): List<GetAdminChannelDonationSettlementByCreatorQueryData> {
return queryFactory
.select(
QGetAdminChannelDonationSettlementByCreatorQueryData(
member.nickname,
useCan.id.countDistinct(),
useCanCalculate.can.sum()
)
)
.from(useCanCalculate)
.innerJoin(useCanCalculate.useCan, useCan)
.innerJoin(member)
.on(member.id.eq(useCanCalculate.recipientCreatorId))
.where(baseWhereCondition(startDate, endDate))
.groupBy(member.id)
.orderBy(member.id.desc())
.fetch()
}
private fun baseWhereCondition(
startDate: LocalDateTime,
endDate: LocalDateTime

View File

@@ -1,15 +1,18 @@
package kr.co.vividnext.sodalive.admin.calculate.channelDonation
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@Service
class AdminChannelDonationCalculateService(
private val repository: AdminChannelDonationCalculateQueryRepository
) {
@Transactional(readOnly = true)
fun getChannelDonationByCreator(
fun getChannelDonationByDate(
startDateStr: String,
endDateStr: String,
offset: Long,
@@ -18,12 +21,75 @@ class AdminChannelDonationCalculateService(
val startDate = startDateStr.convertLocalDateTime()
val endDate = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
val total = repository.getChannelDonationByDateTotal(startDate, endDate).toResponseTotal()
val totalCount = repository.getChannelDonationByDateTotalCount(startDate, endDate)
val items = repository
.getChannelDonationByDate(startDate, endDate, offset, limit)
.map { it.toResponseItem() }
return GetAdminChannelDonationSettlementResponse(totalCount, total, items)
}
@Transactional(readOnly = true)
fun getChannelDonationByCreator(
startDateStr: String,
endDateStr: String,
offset: Long,
limit: Long
): GetAdminChannelDonationSettlementByCreatorResponse {
val startDate = startDateStr.convertLocalDateTime()
val endDate = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
val total = repository.getChannelDonationByCreatorTotal(startDate, endDate).toResponseTotal()
val totalCount = repository.getChannelDonationByCreatorTotalCount(startDate, endDate)
val items = repository
.getChannelDonationByCreator(startDate, endDate, offset, limit)
.map { it.toResponseItem() }
return GetAdminChannelDonationSettlementResponse(totalCount, total, items)
return GetAdminChannelDonationSettlementByCreatorResponse(totalCount, total, items)
}
@Transactional(readOnly = true)
fun downloadChannelDonationByCreatorExcel(startDateStr: String, endDateStr: String): ByteArrayInputStream {
val startDate = startDateStr.convertLocalDateTime()
val endDate = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
val items = repository
.getChannelDonationByCreatorForExcel(startDate, endDate)
.map { it.toResponseItem() }
val byteArrayOutputStream = ByteArrayOutputStream()
XSSFWorkbook().use { workbook ->
val sheet = workbook.createSheet("크리에이터별 채널후원 정산")
val header = listOf(
"크리에이터",
"건수",
"총 받은 캔 수",
"원화",
"수수료",
"정산금액",
"원천세",
"입금액"
)
val headerRow = sheet.createRow(0)
header.forEachIndexed { index, value ->
headerRow.createCell(index).setCellValue(value)
}
items.forEachIndexed { index, item ->
val row = sheet.createRow(index + 1)
row.createCell(0).setCellValue(item.creator)
row.createCell(1).setCellValue(item.count.toDouble())
row.createCell(2).setCellValue(item.totalCan.toDouble())
row.createCell(3).setCellValue(item.krw.toDouble())
row.createCell(4).setCellValue(item.fee.toDouble())
row.createCell(5).setCellValue(item.settlementAmount.toDouble())
row.createCell(6).setCellValue(item.withholdingTax.toDouble())
row.createCell(7).setCellValue(item.depositAmount.toDouble())
}
workbook.write(byteArrayOutputStream)
}
return ByteArrayInputStream(byteArrayOutputStream.toByteArray())
}
}

View File

@@ -0,0 +1,14 @@
package kr.co.vividnext.sodalive.admin.calculate.channelDonation
import com.fasterxml.jackson.annotation.JsonProperty
data class GetAdminChannelDonationSettlementByCreatorItem(
@JsonProperty("creator") val creator: String,
@JsonProperty("count") val count: Int,
@JsonProperty("totalCan") val totalCan: Int,
@JsonProperty("krw") val krw: Int,
@JsonProperty("fee") val fee: Int,
@JsonProperty("settlementAmount") val settlementAmount: Int,
@JsonProperty("withholdingTax") val withholdingTax: Int,
@JsonProperty("depositAmount") val depositAmount: Int
)

View File

@@ -0,0 +1,25 @@
package kr.co.vividnext.sodalive.admin.calculate.channelDonation
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.calculate.channelDonation.ChannelDonationSettlementCalculator
data class GetAdminChannelDonationSettlementByCreatorQueryData @QueryProjection constructor(
val creator: String,
val count: Long,
val totalCan: Int?
) {
fun toResponseItem(): GetAdminChannelDonationSettlementByCreatorItem {
val settlement = ChannelDonationSettlementCalculator.calculate(totalCan ?: 0)
return GetAdminChannelDonationSettlementByCreatorItem(
creator = creator,
count = count.toInt(),
totalCan = totalCan ?: 0,
krw = settlement.krw,
fee = settlement.fee,
settlementAmount = settlement.settlementAmount,
withholdingTax = settlement.withholdingTax,
depositAmount = settlement.depositAmount
)
}
}

View File

@@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.admin.calculate.channelDonation
data class GetAdminChannelDonationSettlementByCreatorResponse(
val totalCount: Int,
val total: GetAdminChannelDonationSettlementTotal,
val items: List<GetAdminChannelDonationSettlementByCreatorItem>
)