Skip to content
Merged
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
@@ -1,7 +1,10 @@
package com.devkor.ifive.nadab.domain.stats.application;

import com.devkor.ifive.nadab.domain.stats.application.helper.PeakStatsTracker;
import com.devkor.ifive.nadab.domain.stats.core.dto.daily.DailyStatsViewModel;
import com.devkor.ifive.nadab.domain.stats.core.dto.daily.DateCountDto;
import com.devkor.ifive.nadab.domain.stats.core.dto.peak.PeakMetric;
import com.devkor.ifive.nadab.domain.stats.core.dto.peak.PeakStatViewModel;
import com.devkor.ifive.nadab.domain.stats.core.repository.DailyStatsRepository;
import com.devkor.ifive.nadab.global.shared.util.TodayDateTimeProvider;
import lombok.RequiredArgsConstructor;
Expand All @@ -18,6 +21,7 @@
public class DailyStatsService {

private final DailyStatsRepository repo;
private final PeakStatsTracker peakStatsTracker;

private static final ZoneId SEOUL = ZoneId.of("Asia/Seoul");
private static final DateTimeFormatter FMT =
Expand Down Expand Up @@ -57,15 +61,28 @@ public DailyStatsViewModel getDailyStatsLast7Days() {
List<Long> assignedCounts = days.stream().map(d -> assignedMap.getOrDefault(d, 0L)).toList();
List<Long> completedCounts = days.stream().map(d -> completedMap.getOrDefault(d, 0L)).toList();

PeakStatViewModel signupPeak = peakStatsTracker.updateAndGet(
PeakMetric.DAILY_SIGNUP, days, signupCounts, today
);
PeakStatViewModel assignedQuestionPeak = peakStatsTracker.updateAndGet(
PeakMetric.DAILY_ASSIGNED_QUESTION, days, assignedCounts, today
);
PeakStatViewModel dauPeak = peakStatsTracker.updateAndGet(
PeakMetric.DAU, days, completedCounts, today
);

long sharedNow = repo.countSharedDailyReportsNow();

return new DailyStatsViewModel(
labels,
signupCounts,
assignedCounts,
completedCounts,
signupPeak,
assignedQuestionPeak,
dauPeak,
sharedNow,
OffsetDateTime.now(SEOUL).format(FMT)
);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.devkor.ifive.nadab.domain.stats.application;

import com.devkor.ifive.nadab.domain.stats.application.helper.PeakStatsTracker;
import com.devkor.ifive.nadab.domain.stats.core.dto.daily.DateCountDto;
import com.devkor.ifive.nadab.domain.stats.core.dto.monthly.MonthlyStatsViewModel;
import com.devkor.ifive.nadab.domain.stats.core.dto.peak.PeakMetric;
import com.devkor.ifive.nadab.domain.stats.core.dto.peak.PeakStatViewModel;
import com.devkor.ifive.nadab.domain.stats.core.repository.MonthlyStatsRepository;
import com.devkor.ifive.nadab.global.shared.util.TodayDateTimeProvider;
import lombok.RequiredArgsConstructor;
Expand All @@ -22,6 +25,7 @@
public class MonthlyStatsService {

private final MonthlyStatsRepository repo;
private final PeakStatsTracker peakStatsTracker;

private static final ZoneId SEOUL = ZoneId.of("Asia/Seoul");
private static final DateTimeFormatter FMT =
Expand Down Expand Up @@ -74,6 +78,24 @@ public MonthlyStatsViewModel getMonthlyStatsLast5Months() {
List<Long> completedTotalCounts = sumCounts(completedV1Counts, completedV2Counts);
List<Long> mauCounts = months.stream().map(m -> mauMap.getOrDefault(m, 0L)).toList();

List<LocalDate> monthStarts = months.stream().map(month -> month.atDay(1)).toList();
LocalDate currentMonthStart = currentMonth.atDay(1);
PeakStatViewModel signupPeak = peakStatsTracker.updateAndGet(
PeakMetric.MONTHLY_SIGNUP, monthStarts, signupCounts, currentMonthStart
);
PeakStatViewModel assignedQuestionPeak = peakStatsTracker.updateAndGet(
PeakMetric.MONTHLY_ASSIGNED_QUESTION, monthStarts, assignedCounts, currentMonthStart
);
PeakStatViewModel completedDailyReportPeak = peakStatsTracker.updateAndGet(
PeakMetric.MONTHLY_DAILY_REPORT, monthStarts, completedDailyCounts, currentMonthStart
);
PeakStatViewModel completedMonthlyReportPeak = peakStatsTracker.updateAndGet(
PeakMetric.MONTHLY_REPORT, monthStarts, completedTotalCounts, currentMonthStart
);
PeakStatViewModel mauPeak = peakStatsTracker.updateAndGet(
PeakMetric.MAU, monthStarts, mauCounts, currentMonthStart
);

long inProgressV1Now = repo.countInProgressMonthlyReportV1Now();
long inProgressV2Now = repo.countInProgressMonthlyReportV2Now();
long inProgressTotalNow = inProgressV1Now + inProgressV2Now;
Expand All @@ -87,6 +109,11 @@ public MonthlyStatsViewModel getMonthlyStatsLast5Months() {
completedV2Counts,
completedTotalCounts,
mauCounts,
signupPeak,
assignedQuestionPeak,
completedDailyReportPeak,
completedMonthlyReportPeak,
mauPeak,
inProgressV1Now,
inProgressV2Now,
inProgressTotalNow,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.devkor.ifive.nadab.domain.stats.application;

import com.devkor.ifive.nadab.domain.stats.core.repository.PeakStatsRepository;
import com.devkor.ifive.nadab.domain.stats.core.repository.PeakStatsStore;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class PeakStatsInitializer implements ApplicationRunner {

private final PeakStatsRepository peakStatsRepository;
private final PeakStatsStore peakStatsStore;

@Override
public void run(ApplicationArguments args) {
try {
var peaks = peakStatsRepository.findAllPeakStats();
peaks.forEach(peakStatsStore::updateIfGreater);
log.info("Peak statistics initialized: {} metrics", peaks.size());
} catch (RuntimeException e) {
log.error("Failed to initialize peak statistics", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.devkor.ifive.nadab.domain.stats.application;

import com.devkor.ifive.nadab.domain.stats.application.helper.PeakStatsTracker;
import com.devkor.ifive.nadab.domain.stats.core.dto.daily.DateCountDto;
import com.devkor.ifive.nadab.domain.stats.core.dto.peak.PeakMetric;
import com.devkor.ifive.nadab.domain.stats.core.dto.peak.PeakStatViewModel;
import com.devkor.ifive.nadab.domain.stats.core.dto.weekly.WeeklyStatsViewModel;
import com.devkor.ifive.nadab.domain.stats.core.repository.WeeklyStatsRepository;
import com.devkor.ifive.nadab.global.shared.util.TodayDateTimeProvider;
Expand All @@ -23,6 +26,7 @@
public class WeeklyStatsService {

private final WeeklyStatsRepository repo;
private final PeakStatsTracker peakStatsTracker;

private static final ZoneId SEOUL = ZoneId.of("Asia/Seoul");
private static final DateTimeFormatter FMT =
Expand Down Expand Up @@ -68,6 +72,22 @@ public WeeklyStatsViewModel getWeeklyStatsLast7Weeks() {
List<Long> completedCounts = weekStarts.stream().map(d -> completedMap.getOrDefault(d, 0L)).toList();
List<Long> wauCounts = weekStarts.stream().map(d -> wauMap.getOrDefault(d, 0L)).toList();

PeakStatViewModel signupPeak = peakStatsTracker.updateAndGet(
PeakMetric.WEEKLY_SIGNUP, weekStarts, signupCounts, currentWeekStart
);
PeakStatViewModel assignedQuestionPeak = peakStatsTracker.updateAndGet(
PeakMetric.WEEKLY_ASSIGNED_QUESTION, weekStarts, assignedCounts, currentWeekStart
);
PeakStatViewModel completedDailyReportPeak = peakStatsTracker.updateAndGet(
PeakMetric.WEEKLY_DAILY_REPORT, weekStarts, completedDailyCounts, currentWeekStart
);
PeakStatViewModel completedWeeklyReportPeak = peakStatsTracker.updateAndGet(
PeakMetric.WEEKLY_REPORT, weekStarts, completedCounts, currentWeekStart
);
PeakStatViewModel wauPeak = peakStatsTracker.updateAndGet(
PeakMetric.WAU, weekStarts, wauCounts, currentWeekStart
);

long inProgressNow = repo.countInProgressWeeklyReportsNow();

return new WeeklyStatsViewModel(
Expand All @@ -77,6 +97,11 @@ public WeeklyStatsViewModel getWeeklyStatsLast7Weeks() {
completedDailyCounts,
completedCounts,
wauCounts,
signupPeak,
assignedQuestionPeak,
completedDailyReportPeak,
completedWeeklyReportPeak,
wauPeak,
inProgressNow,
OffsetDateTime.now(SEOUL).format(FMT)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.devkor.ifive.nadab.domain.stats.application.helper;

import com.devkor.ifive.nadab.domain.stats.core.dto.peak.PeakMetric;
import com.devkor.ifive.nadab.domain.stats.core.dto.peak.PeakStat;
import com.devkor.ifive.nadab.domain.stats.core.dto.peak.PeakStatViewModel;
import com.devkor.ifive.nadab.domain.stats.core.repository.PeakStatsStore;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.util.List;

@Component
@RequiredArgsConstructor
public class PeakStatsTracker {

private final PeakStatsStore peakStatsStore;

public PeakStatViewModel updateAndGet(
PeakMetric metric,
List<LocalDate> periodStarts,
List<Long> values,
LocalDate currentPeriodStart
) {
if (periodStarts.size() != values.size()) {
throw new IllegalArgumentException("periodStarts and values must have the same size");
}

PeakStat candidate = findPeakCandidate(periodStarts, values);
if (candidate != null) {
peakStatsStore.updateIfGreater(metric, candidate);
}

return peakStatsStore.find(metric)
.map(peak -> PeakStatViewModel.from(peak, metric.periodUnit(), currentPeriodStart))
.orElseGet(PeakStatViewModel::empty);
}

private PeakStat findPeakCandidate(List<LocalDate> periodStarts, List<Long> values) {
PeakStat candidate = null;

for (int i = 0; i < periodStarts.size(); i++) {
long value = values.get(i);
LocalDate periodStart = periodStarts.get(i);

if (value <= 0) {
continue;
}
if (candidate == null
|| value > candidate.value()
|| value == candidate.value() && periodStart.isAfter(candidate.periodStart())) {
candidate = new PeakStat(value, periodStart);
}
}

return candidate;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package com.devkor.ifive.nadab.domain.stats.core.dto.daily;

import com.devkor.ifive.nadab.domain.stats.core.dto.peak.PeakStatViewModel;

import java.util.List;

public record DailyStatsViewModel(
List<String> labels, // yyyy-MM-dd
List<Long> signupCounts,
List<Long> assignedQuestionCounts,
List<Long> completedDailyReportCounts,
PeakStatViewModel signupPeak,
PeakStatViewModel assignedQuestionPeak,
PeakStatViewModel dauPeak,
long sharedDailyReportCount,
String refreshedAt // "2026-02-27 21:34:12"
) {}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.devkor.ifive.nadab.domain.stats.core.dto.monthly;

import com.devkor.ifive.nadab.domain.stats.core.dto.peak.PeakStatViewModel;

import java.util.List;

public record MonthlyStatsViewModel(
Expand All @@ -11,6 +13,11 @@ public record MonthlyStatsViewModel(
List<Long> completedMonthlyReportV2Counts,
List<Long> completedMonthlyReportTotalCounts,
List<Long> mauCounts,
PeakStatViewModel signupPeak,
PeakStatViewModel assignedQuestionPeak,
PeakStatViewModel completedDailyReportPeak,
PeakStatViewModel completedMonthlyReportPeak,
PeakStatViewModel mauPeak,
long inProgressMonthlyReportV1Count,
long inProgressMonthlyReportV2Count,
long inProgressMonthlyReportTotalCount,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.devkor.ifive.nadab.domain.stats.core.dto.peak;

public enum PeakMetric {
DAILY_SIGNUP(PeakPeriodUnit.DAY),
DAILY_ASSIGNED_QUESTION(PeakPeriodUnit.DAY),
DAU(PeakPeriodUnit.DAY),
WEEKLY_SIGNUP(PeakPeriodUnit.WEEK),
WEEKLY_ASSIGNED_QUESTION(PeakPeriodUnit.WEEK),
WEEKLY_DAILY_REPORT(PeakPeriodUnit.WEEK),
WEEKLY_REPORT(PeakPeriodUnit.WEEK),
WAU(PeakPeriodUnit.WEEK),
MONTHLY_SIGNUP(PeakPeriodUnit.MONTH),
MONTHLY_ASSIGNED_QUESTION(PeakPeriodUnit.MONTH),
MONTHLY_DAILY_REPORT(PeakPeriodUnit.MONTH),
MONTHLY_REPORT(PeakPeriodUnit.MONTH),
MAU(PeakPeriodUnit.MONTH);

private final PeakPeriodUnit periodUnit;

PeakMetric(PeakPeriodUnit periodUnit) {
this.periodUnit = periodUnit;
}

public PeakPeriodUnit periodUnit() {
return periodUnit;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.devkor.ifive.nadab.domain.stats.core.dto.peak;

public enum PeakPeriodUnit {
DAY,
WEEK,
MONTH
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.devkor.ifive.nadab.domain.stats.core.dto.peak;

import java.time.LocalDate;
import java.util.Objects;

public record PeakStat(
long value,
LocalDate periodStart
) {

public PeakStat {
if (value < 0) {
throw new IllegalArgumentException("Peak value must not be negative");
}
Objects.requireNonNull(periodStart, "periodStart must not be null");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.devkor.ifive.nadab.domain.stats.core.dto.peak;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public record PeakStatViewModel(
boolean available,
long value,
String periodStart,
String periodLabel,
boolean currentPeriod
) {

private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");

public static PeakStatViewModel from(
PeakStat peak,
PeakPeriodUnit periodUnit,
LocalDate currentPeriodStart
) {
return new PeakStatViewModel(
true,
peak.value(),
peak.periodStart().toString(),
formatPeriodLabel(peak.periodStart(), periodUnit),
peak.periodStart().equals(currentPeriodStart)
);
}

public static PeakStatViewModel empty() {
return new PeakStatViewModel(false, 0L, "", "", false);
}

private static String formatPeriodLabel(LocalDate periodStart, PeakPeriodUnit periodUnit) {
return switch (periodUnit) {
case DAY -> periodStart.toString();
case WEEK -> periodStart + " ~ " + periodStart.plusDays(6);
case MONTH -> MONTH_FORMATTER.format(periodStart);
};
}
}
Loading
Loading