parent
b9063fb22f
commit
1dec8913c5
|
@ -0,0 +1,28 @@
|
|||
package kr.co.vividnext.sodalive.admin.statistics.member
|
||||
|
||||
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/member/statistics")
|
||||
class AdminMemberStatisticsController(private val service: AdminMemberStatisticsService) {
|
||||
@GetMapping
|
||||
fun getStatistics(
|
||||
@RequestParam startDateStr: String,
|
||||
@RequestParam endDateStr: String,
|
||||
pageable: Pageable
|
||||
) = ApiResponse.ok(
|
||||
service.getStatistics(
|
||||
startDateStr = startDateStr,
|
||||
endDateStr = endDateStr,
|
||||
offset = pageable.offset,
|
||||
limit = pageable.pageSize.toLong()
|
||||
)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package kr.co.vividnext.sodalive.admin.statistics.member
|
||||
|
||||
import com.querydsl.core.types.dsl.DateTimePath
|
||||
import com.querydsl.core.types.dsl.Expressions
|
||||
import com.querydsl.core.types.dsl.StringTemplate
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
|
||||
import kr.co.vividnext.sodalive.can.charge.QCharge.charge
|
||||
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
|
||||
import kr.co.vividnext.sodalive.can.payment.QPayment.payment
|
||||
import kr.co.vividnext.sodalive.member.QMember.member
|
||||
import kr.co.vividnext.sodalive.member.QSignOut.signOut
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Repository
|
||||
class AdminMemberStatisticsRepository(private val queryFactory: JPAQueryFactory) {
|
||||
fun getTotalSignUpCount(startDate: LocalDateTime, endDate: LocalDateTime): Int {
|
||||
return queryFactory
|
||||
.select(member.id)
|
||||
.from(member)
|
||||
.where(
|
||||
member.createdAt.goe(startDate),
|
||||
member.createdAt.loe(endDate)
|
||||
)
|
||||
.fetch()
|
||||
.size
|
||||
}
|
||||
|
||||
fun getTotalSignOutCount(startDate: LocalDateTime, endDate: LocalDateTime): Int {
|
||||
return queryFactory
|
||||
.select(signOut.id)
|
||||
.from(signOut)
|
||||
.where(
|
||||
signOut.createdAt.goe(startDate),
|
||||
signOut.createdAt.loe(endDate)
|
||||
)
|
||||
.fetch()
|
||||
.size
|
||||
}
|
||||
|
||||
fun getSignUpCountInRange(startDate: LocalDateTime, endDate: LocalDateTime): List<DateAndMemberCount> {
|
||||
return queryFactory
|
||||
.select(
|
||||
QDateAndMemberCount(
|
||||
getFormattedDate(member.createdAt),
|
||||
member.id.countDistinct().castToNum(Int::class.java)
|
||||
)
|
||||
)
|
||||
.from(member)
|
||||
.where(
|
||||
member.createdAt.goe(startDate),
|
||||
member.createdAt.loe(endDate)
|
||||
)
|
||||
.groupBy(getFormattedDate(member.createdAt))
|
||||
.fetch()
|
||||
}
|
||||
|
||||
fun getSignOutCountInRange(startDate: LocalDateTime, endDate: LocalDateTime): List<DateAndMemberCount> {
|
||||
return queryFactory
|
||||
.select(
|
||||
QDateAndMemberCount(
|
||||
getFormattedDate(signOut.createdAt),
|
||||
signOut.id.countDistinct().castToNum(Int::class.java)
|
||||
)
|
||||
)
|
||||
.from(signOut)
|
||||
.where(
|
||||
signOut.createdAt.goe(startDate),
|
||||
signOut.createdAt.loe(endDate)
|
||||
)
|
||||
.groupBy(getFormattedDate(signOut.createdAt))
|
||||
.fetch()
|
||||
}
|
||||
|
||||
fun getPaymentMemberCountInRange(startDate: LocalDateTime, endDate: LocalDateTime): List<DateAndMemberCount> {
|
||||
return queryFactory
|
||||
.select(
|
||||
QDateAndMemberCount(
|
||||
getFormattedDate(charge.createdAt),
|
||||
member.id.countDistinct().castToNum(Int::class.java)
|
||||
)
|
||||
)
|
||||
.from(charge)
|
||||
.innerJoin(charge.member, member)
|
||||
.leftJoin(charge.payment, payment)
|
||||
.where(
|
||||
charge.status.eq(ChargeStatus.CHARGE),
|
||||
payment.status.eq(PaymentStatus.COMPLETE),
|
||||
charge.createdAt.goe(startDate),
|
||||
charge.createdAt.loe(endDate)
|
||||
)
|
||||
.groupBy(getFormattedDate(charge.createdAt))
|
||||
.fetch()
|
||||
}
|
||||
|
||||
private fun getFormattedDate(dateTimePath: DateTimePath<LocalDateTime>): StringTemplate {
|
||||
return Expressions.stringTemplate(
|
||||
"DATE_FORMAT({0}, {1})",
|
||||
Expressions.dateTimeTemplate(
|
||||
LocalDateTime::class.java,
|
||||
"CONVERT_TZ({0},{1},{2})",
|
||||
dateTimePath,
|
||||
"UTC",
|
||||
"Asia/Seoul"
|
||||
),
|
||||
"%Y-%m-%d"
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package kr.co.vividnext.sodalive.admin.statistics.member
|
||||
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@Service
|
||||
class AdminMemberStatisticsService(private val repository: AdminMemberStatisticsRepository) {
|
||||
fun getStatistics(
|
||||
startDateStr: String,
|
||||
endDateStr: String,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): GetMemberStatisticsResponse {
|
||||
val dateRange = getPagedDateRange(
|
||||
startDate = startDateStr,
|
||||
endDate = endDateStr,
|
||||
page = (offset + 1).toInt(),
|
||||
pageSize = limit.toInt()
|
||||
)
|
||||
|
||||
if (dateRange == null) {
|
||||
throw SodaException("잘못된 접근입니다.")
|
||||
}
|
||||
|
||||
val startDate = dateRange.startDate
|
||||
.atZone(ZoneId.of("Asia/Seoul"))
|
||||
.withZoneSameInstant(ZoneId.of("UTC"))
|
||||
.toLocalDateTime()
|
||||
|
||||
val endDate = dateRange.endDate
|
||||
.atZone(ZoneId.of("Asia/Seoul"))
|
||||
.withZoneSameInstant(ZoneId.of("UTC"))
|
||||
.toLocalDateTime()
|
||||
|
||||
val totalSignUpCount = repository.getTotalSignUpCount(startDate = startDate, endDate = endDate)
|
||||
val totalSignOutCount = repository.getTotalSignOutCount(startDate = startDate, endDate = endDate)
|
||||
|
||||
val signUpCountInRange = repository.getSignUpCountInRange(startDate = startDate, endDate = endDate)
|
||||
.associateBy({ it.date }, { it.memberCount })
|
||||
|
||||
val signOutCountInRange = repository.getSignOutCountInRange(startDate = startDate, endDate = endDate)
|
||||
.associateBy({ it.date }, { it.memberCount })
|
||||
|
||||
val paymentMemberCountInRange = repository.getPaymentMemberCountInRange(
|
||||
startDate = startDate,
|
||||
endDate = endDate
|
||||
)
|
||||
|
||||
val totalPaymentMemberCount = paymentMemberCountInRange.sumOf { it.memberCount }
|
||||
val paymentMemberCountInRangeMap = paymentMemberCountInRange.associateBy({ it.date }, { it.memberCount })
|
||||
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||
val items = generateSequence(dateRange.startDate) { it.plusDays(1) }
|
||||
.takeWhile { !it.isAfter(dateRange.endDate) }
|
||||
.map {
|
||||
val date = it.format(formatter)
|
||||
GetMemberStatisticsItem(
|
||||
date = date,
|
||||
signUpCount = signUpCountInRange[date] ?: 0,
|
||||
signOutCount = signOutCountInRange[date] ?: 0,
|
||||
paymentMemberCount = paymentMemberCountInRangeMap[date] ?: 0
|
||||
)
|
||||
}
|
||||
.toList()
|
||||
|
||||
return GetMemberStatisticsResponse(
|
||||
totalCount = dateRange.totalDays,
|
||||
totalSignUpCount = totalSignUpCount,
|
||||
totalSignOutCount = totalSignOutCount,
|
||||
totalPaymentMemberCount = totalPaymentMemberCount,
|
||||
items = items
|
||||
)
|
||||
}
|
||||
|
||||
private fun getPagedDateRange(startDate: String, endDate: String, page: Int, pageSize: Int): PagedDateRange? {
|
||||
val start = LocalDate.parse(startDate)
|
||||
val end = LocalDate.parse(endDate)
|
||||
|
||||
val totalDays = ChronoUnit.DAYS.between(start, end).toInt() + 1
|
||||
val totalPages = (totalDays + pageSize - 1) / pageSize // 전체 페이지 개수 계산
|
||||
|
||||
if (page < 1 || page > totalPages) return null // 페이지 범위를 벗어나면 null 반환
|
||||
|
||||
val rangeStart = start.plusDays((page - 1) * pageSize.toLong()).atStartOfDay()
|
||||
val rangeEnd = start.plusDays((page * pageSize - 1).toLong()).coerceAtMost(end).atTime(LocalTime.MAX)
|
||||
|
||||
return PagedDateRange(rangeStart, rangeEnd, totalDays)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package kr.co.vividnext.sodalive.admin.statistics.member
|
||||
|
||||
import com.querydsl.core.annotations.QueryProjection
|
||||
|
||||
data class DateAndMemberCount @QueryProjection constructor(
|
||||
val date: String,
|
||||
val memberCount: Int
|
||||
)
|
|
@ -0,0 +1,16 @@
|
|||
package kr.co.vividnext.sodalive.admin.statistics.member
|
||||
|
||||
data class GetMemberStatisticsResponse(
|
||||
val totalCount: Int,
|
||||
val totalSignUpCount: Int,
|
||||
val totalSignOutCount: Int,
|
||||
val totalPaymentMemberCount: Int,
|
||||
val items: List<GetMemberStatisticsItem>
|
||||
)
|
||||
|
||||
data class GetMemberStatisticsItem(
|
||||
val date: String,
|
||||
val signUpCount: Int,
|
||||
val signOutCount: Int,
|
||||
val paymentMemberCount: Int
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
package kr.co.vividnext.sodalive.admin.statistics.member
|
||||
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class PagedDateRange(
|
||||
val startDate: LocalDateTime,
|
||||
val endDate: LocalDateTime,
|
||||
val totalDays: Int
|
||||
)
|
Loading…
Reference in New Issue