Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.side.tiggle.domain.scheduler.model

import com.fasterxml.jackson.annotation.JsonIgnore
import com.side.tiggle.domain.category.model.Category
import com.side.tiggle.domain.member.model.Member
import java.time.LocalDate
import javax.persistence.*

@Entity
@Table(name = "monthly_stats")
class MonthlyStats(
@JsonIgnore
@JoinColumn(name = "member_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
val member: Member,
val monthlyStart: LocalDate,
val monthlyEnd: LocalDate,
val totalAmount: Int,
val highestAmount: Int,
val lowestAmount: Int,
@JsonIgnore
@JoinColumn(name = "most_frequent_category_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
val category: Category
){
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.side.tiggle.domain.scheduler.model

import com.side.tiggle.domain.category.model.Category
import com.side.tiggle.domain.member.model.Member
import java.time.LocalDate
import javax.persistence.*

@Entity
@Table(name = "weekly_stats")
class WeeklyStats(
@JoinColumn(name = "member_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
val member: Member,
val weeklyStart: LocalDate,
val weeklyEnd: LocalDate,
val totalAmount: Int,
val highestAmount: Int,
val lowestAmount: Int,
@JoinColumn(name = "most_frequent_category_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
val category: Category
){
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.side.tiggle.domain.scheduler.repository

import com.side.tiggle.domain.scheduler.model.MonthlyStats
import org.springframework.data.jpa.repository.JpaRepository

interface MonthlyStatsRepository: JpaRepository<MonthlyStats, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.side.tiggle.domain.scheduler.repository

import com.side.tiggle.domain.scheduler.model.WeeklyStats
import org.springframework.data.jpa.repository.JpaRepository

interface WeeklyStatsRepository: JpaRepository<WeeklyStats, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.side.tiggle.domain.scheduler.service

import com.side.tiggle.domain.transaction.repository.TransactionRepository
import org.springframework.stereotype.Service
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth

@Service
class JobService(
private val txRepository: TransactionRepository,
private val statsService: StatsService
) {

fun runNowJob() {
// 현재 작업 로직
val (weeklyStart, weeklyEnd) = getDate("weekly", LocalDate.now())
val weeklyMemberTxsMap = txRepository.findByDateBetweenOrderByDateAsc(weeklyStart, weeklyEnd).groupBy { it.member.id }
println("시작일 : ${weeklyStart}, 종료일 : ${weeklyEnd}")
statsService.calculateAndSaveStats(weeklyMemberTxsMap, weeklyStart, weeklyEnd)
}

fun generateWeeklySummary() {
val (weeklyStart, weeklyEnd) = getDate("weekly", LocalDate.now())
val weeklyMemberTxsMap = txRepository.findByDateBetweenOrderByDateAsc(weeklyStart, weeklyEnd).groupBy { it.member.id }
}

fun generateMonthlySummary() {

println("Montly summary is running...")

val (monthlyStart, monthlyEnd) = getDate("monthly", LocalDate.now())
val monthlyMemberTxsMap = txRepository.findByDateBetweenOrderByDateAsc(monthlyStart, monthlyEnd).groupBy { it.member.id }
}

fun getDate(type: String, today: LocalDate): Pair<LocalDate, LocalDate> {
return when (type) {
"weekly" -> {
val lastMonday = today.minusWeeks(1).with(DayOfWeek.MONDAY)
val lastSunday = today.minusWeeks(1).with(DayOfWeek.SUNDAY)
Pair(lastMonday, lastSunday)
}
"monthly" -> {
val firstDayOfLastMonth = today.minusMonths(1).withDayOfMonth(1)
val lastDayOfLastMonth = YearMonth.from(today.minusMonths(1)).atEndOfMonth()
Pair(firstDayOfLastMonth, lastDayOfLastMonth)
}
else -> throw IllegalStateException("Unknown type $type")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.side.tiggle.domain.scheduler.service

import com.side.tiggle.domain.category.repository.CategoryRepository
import com.side.tiggle.domain.member.repository.MemberRepository
import com.side.tiggle.domain.scheduler.model.MonthlyStats
import com.side.tiggle.domain.scheduler.model.WeeklyStats
import com.side.tiggle.domain.scheduler.repository.MonthlyStatsRepository
import com.side.tiggle.domain.scheduler.repository.WeeklyStatsRepository
import com.side.tiggle.domain.transaction.model.Transaction
import com.side.tiggle.global.exception.NotFoundException
import org.springframework.stereotype.Service
import java.time.LocalDate
import java.time.temporal.ChronoUnit

@Service
class StatsService(
private val weeklyStatsRepository: WeeklyStatsRepository,
private val monthlyStatsRepository: MonthlyStatsRepository,
private val memberRepository: MemberRepository,
private val categoryRepository: CategoryRepository
) {

fun calculateAndSaveStats(memberTxs: Map<Long, List<Transaction>>, periodStart: LocalDate, periodEnd: LocalDate) {
memberTxs.mapValues { (memberId, txs) ->
val totalAmount = txs.sumOf { it.amount }
val highestAmount = txs.maxOf { it.amount }
val lowestAmount = txs.minOf { it.amount }
val mostFrequentCategory = txs
.groupBy { it.category.id }
.mapValues { entry ->
entry.value.size to entry.value.sumOf { it.amount }
}
.entries
.sortedWith(
compareByDescending<Map.Entry<Long?, Pair<Int, Int>>> { it.value.first }
.thenByDescending { it.value.second }
)
.firstOrNull()?.key ?: -1

val member = memberRepository.findById(memberId).orElseThrow { NotFoundException() }
val category = categoryRepository.findById(mostFrequentCategory).orElseThrow { NotFoundException() }

val daysBetween = ChronoUnit.DAYS.between(periodStart, periodEnd)
println("total : $totalAmount \n highest : $highestAmount \n lowest : $lowestAmount")
if (daysBetween <= 7) {
val weeklyStats = WeeklyStats(
member = member,
weeklyStart = periodStart,
weeklyEnd = periodEnd,
totalAmount = totalAmount,
highestAmount = highestAmount,
lowestAmount = lowestAmount,
category = category
)
weeklyStatsRepository.save(weeklyStats)
} else {
val monthlyStats = MonthlyStats(
member = member,
monthlyStart = periodStart,
monthlyEnd = periodEnd,
totalAmount = totalAmount,
highestAmount = highestAmount,
lowestAmount = lowestAmount,
category = category
)
monthlyStatsRepository.save(monthlyStats)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import com.side.tiggle.domain.transaction.model.Transaction
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository
import java.time.LocalDate

interface TransactionRepository: JpaRepository<Transaction, Long> {

fun findByMemberId(memberId: Long, pageable: Pageable): Page<Transaction>
}
fun findByDateBetweenOrderByDateAsc(startDate: LocalDate, endDate: LocalDate): List<Transaction>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.side.tiggle.global.config

import com.side.tiggle.domain.scheduler.service.JobService
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.SchedulingConfigurer
import org.springframework.scheduling.config.ScheduledTaskRegistrar

@Configuration
@EnableScheduling
class SchedulerConfig(
private val jobService: JobService,
@Value("\${week-schedule.cron}") private val weekCron: String,
@Value("\${week-schedule.use}") private val useWeekSchedule: Boolean,
@Value("\${monthly-schedule.cron}") private val monthlyCron: String,
@Value("\${monthly-schedule.use}") private val useMonthlySchedule: Boolean,
@Value("\${now-schedule.cron}") private val nowCron: String,
@Value("\${now-schedule.use}") private val useNowSchedule: Boolean
) : SchedulingConfigurer {
Comment thread
choesw marked this conversation as resolved.

override fun configureTasks(taskRegistrar: ScheduledTaskRegistrar) {
if (useWeekSchedule) {
taskRegistrar.addCronTask({ runJob("weekly") }, weekCron)
}
if (useMonthlySchedule) {
taskRegistrar.addCronTask({ runJob("monthly") }, monthlyCron)
}
if (useNowSchedule) {
taskRegistrar.addCronTask({ runJob("now") }, nowCron)
}
}

private fun runJob(jobType: String) {
try {
when (jobType) {
"weekly" -> jobService.generateWeeklySummary()
"monthly" -> jobService.generateMonthlySummary()
"now" -> jobService.runNowJob()
else -> println("Unknown job type: $jobType")
}
} catch (e: InterruptedException) {
println("* Thread가 강제 종료되었습니다. Message: ${e.message}")
} catch (e: Exception) {
println("* Batch 시스템이 예기치 않게 종료되었습니다. Message: ${e.message}")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.side.tiggle.global.scheduler

import com.side.tiggle.domain.transaction.model.Transaction
import com.side.tiggle.domain.transaction.repository.TransactionRepository
import org.springframework.stereotype.Service
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth

@Service
class JobService(
private val txRepository: TransactionRepository
) {

fun runNowJob() {
// 현재 작업 로직
val (weeklyStart, weeklyEnd) = getDate("weekly", LocalDate.now())
val weeklyMemberTxsMap = txRepository.findByDateBetweenOrderByDateAsc(weeklyStart, weeklyEnd).groupBy { it.member.id }

println(calculateMemberStats(weeklyMemberTxsMap))
}

fun generateWeeklySummary() {
val (weeklyStart, weeklyEnd) = getDate("weekly", LocalDate.now())
val weeklyMemberTxsMap = txRepository.findByDateBetweenOrderByDateAsc(weeklyStart, weeklyEnd).groupBy { it.member.id }
}

fun generateMonthlySummary() {

println("Montly summary is running...")

val (monthlyStart, monthlyEnd) = getDate("monthly", LocalDate.now())
val monthlyMemberTxsMap = txRepository.findByDateBetweenOrderByDateAsc(monthlyStart, monthlyEnd).groupBy { it.member.id }
}

fun getDate(type: String, today: LocalDate): Pair<LocalDate, LocalDate> {
return when (type) {
"weekly" -> {
val lastMonday = today.minusWeeks(1).with(DayOfWeek.MONDAY)
val lastSunday = today.minusDays(1).with(DayOfWeek.SUNDAY)
Pair(lastMonday, lastSunday)
}
"monthly" -> {
val firstDayOfLastMonth = today.minusMonths(1).withDayOfMonth(1)
val lastDayOfLastMonth = YearMonth.from(today.minusMonths(1)).atEndOfMonth()
Pair(firstDayOfLastMonth, lastDayOfLastMonth)
}
else -> throw IllegalStateException("Unknown type $type")
}
}

fun calculateMemberStats(memberTxs: Map<Long, List<Transaction>>) : Map<Long, MemberStats> {
return memberTxs.mapValues { entry ->
val txs = entry.value
val totalAmount = txs.sumOf { it.amount }
val highestAmount = txs.maxOf { it.amount }
val lowestAmount = txs.minOf { it.amount }
val mostFrequentCategory = txs
.groupingBy { it.category.id }
.eachCount()
.maxByOrNull { it.value }?.key ?: -1

MemberStats(
totalAmount = totalAmount,
highestAmount = highestAmount,
lowestAmount = lowestAmount,
mostFrequentCategory = mostFrequentCategory
)
}
}
}

// 총 금액
// 최고 금액
// 최저 금액
// 최다 카테고리 항목
// 이전 주(달)과 차액
data class MemberStats(
val totalAmount: Int,
val highestAmount: Int,
val lowestAmount: Int,
val mostFrequentCategory: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
week-schedule:
cron: "0 0 0 * * MON"
use: true

monthly-schedule:
cron: "0 0 0 1 * *"
use: true

now-schedule:
cron: "0 0/1 * * * ?" # 매분 동작
use: true
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ part:
path: upload/image/

app:
gateway-path: http://localhost:8081
client-redirect-uri: http://localhost:3000/login/success
client-redirect-uri: https://tiggle.duckdns.org/login/success

---
spring:
Expand Down

This file was deleted.

Loading