diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/application/AskChatStatsService.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/application/AskChatStatsService.java new file mode 100644 index 00000000..d77f22a4 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/application/AskChatStatsService.java @@ -0,0 +1,260 @@ +package com.devkor.ifive.nadab.domain.stats.application; + +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatDailyMessageStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatDailyRagDocumentStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatDailyRagReferenceStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatDailyRagStatsViewModel; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatDailySessionStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatDailyStatsViewModel; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatDailyWalletStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatDailyWalletStatsViewModel; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatErrorStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatMessageSummaryDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatRagErrorStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatRagSourceStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatRagStatsViewModel; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatRagSummaryDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatSessionSummaryDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatStatsViewModel; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatWalletStatsViewModel; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatWalletSummaryDto; +import com.devkor.ifive.nadab.domain.stats.core.repository.AskChatStatsRepository; +import com.devkor.ifive.nadab.global.shared.util.TodayDateTimeProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AskChatStatsService { + + public static final int DEFAULT_CHART_DAYS = 7; + + private static final ZoneId SEOUL = ZoneId.of("Asia/Seoul"); + private static final DateTimeFormatter REFRESHED_AT_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private final AskChatStatsRepository repository; + + public AskChatStatsViewModel getAskChatStats() { + LocalDate endDate = TodayDateTimeProvider.getTodayDate(); + return getAskChatStats(endDate.minusDays(DEFAULT_CHART_DAYS - 1L), endDate); + } + + public AskChatStatsViewModel getAskChatStats(LocalDate startDate, LocalDate endDate) { + Objects.requireNonNull(startDate, "startDate must not be null"); + Objects.requireNonNull(endDate, "endDate must not be null"); + if (startDate.isAfter(endDate)) { + throw new IllegalArgumentException("startDate must be on or before endDate"); + } + + OffsetDateTime startInclusive = startDate.atStartOfDay(SEOUL).toOffsetDateTime(); + OffsetDateTime endExclusive = endDate.plusDays(1).atStartOfDay(SEOUL).toOffsetDateTime(); + + List dailySessionStats = repository.findDailySessionStats( + startInclusive, + endExclusive + ); + List dailyMessageStats = repository.findDailyMessageStats( + startInclusive, + endExclusive + ); + AskChatSessionSummaryDto sessionSummary = repository.findSessionSummary( + startInclusive, + endExclusive + ); + AskChatMessageSummaryDto messageSummary = repository.findMessageSummary( + startInclusive, + endExclusive + ); + List errorStats = repository.findAssistantErrorStats( + startInclusive, + endExclusive + ); + List dailyWalletStats = repository.findDailyWalletStats( + startInclusive, + endExclusive + ); + AskChatWalletSummaryDto walletSummary = repository.findWalletSummary( + startInclusive, + endExclusive + ); + List dailyRagDocumentStats = repository.findDailyRagDocumentStats( + startInclusive, + endExclusive + ); + List dailyRagReferenceStats = repository.findDailyRagReferenceStats( + startInclusive, + endExclusive + ); + AskChatRagSummaryDto ragSummary = repository.findRagSummary(startInclusive, endExclusive); + List ragSourceStats = repository.findRagSourceStats( + startInclusive, + endExclusive + ); + List ragErrorStats = repository.findRagErrorStats( + startInclusive, + endExclusive + ); + + Map sessionStatsByDate = new HashMap<>(); + dailySessionStats.forEach(stats -> sessionStatsByDate.put(stats.date(), stats)); + Map messageStatsByDate = new HashMap<>(); + dailyMessageStats.forEach(stats -> messageStatsByDate.put(stats.date(), stats)); + Map walletStatsByDate = new HashMap<>(); + dailyWalletStats.forEach(stats -> walletStatsByDate.put(stats.date(), stats)); + Map ragDocumentStatsByDate = new HashMap<>(); + dailyRagDocumentStats.forEach(stats -> ragDocumentStatsByDate.put(stats.date(), stats)); + Map ragReferenceStatsByDate = new HashMap<>(); + dailyRagReferenceStats.forEach(stats -> ragReferenceStatsByDate.put(stats.date(), stats)); + + List dailyStats = startDate.datesUntil(endDate.plusDays(1)) + .map(date -> toDailyViewModel( + date, + sessionStatsByDate.get(date), + messageStatsByDate.get(date), + walletStatsByDate.get(date), + ragDocumentStatsByDate.get(date), + ragReferenceStatsByDate.get(date) + )) + .toList(); + + long assistantResponseCount = messageSummary.totalCompletedAssistantMessageCount() + + messageSummary.totalFailedAssistantMessageCount(); + double assistantSuccessRatePercent = assistantResponseCount == 0 + ? 0.0 + : messageSummary.totalCompletedAssistantMessageCount() * 100.0 / assistantResponseCount; + + return new AskChatStatsViewModel( + startDate, + endDate, + dailyStats, + sessionSummary.totalSessionCount(), + sessionSummary.totalUniqueUserCount(), + sessionSummary.totalActiveSessionCount(), + sessionSummary.totalEndedSessionCount(), + messageSummary.totalUserMessageCount(), + messageSummary.totalCompletedAssistantMessageCount(), + messageSummary.totalFailedAssistantMessageCount(), + assistantSuccessRatePercent, + messageSummary.averageGenerationDurationMs(), + messageSummary.p95GenerationDurationMs(), + errorStats, + toWalletStatsViewModel(walletSummary), + toRagStatsViewModel(ragSummary, ragSourceStats, ragErrorStats), + OffsetDateTime.now(SEOUL).format(REFRESHED_AT_FORMATTER) + ); + } + + private AskChatDailyStatsViewModel toDailyViewModel( + LocalDate date, + AskChatDailySessionStatsDto sessionStats, + AskChatDailyMessageStatsDto messageStats, + AskChatDailyWalletStatsDto walletStats, + AskChatDailyRagDocumentStatsDto ragDocumentStats, + AskChatDailyRagReferenceStatsDto ragReferenceStats + ) { + return new AskChatDailyStatsViewModel( + date, + sessionStats == null ? 0L : sessionStats.sessionCount(), + sessionStats == null ? 0L : sessionStats.uniqueUserCount(), + sessionStats == null ? 0L : sessionStats.activeSessionCount(), + sessionStats == null ? 0L : sessionStats.endedSessionCount(), + messageStats == null ? 0L : messageStats.userMessageCount(), + messageStats == null ? 0L : messageStats.completedAssistantMessageCount(), + messageStats == null ? 0L : messageStats.failedAssistantMessageCount(), + messageStats == null ? 0.0 : messageStats.averageGenerationDurationMs(), + messageStats == null ? 0L : messageStats.p95GenerationDurationMs(), + toDailyWalletStatsViewModel(walletStats), + toDailyRagStatsViewModel(ragDocumentStats, ragReferenceStats) + ); + } + + private AskChatDailyWalletStatsViewModel toDailyWalletStatsViewModel( + AskChatDailyWalletStatsDto stats + ) { + if (stats == null) { + return new AskChatDailyWalletStatsViewModel(0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L); + } + return new AskChatDailyWalletStatsViewModel( + stats.totalLogCount(), + stats.pendingLogCount(), + stats.confirmedLogCount(), + stats.refundedLogCount(), + stats.freeTurnsGranted(), + stats.freeTurnsConsumed(), + stats.paidTurnsConsumed(), + stats.freeTurnsRefunded(), + stats.paidTurnsRefunded(), + stats.paidTurnsCharged(), + stats.netFreeTurnDelta(), + stats.netPaidTurnDelta() + ); + } + + private AskChatDailyRagStatsViewModel toDailyRagStatsViewModel( + AskChatDailyRagDocumentStatsDto documentStats, + AskChatDailyRagReferenceStatsDto referenceStats + ) { + return new AskChatDailyRagStatsViewModel( + documentStats == null ? 0L : documentStats.totalDocumentCount(), + documentStats == null ? 0L : documentStats.pendingDocumentCount(), + documentStats == null ? 0L : documentStats.completedDocumentCount(), + documentStats == null ? 0L : documentStats.failedDocumentCount(), + documentStats == null ? 0L : documentStats.deadLetterDocumentCount(), + documentStats == null ? 0.0 : documentStats.averageFailedRetryCount(), + referenceStats == null ? 0L : referenceStats.referenceCount(), + referenceStats == null ? 0L : referenceStats.uniqueReferencedDocumentCount() + ); + } + + private AskChatWalletStatsViewModel toWalletStatsViewModel(AskChatWalletSummaryDto summary) { + return new AskChatWalletStatsViewModel( + summary.totalLogCount(), + summary.pendingLogCount(), + summary.confirmedLogCount(), + summary.refundedLogCount(), + summary.freeTurnsGranted(), + summary.freeTurnsConsumed(), + summary.paidTurnsConsumed(), + summary.freeTurnsRefunded(), + summary.paidTurnsRefunded(), + summary.paidTurnsCharged(), + summary.netFreeTurnDelta(), + summary.netPaidTurnDelta() + ); + } + + private AskChatRagStatsViewModel toRagStatsViewModel( + AskChatRagSummaryDto summary, + List sourceStats, + List errorStats + ) { + double embeddingCompletionRatePercent = summary.totalDocumentCount() == 0 + ? 0.0 + : summary.completedDocumentCount() * 100.0 / summary.totalDocumentCount(); + return new AskChatRagStatsViewModel( + summary.totalDocumentCount(), + summary.pendingDocumentCount(), + summary.completedDocumentCount(), + summary.failedDocumentCount(), + summary.deadLetterDocumentCount(), + embeddingCompletionRatePercent, + summary.averageFailedRetryCount(), + summary.totalReferenceCount(), + summary.uniqueReferencedDocumentCount(), + sourceStats, + errorStats + ); + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/controller/StatsController.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/controller/StatsController.java index e2cee631..50f4323d 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/stats/controller/StatsController.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/controller/StatsController.java @@ -1,5 +1,6 @@ package com.devkor.ifive.nadab.domain.stats.controller; +import com.devkor.ifive.nadab.domain.stats.application.AskChatStatsService; import com.devkor.ifive.nadab.domain.stats.application.DailyStatsService; import com.devkor.ifive.nadab.domain.stats.application.MonthlyStatsService; import com.devkor.ifive.nadab.domain.stats.application.QuestionStatsService; @@ -9,6 +10,7 @@ import com.devkor.ifive.nadab.domain.stats.application.WeeklyStatsService; import com.devkor.ifive.nadab.domain.stats.application.helper.DailyQuestionOverviewCsvExporter; import com.devkor.ifive.nadab.domain.stats.application.helper.StatsPeriodResolver; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatStatsViewModel; import com.devkor.ifive.nadab.domain.stats.core.dto.daily.DailyStatsViewModel; import com.devkor.ifive.nadab.domain.stats.core.dto.monthly.MonthlyStatsViewModel; import com.devkor.ifive.nadab.domain.stats.core.dto.question.DailyQuestionOverviewQuery; @@ -45,6 +47,7 @@ public class StatsController { private static final String QUESTION_OVERVIEW_CSV_FILENAME = "nadab_daily_question_stats.csv"; private final DailyStatsService dailyStatsService; + private final AskChatStatsService askChatStatsService; private final WeeklyStatsService weeklyStatsService; private final MonthlyStatsService monthlyStatsService; private final TotalStatsService totalStatsService; @@ -53,6 +56,13 @@ public class StatsController { private final WithdrawalStatsService withdrawalStatsService; private final DailyQuestionOverviewCsvExporter dailyQuestionOverviewCsvExporter; + @GetMapping("/stats/ask-chat") + public String askChatStats(Model model) { + AskChatStatsViewModel vm = askChatStatsService.getAskChatStats(); + model.addAttribute("vm", vm); + model.addAttribute("activeTab", "ask-chat"); + return "stats/ask-chat"; + } @GetMapping("/stats/daily") public String dailyStats( diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyMessageStatsDto.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyMessageStatsDto.java new file mode 100644 index 00000000..30966b3c --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyMessageStatsDto.java @@ -0,0 +1,13 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +import java.time.LocalDate; + +public record AskChatDailyMessageStatsDto( + LocalDate date, + long userMessageCount, + long completedAssistantMessageCount, + long failedAssistantMessageCount, + double averageGenerationDurationMs, + long p95GenerationDurationMs +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyRagDocumentStatsDto.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyRagDocumentStatsDto.java new file mode 100644 index 00000000..b1a84c1d --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyRagDocumentStatsDto.java @@ -0,0 +1,14 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +import java.time.LocalDate; + +public record AskChatDailyRagDocumentStatsDto( + LocalDate date, + long totalDocumentCount, + long pendingDocumentCount, + long completedDocumentCount, + long failedDocumentCount, + long deadLetterDocumentCount, + double averageFailedRetryCount +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyRagReferenceStatsDto.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyRagReferenceStatsDto.java new file mode 100644 index 00000000..30213e17 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyRagReferenceStatsDto.java @@ -0,0 +1,10 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +import java.time.LocalDate; + +public record AskChatDailyRagReferenceStatsDto( + LocalDate date, + long referenceCount, + long uniqueReferencedDocumentCount +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyRagStatsViewModel.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyRagStatsViewModel.java new file mode 100644 index 00000000..daa20679 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyRagStatsViewModel.java @@ -0,0 +1,13 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +public record AskChatDailyRagStatsViewModel( + long totalDocumentCount, + long pendingDocumentCount, + long completedDocumentCount, + long failedDocumentCount, + long deadLetterDocumentCount, + double averageFailedRetryCount, + long referenceCount, + long uniqueReferencedDocumentCount +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailySessionStatsDto.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailySessionStatsDto.java new file mode 100644 index 00000000..243c46de --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailySessionStatsDto.java @@ -0,0 +1,12 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +import java.time.LocalDate; + +public record AskChatDailySessionStatsDto( + LocalDate date, + long sessionCount, + long uniqueUserCount, + long activeSessionCount, + long endedSessionCount +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyStatsViewModel.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyStatsViewModel.java new file mode 100644 index 00000000..38ada65f --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyStatsViewModel.java @@ -0,0 +1,19 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +import java.time.LocalDate; + +public record AskChatDailyStatsViewModel( + LocalDate date, + long sessionCount, + long uniqueUserCount, + long activeSessionCount, + long endedSessionCount, + long userMessageCount, + long completedAssistantMessageCount, + long failedAssistantMessageCount, + double averageGenerationDurationMs, + long p95GenerationDurationMs, + AskChatDailyWalletStatsViewModel walletStats, + AskChatDailyRagStatsViewModel ragStats +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyWalletStatsDto.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyWalletStatsDto.java new file mode 100644 index 00000000..398c67fd --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyWalletStatsDto.java @@ -0,0 +1,20 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +import java.time.LocalDate; + +public record AskChatDailyWalletStatsDto( + LocalDate date, + long totalLogCount, + long pendingLogCount, + long confirmedLogCount, + long refundedLogCount, + long freeTurnsGranted, + long freeTurnsConsumed, + long paidTurnsConsumed, + long freeTurnsRefunded, + long paidTurnsRefunded, + long paidTurnsCharged, + long netFreeTurnDelta, + long netPaidTurnDelta +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyWalletStatsViewModel.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyWalletStatsViewModel.java new file mode 100644 index 00000000..e6ba9217 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatDailyWalletStatsViewModel.java @@ -0,0 +1,17 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +public record AskChatDailyWalletStatsViewModel( + long totalLogCount, + long pendingLogCount, + long confirmedLogCount, + long refundedLogCount, + long freeTurnsGranted, + long freeTurnsConsumed, + long paidTurnsConsumed, + long freeTurnsRefunded, + long paidTurnsRefunded, + long paidTurnsCharged, + long netFreeTurnDelta, + long netPaidTurnDelta +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatErrorStatsDto.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatErrorStatsDto.java new file mode 100644 index 00000000..b52e7cbb --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatErrorStatsDto.java @@ -0,0 +1,7 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +public record AskChatErrorStatsDto( + String errorCode, + long count +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatMessageSummaryDto.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatMessageSummaryDto.java new file mode 100644 index 00000000..4f4f6fe2 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatMessageSummaryDto.java @@ -0,0 +1,10 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +public record AskChatMessageSummaryDto( + long totalUserMessageCount, + long totalCompletedAssistantMessageCount, + long totalFailedAssistantMessageCount, + double averageGenerationDurationMs, + long p95GenerationDurationMs +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatRagErrorStatsDto.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatRagErrorStatsDto.java new file mode 100644 index 00000000..9a4eb797 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatRagErrorStatsDto.java @@ -0,0 +1,7 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +public record AskChatRagErrorStatsDto( + String errorCode, + long count +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatRagSourceStatsDto.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatRagSourceStatsDto.java new file mode 100644 index 00000000..b129f91b --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatRagSourceStatsDto.java @@ -0,0 +1,11 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +public record AskChatRagSourceStatsDto( + String sourceType, + long totalDocumentCount, + long pendingDocumentCount, + long completedDocumentCount, + long failedDocumentCount, + long deadLetterDocumentCount +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatRagStatsViewModel.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatRagStatsViewModel.java new file mode 100644 index 00000000..e944deb8 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatRagStatsViewModel.java @@ -0,0 +1,18 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +import java.util.List; + +public record AskChatRagStatsViewModel( + long totalDocumentCount, + long pendingDocumentCount, + long completedDocumentCount, + long failedDocumentCount, + long deadLetterDocumentCount, + double embeddingCompletionRatePercent, + double averageFailedRetryCount, + long totalReferenceCount, + long uniqueReferencedDocumentCount, + List sourceStats, + List errorStats +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatRagSummaryDto.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatRagSummaryDto.java new file mode 100644 index 00000000..d5c36a49 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatRagSummaryDto.java @@ -0,0 +1,13 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +public record AskChatRagSummaryDto( + long totalDocumentCount, + long pendingDocumentCount, + long completedDocumentCount, + long failedDocumentCount, + long deadLetterDocumentCount, + double averageFailedRetryCount, + long totalReferenceCount, + long uniqueReferencedDocumentCount +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatSessionSummaryDto.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatSessionSummaryDto.java new file mode 100644 index 00000000..578edd45 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatSessionSummaryDto.java @@ -0,0 +1,9 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +public record AskChatSessionSummaryDto( + long totalSessionCount, + long totalUniqueUserCount, + long totalActiveSessionCount, + long totalEndedSessionCount +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatStatsViewModel.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatStatsViewModel.java new file mode 100644 index 00000000..7da7af77 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatStatsViewModel.java @@ -0,0 +1,25 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +import java.time.LocalDate; +import java.util.List; + +public record AskChatStatsViewModel( + LocalDate startDate, + LocalDate endDate, + List dailyStats, + long totalSessionCount, + long totalUniqueUserCount, + long totalActiveSessionCount, + long totalEndedSessionCount, + long totalUserMessageCount, + long totalCompletedAssistantMessageCount, + long totalFailedAssistantMessageCount, + double assistantSuccessRatePercent, + double averageGenerationDurationMs, + long p95GenerationDurationMs, + List errorStats, + AskChatWalletStatsViewModel walletStats, + AskChatRagStatsViewModel ragStats, + String refreshedAt +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatWalletStatsViewModel.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatWalletStatsViewModel.java new file mode 100644 index 00000000..df0affc9 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatWalletStatsViewModel.java @@ -0,0 +1,17 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +public record AskChatWalletStatsViewModel( + long totalLogCount, + long pendingLogCount, + long confirmedLogCount, + long refundedLogCount, + long freeTurnsGranted, + long freeTurnsConsumed, + long paidTurnsConsumed, + long freeTurnsRefunded, + long paidTurnsRefunded, + long paidTurnsCharged, + long netFreeTurnDelta, + long netPaidTurnDelta +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatWalletSummaryDto.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatWalletSummaryDto.java new file mode 100644 index 00000000..f35f3308 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/dto/askchat/AskChatWalletSummaryDto.java @@ -0,0 +1,17 @@ +package com.devkor.ifive.nadab.domain.stats.core.dto.askchat; + +public record AskChatWalletSummaryDto( + long totalLogCount, + long pendingLogCount, + long confirmedLogCount, + long refundedLogCount, + long freeTurnsGranted, + long freeTurnsConsumed, + long paidTurnsConsumed, + long freeTurnsRefunded, + long paidTurnsRefunded, + long paidTurnsCharged, + long netFreeTurnDelta, + long netPaidTurnDelta +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/stats/core/repository/AskChatStatsRepository.java b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/repository/AskChatStatsRepository.java new file mode 100644 index 00000000..ab15ebc9 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/stats/core/repository/AskChatStatsRepository.java @@ -0,0 +1,537 @@ +package com.devkor.ifive.nadab.domain.stats.core.repository; + +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatDailyMessageStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatDailyRagDocumentStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatDailyRagReferenceStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatDailySessionStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatErrorStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatMessageSummaryDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatDailyWalletStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatRagErrorStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatRagSourceStatsDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatRagSummaryDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatSessionSummaryDto; +import com.devkor.ifive.nadab.domain.stats.core.dto.askchat.AskChatWalletSummaryDto; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.sql.Date; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class AskChatStatsRepository { + + private final EntityManager em; + + public List findDailySessionStats( + OffsetDateTime startInclusive, + OffsetDateTime endExclusive + ) { + List rows = em.createNativeQuery(""" + select + (s.created_at at time zone 'Asia/Seoul')::date as stats_date, + count(*) as session_count, + count(distinct s.user_id) as unique_user_count, + count(*) filter (where s.status = 'ACTIVE') as active_session_count, + count(*) filter (where s.status = 'ENDED') as ended_session_count + from ask_chat_sessions s + where s.created_at >= :startInclusive + and s.created_at < :endExclusive + group by 1 + order by 1 + """) + .setParameter("startInclusive", startInclusive) + .setParameter("endExclusive", endExclusive) + .getResultList(); + + return rows.stream() + .map(row -> new AskChatDailySessionStatsDto( + toLocalDate(row[0]), + toLong(row[1]), + toLong(row[2]), + toLong(row[3]), + toLong(row[4]) + )) + .toList(); + } + + public AskChatSessionSummaryDto findSessionSummary( + OffsetDateTime startInclusive, + OffsetDateTime endExclusive + ) { + Object[] row = (Object[]) em.createNativeQuery(""" + select + count(*) as session_count, + count(distinct s.user_id) as unique_user_count, + count(*) filter (where s.status = 'ACTIVE') as active_session_count, + count(*) filter (where s.status = 'ENDED') as ended_session_count + from ask_chat_sessions s + where s.created_at >= :startInclusive + and s.created_at < :endExclusive + """) + .setParameter("startInclusive", startInclusive) + .setParameter("endExclusive", endExclusive) + .getSingleResult(); + + return new AskChatSessionSummaryDto( + toLong(row[0]), + toLong(row[1]), + toLong(row[2]), + toLong(row[3]) + ); + } + + public List findDailyMessageStats( + OffsetDateTime startInclusive, + OffsetDateTime endExclusive + ) { + List rows = em.createNativeQuery(""" + select + (m.created_at at time zone 'Asia/Seoul')::date as stats_date, + count(*) filter (where m.role = 'USER') as user_message_count, + count(*) filter (where m.role = 'ASSISTANT' and m.status = 'COMPLETED') + as completed_assistant_message_count, + count(*) filter (where m.role = 'ASSISTANT' and m.status = 'FAILED') + as failed_assistant_message_count, + avg(m.generation_duration_ms) filter ( + where m.role = 'ASSISTANT' + and m.status = 'COMPLETED' + and m.generation_duration_ms is not null + ) as average_generation_duration_ms, + percentile_cont(0.95) within group (order by m.generation_duration_ms) filter ( + where m.role = 'ASSISTANT' + and m.status = 'COMPLETED' + and m.generation_duration_ms is not null + ) as p95_generation_duration_ms + from ask_chat_messages m + where m.created_at >= :startInclusive + and m.created_at < :endExclusive + group by 1 + order by 1 + """) + .setParameter("startInclusive", startInclusive) + .setParameter("endExclusive", endExclusive) + .getResultList(); + + return rows.stream() + .map(row -> new AskChatDailyMessageStatsDto( + toLocalDate(row[0]), + toLong(row[1]), + toLong(row[2]), + toLong(row[3]), + toDouble(row[4]), + Math.round(toDouble(row[5])) + )) + .toList(); + } + + public AskChatMessageSummaryDto findMessageSummary( + OffsetDateTime startInclusive, + OffsetDateTime endExclusive + ) { + Object[] row = (Object[]) em.createNativeQuery(""" + select + count(*) filter (where m.role = 'USER') as user_message_count, + count(*) filter (where m.role = 'ASSISTANT' and m.status = 'COMPLETED') + as completed_assistant_message_count, + count(*) filter (where m.role = 'ASSISTANT' and m.status = 'FAILED') + as failed_assistant_message_count, + avg(m.generation_duration_ms) filter ( + where m.role = 'ASSISTANT' + and m.status = 'COMPLETED' + and m.generation_duration_ms is not null + ) as average_generation_duration_ms, + percentile_cont(0.95) within group (order by m.generation_duration_ms) filter ( + where m.role = 'ASSISTANT' + and m.status = 'COMPLETED' + and m.generation_duration_ms is not null + ) as p95_generation_duration_ms + from ask_chat_messages m + where m.created_at >= :startInclusive + and m.created_at < :endExclusive + """) + .setParameter("startInclusive", startInclusive) + .setParameter("endExclusive", endExclusive) + .getSingleResult(); + + return new AskChatMessageSummaryDto( + toLong(row[0]), + toLong(row[1]), + toLong(row[2]), + toDouble(row[3]), + Math.round(toDouble(row[4])) + ); + } + + public List findAssistantErrorStats( + OffsetDateTime startInclusive, + OffsetDateTime endExclusive + ) { + List rows = em.createNativeQuery(""" + select + coalesce(nullif(trim(m.error_code), ''), 'UNKNOWN') as error_code, + count(*) as error_count + from ask_chat_messages m + where m.created_at >= :startInclusive + and m.created_at < :endExclusive + and m.role = 'ASSISTANT' + and m.status = 'FAILED' + group by 1 + order by error_count desc, error_code asc + """) + .setParameter("startInclusive", startInclusive) + .setParameter("endExclusive", endExclusive) + .getResultList(); + + return rows.stream() + .map(row -> new AskChatErrorStatsDto(String.valueOf(row[0]), toLong(row[1]))) + .toList(); + } + + public List findDailyWalletStats( + OffsetDateTime startInclusive, + OffsetDateTime endExclusive + ) { + List rows = em.createNativeQuery(""" + select + (l.created_at at time zone 'Asia/Seoul')::date as stats_date, + count(*) as total_log_count, + count(*) filter (where l.status = 'PENDING') as pending_log_count, + count(*) filter (where l.status = 'CONFIRMED') as confirmed_log_count, + count(*) filter (where l.status = 'REFUNDED') as refunded_log_count, + coalesce(sum(case + when l.status = 'CONFIRMED' + and l.reason = 'INITIAL_FREE_GRANT' + and l.free_turn_delta > 0 + then l.free_turn_delta else 0 end), 0) as free_turns_granted, + coalesce(sum(case + when l.status = 'CONFIRMED' + and l.reason = 'ANSWER_SUCCESS_CONSUME' + and l.free_turn_delta < 0 + then -l.free_turn_delta else 0 end), 0) as free_turns_consumed, + coalesce(sum(case + when l.status = 'CONFIRMED' + and l.reason = 'ANSWER_SUCCESS_CONSUME' + and l.paid_turn_delta < 0 + then -l.paid_turn_delta else 0 end), 0) as paid_turns_consumed, + coalesce(sum(case + when l.status = 'CONFIRMED' + and l.reason = 'ANSWER_FAILURE_REFUND' + and l.free_turn_delta > 0 + then l.free_turn_delta else 0 end), 0) as free_turns_refunded, + coalesce(sum(case + when l.status = 'CONFIRMED' + and l.reason = 'ANSWER_FAILURE_REFUND' + and l.paid_turn_delta > 0 + then l.paid_turn_delta else 0 end), 0) as paid_turns_refunded, + coalesce(sum(case + when l.status = 'CONFIRMED' + and l.reason = 'CRYSTAL_CHARGE' + and l.paid_turn_delta > 0 + then l.paid_turn_delta else 0 end), 0) as paid_turns_charged, + coalesce(sum(l.free_turn_delta), 0) as net_free_turn_delta, + coalesce(sum(l.paid_turn_delta), 0) as net_paid_turn_delta + from ask_chat_wallet_logs l + where l.created_at >= :startInclusive + and l.created_at < :endExclusive + group by 1 + order by 1 + """) + .setParameter("startInclusive", startInclusive) + .setParameter("endExclusive", endExclusive) + .getResultList(); + + return rows.stream() + .map(row -> new AskChatDailyWalletStatsDto( + toLocalDate(row[0]), + toLong(row[1]), + toLong(row[2]), + toLong(row[3]), + toLong(row[4]), + toLong(row[5]), + toLong(row[6]), + toLong(row[7]), + toLong(row[8]), + toLong(row[9]), + toLong(row[10]), + toLong(row[11]), + toLong(row[12]) + )) + .toList(); + } + + public AskChatWalletSummaryDto findWalletSummary( + OffsetDateTime startInclusive, + OffsetDateTime endExclusive + ) { + Object[] row = (Object[]) em.createNativeQuery(""" + select + count(*) as total_log_count, + count(*) filter (where l.status = 'PENDING') as pending_log_count, + count(*) filter (where l.status = 'CONFIRMED') as confirmed_log_count, + count(*) filter (where l.status = 'REFUNDED') as refunded_log_count, + coalesce(sum(case + when l.status = 'CONFIRMED' + and l.reason = 'INITIAL_FREE_GRANT' + and l.free_turn_delta > 0 + then l.free_turn_delta else 0 end), 0) as free_turns_granted, + coalesce(sum(case + when l.status = 'CONFIRMED' + and l.reason = 'ANSWER_SUCCESS_CONSUME' + and l.free_turn_delta < 0 + then -l.free_turn_delta else 0 end), 0) as free_turns_consumed, + coalesce(sum(case + when l.status = 'CONFIRMED' + and l.reason = 'ANSWER_SUCCESS_CONSUME' + and l.paid_turn_delta < 0 + then -l.paid_turn_delta else 0 end), 0) as paid_turns_consumed, + coalesce(sum(case + when l.status = 'CONFIRMED' + and l.reason = 'ANSWER_FAILURE_REFUND' + and l.free_turn_delta > 0 + then l.free_turn_delta else 0 end), 0) as free_turns_refunded, + coalesce(sum(case + when l.status = 'CONFIRMED' + and l.reason = 'ANSWER_FAILURE_REFUND' + and l.paid_turn_delta > 0 + then l.paid_turn_delta else 0 end), 0) as paid_turns_refunded, + coalesce(sum(case + when l.status = 'CONFIRMED' + and l.reason = 'CRYSTAL_CHARGE' + and l.paid_turn_delta > 0 + then l.paid_turn_delta else 0 end), 0) as paid_turns_charged, + coalesce(sum(l.free_turn_delta), 0) as net_free_turn_delta, + coalesce(sum(l.paid_turn_delta), 0) as net_paid_turn_delta + from ask_chat_wallet_logs l + where l.created_at >= :startInclusive + and l.created_at < :endExclusive + """) + .setParameter("startInclusive", startInclusive) + .setParameter("endExclusive", endExclusive) + .getSingleResult(); + + return new AskChatWalletSummaryDto( + toLong(row[0]), + toLong(row[1]), + toLong(row[2]), + toLong(row[3]), + toLong(row[4]), + toLong(row[5]), + toLong(row[6]), + toLong(row[7]), + toLong(row[8]), + toLong(row[9]), + toLong(row[10]), + toLong(row[11]) + ); + } + + public List findDailyRagDocumentStats( + OffsetDateTime startInclusive, + OffsetDateTime endExclusive + ) { + List rows = em.createNativeQuery(""" + select + (d.created_at at time zone 'Asia/Seoul')::date as stats_date, + count(*) as total_document_count, + count(*) filter (where d.embedding_status = 'PENDING') as pending_document_count, + count(*) filter (where d.embedding_status = 'COMPLETED') as completed_document_count, + count(*) filter (where d.embedding_status = 'FAILED') as failed_document_count, + count(*) filter (where d.embedding_status = 'DEAD_LETTER') as dead_letter_document_count, + avg(d.retry_count) filter ( + where d.embedding_status in ('FAILED', 'DEAD_LETTER') + ) as average_failed_retry_count + from ask_chat_rag_documents d + where d.created_at >= :startInclusive + and d.created_at < :endExclusive + group by 1 + order by 1 + """) + .setParameter("startInclusive", startInclusive) + .setParameter("endExclusive", endExclusive) + .getResultList(); + + return rows.stream() + .map(row -> new AskChatDailyRagDocumentStatsDto( + toLocalDate(row[0]), + toLong(row[1]), + toLong(row[2]), + toLong(row[3]), + toLong(row[4]), + toLong(row[5]), + toDouble(row[6]) + )) + .toList(); + } + + public List findDailyRagReferenceStats( + OffsetDateTime startInclusive, + OffsetDateTime endExclusive + ) { + List rows = em.createNativeQuery(""" + select + (r.created_at at time zone 'Asia/Seoul')::date as stats_date, + count(*) as reference_count, + count(distinct r.rag_document_id) as unique_referenced_document_count + from ask_chat_message_references r + where r.created_at >= :startInclusive + and r.created_at < :endExclusive + group by 1 + order by 1 + """) + .setParameter("startInclusive", startInclusive) + .setParameter("endExclusive", endExclusive) + .getResultList(); + + return rows.stream() + .map(row -> new AskChatDailyRagReferenceStatsDto( + toLocalDate(row[0]), + toLong(row[1]), + toLong(row[2]) + )) + .toList(); + } + + public AskChatRagSummaryDto findRagSummary( + OffsetDateTime startInclusive, + OffsetDateTime endExclusive + ) { + Object[] row = (Object[]) em.createNativeQuery(""" + with document_stats as ( + select + count(*) as total_document_count, + count(*) filter (where d.embedding_status = 'PENDING') as pending_document_count, + count(*) filter (where d.embedding_status = 'COMPLETED') as completed_document_count, + count(*) filter (where d.embedding_status = 'FAILED') as failed_document_count, + count(*) filter (where d.embedding_status = 'DEAD_LETTER') as dead_letter_document_count, + avg(d.retry_count) filter ( + where d.embedding_status in ('FAILED', 'DEAD_LETTER') + ) as average_failed_retry_count + from ask_chat_rag_documents d + where d.created_at >= :startInclusive + and d.created_at < :endExclusive + ), reference_stats as ( + select + count(*) as total_reference_count, + count(distinct r.rag_document_id) as unique_referenced_document_count + from ask_chat_message_references r + where r.created_at >= :startInclusive + and r.created_at < :endExclusive + ) + select + d.total_document_count, + d.pending_document_count, + d.completed_document_count, + d.failed_document_count, + d.dead_letter_document_count, + d.average_failed_retry_count, + r.total_reference_count, + r.unique_referenced_document_count + from document_stats d + cross join reference_stats r + """) + .setParameter("startInclusive", startInclusive) + .setParameter("endExclusive", endExclusive) + .getSingleResult(); + + return new AskChatRagSummaryDto( + toLong(row[0]), + toLong(row[1]), + toLong(row[2]), + toLong(row[3]), + toLong(row[4]), + toDouble(row[5]), + toLong(row[6]), + toLong(row[7]) + ); + } + + public List findRagSourceStats( + OffsetDateTime startInclusive, + OffsetDateTime endExclusive + ) { + List rows = em.createNativeQuery(""" + select + d.source_type, + count(*) as total_document_count, + count(*) filter (where d.embedding_status = 'PENDING') as pending_document_count, + count(*) filter (where d.embedding_status = 'COMPLETED') as completed_document_count, + count(*) filter (where d.embedding_status = 'FAILED') as failed_document_count, + count(*) filter (where d.embedding_status = 'DEAD_LETTER') as dead_letter_document_count + from ask_chat_rag_documents d + where d.created_at >= :startInclusive + and d.created_at < :endExclusive + group by d.source_type + order by total_document_count desc, d.source_type asc + """) + .setParameter("startInclusive", startInclusive) + .setParameter("endExclusive", endExclusive) + .getResultList(); + + return rows.stream() + .map(row -> new AskChatRagSourceStatsDto( + String.valueOf(row[0]), + toLong(row[1]), + toLong(row[2]), + toLong(row[3]), + toLong(row[4]), + toLong(row[5]) + )) + .toList(); + } + + public List findRagErrorStats( + OffsetDateTime startInclusive, + OffsetDateTime endExclusive + ) { + List rows = em.createNativeQuery(""" + select + coalesce(nullif(trim(d.error_code), ''), 'UNKNOWN') as error_code, + count(*) as error_count + from ask_chat_rag_documents d + where d.created_at >= :startInclusive + and d.created_at < :endExclusive + and d.embedding_status in ('FAILED', 'DEAD_LETTER') + group by 1 + order by error_count desc, error_code asc + """) + .setParameter("startInclusive", startInclusive) + .setParameter("endExclusive", endExclusive) + .getResultList(); + + return rows.stream() + .map(row -> new AskChatRagErrorStatsDto(String.valueOf(row[0]), toLong(row[1]))) + .toList(); + } + + private static LocalDate toLocalDate(Object value) { + if (value instanceof LocalDate localDate) { + return localDate; + } + if (value instanceof Date sqlDate) { + return sqlDate.toLocalDate(); + } + if (value instanceof Timestamp timestamp) { + return timestamp.toLocalDateTime().toLocalDate(); + } + if (value instanceof LocalDateTime localDateTime) { + return localDateTime.toLocalDate(); + } + String text = String.valueOf(value); + return LocalDate.parse(text.substring(0, 10)); + } + + private static long toLong(Object value) { + return value == null ? 0L : ((Number) value).longValue(); + } + + private static double toDouble(Object value) { + return value == null ? 0.0 : ((Number) value).doubleValue(); + } +} diff --git a/src/main/resources/db/migration/V20260831_1300__IS_add_ask_chat_stats_indexes.sql b/src/main/resources/db/migration/V20260831_1300__IS_add_ask_chat_stats_indexes.sql new file mode 100644 index 00000000..f0018e7a --- /dev/null +++ b/src/main/resources/db/migration/V20260831_1300__IS_add_ask_chat_stats_indexes.sql @@ -0,0 +1,13 @@ +-- Ask Chat 통계는 사용자·세션을 특정하지 않고 created_at 기간으로 집계한다. +-- 기존 사용자·세션 선두 인덱스와 별도로 전역 기간 조회 경로를 제공한다. +CREATE INDEX idx_ask_chat_sessions_stats_created_id + ON ask_chat_sessions (created_at DESC, id DESC); + +CREATE INDEX idx_ask_chat_messages_stats_created_id + ON ask_chat_messages (created_at DESC, id DESC); + +CREATE INDEX idx_ask_chat_message_references_stats_created_id + ON ask_chat_message_references (created_at DESC, id DESC); + +CREATE INDEX idx_ask_chat_rag_documents_stats_created_id + ON ask_chat_rag_documents (created_at DESC, id DESC); diff --git a/src/main/resources/templates/stats/ask-chat.html b/src/main/resources/templates/stats/ask-chat.html new file mode 100644 index 00000000..e80d7dae --- /dev/null +++ b/src/main/resources/templates/stats/ask-chat.html @@ -0,0 +1,570 @@ + + + + + + NADAB — Ask Chat Stats + + + + + + + + + +
+ +
+
Ask Chat 집계 기간
+
2026-08-25 ~ 2026-08-31
+
최근 7일 · Asia/Seoul 기준 · 세션, 메시지, 대화권, RAG read model을 한 화면에서 확인합니다.
+
+ +
+
+
총 세션
+
0
+
진행 중 0 · 종료 0
+
+
+
고유 사용자
+
0
+
기간 내 Ask Chat 세션을 만든 사용자
+
+
+
사용자 메시지
+
0
+
답변 생성 요청 수
+
+
+
답변 성공률
+
0.0%
+
완료 0 · 실패 0
+
+
+
완료 답변
+
0
+
정상적으로 저장된 assistant 메시지
+
+
+
실패 답변
+
0
+
오류 코드별 분포는 아래에서 확인
+
+
+
평균 생성 시간
+
0.0 ms
+
assistant 생성 완료 요청 기준
+
+
+
p95 생성 시간
+
0 ms
+
느린 요청의 상위 5% 기준
+
+
+ +
+
+
+
일별 세션 · 메시지
+
기간 내 세션 생성과 사용자 메시지 추이
+
+
+
+
+
+
일별 답변 결과
+
완료와 실패 assistant 메시지 비교
+
+
+
+
+
+
일별 생성 지연
+
평균과 p95 assistant 생성 시간 (ms)
+
+
+
+
+
+
일별 대화권 소비
+
무료·유료 대화권 소비량
+
+
+
+
+
+
일별 RAG 문서 처리
+
임베딩 상태별 문서 수
+
+
+
+
+ +
+
+
+
대화권 현황
+
wallet log의 상태·delta·reason 기반 기간 집계
+
+
+
+
전체 로그
0
+
확정 로그
0
+
대기 로그
0
+
환불 로그
0
+
무료 지급
0
+
무료 소비
0
+
유료 소비
0
+
유료 충전
0
+
유료 순증감
0
+
+
+
+ +
+
+
RAG 현황
+
문서 임베딩과 메시지 참조 read model 집계
+
+
+
+
전체 문서
0
+
임베딩 완료율
0.0%
+
대기 문서
0
+
실패 문서
0
+
Dead-letter
0
+
평균 실패 retry
0.0
+
전체 참조
0
+
참조 문서 수
0
+
+
+
+
+ +
+
+
일별 Ask Chat 상세
+
조회 기간에 데이터가 없는 날도 0으로 표시합니다.
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
기준일세션사용자사용자 메시지완료 답변실패 답변평균 msp95 ms유료 소비RAG 완료참조
2026-08-31000000.00000
+
+
표시할 일별 통계가 없습니다.
+
+
+ +
+
+
+
답변 오류 코드
+
assistant 생성 실패 원인별 건수
+
+
+
+ + + +
오류 코드건수
TIMEOUT0
+
+
답변 오류가 없습니다.
+
+
+ +
+
+
RAG source별 처리 현황
+
source type별 임베딩 상태 분포
+
+
+
+ + + + + + + + + + + + +
source전체대기완료실패dead-letter
ASK_CHAT_MESSAGE00000
+
+
RAG source 데이터가 없습니다.
+
+
+ +
+
+
RAG 오류 코드
+
임베딩 실패 원인별 건수
+
+
+
+ + + +
오류 코드건수
EMBEDDING_TIMEOUT0
+
+
RAG 오류가 없습니다.
+
+
+
+ + + + diff --git a/src/main/resources/templates/stats/daily.html b/src/main/resources/templates/stats/daily.html index 904f853e..b3a0b3a7 100644 --- a/src/main/resources/templates/stats/daily.html +++ b/src/main/resources/templates/stats/daily.html @@ -269,29 +269,7 @@ - +
diff --git a/src/main/resources/templates/stats/fragments/tabs.html b/src/main/resources/templates/stats/fragments/tabs.html new file mode 100644 index 00000000..31412b1e --- /dev/null +++ b/src/main/resources/templates/stats/fragments/tabs.html @@ -0,0 +1,31 @@ + + + + + + diff --git a/src/main/resources/templates/stats/monthly.html b/src/main/resources/templates/stats/monthly.html index f7734d98..56e46512 100644 --- a/src/main/resources/templates/stats/monthly.html +++ b/src/main/resources/templates/stats/monthly.html @@ -259,29 +259,7 @@
- +
diff --git a/src/main/resources/templates/stats/question-overview.html b/src/main/resources/templates/stats/question-overview.html index e7a93ce5..0fde0d12 100644 --- a/src/main/resources/templates/stats/question-overview.html +++ b/src/main/resources/templates/stats/question-overview.html @@ -328,15 +328,7 @@
- +