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