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
Expand Up @@ -77,6 +77,7 @@ public class DailyReportController {
description = """
- ErrorCode: DAILY_QUESTION_MISMATCH - 요청한 질문이 사용자에게 할당된 오늘의 질문과 일치하지 않음
- ErrorCode: IMAGE_INVALID_KEY - 유효하지 않은 이미지 키
- ErrorCode: IMAGE_WEBP_KEY_REQUIRED - objectKey는 존재하지만 webpKey가 누락됨
""",
content = @Content
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,12 @@ public CreateDailyReportResponse generateDailyReport(Long userId, DailyReportReq
DailyQuestion question = dailyQuestionRepository.findByIdWithInterest(request.questionId())
.orElseThrow(() -> new NotFoundException(ErrorCode.QUESTION_NOT_FOUND));

// webpKey 존재 시 검증
if(!isBlank(request.webpKey())) {
validateWebpKey(request.webpKey(), userId);
// objectKey, webpKey 존재 시 검증
if(!isBlank(request.objectKey())) {
if(isBlank(request.webpKey())) {
throw new BadRequestException(ErrorCode.IMAGE_WEBP_KEY_REQUIRED);
}
validateWebpKey(request.webpKey(), userId);
}

LocalDate today = TodayDateTimeProvider.getTodayDate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,15 @@ public MonthlyStatsViewModel getMonthlyStatsLast5Months() {
Map<YearMonth, Long> completedDailyMap = aggregateByMonth(
repo.findCompletedDailyReportCountsByDateBetween(startDate, endDateInclusive)
);
Map<YearMonth, Long> mauMap = aggregateByMonth(
repo.findMonthlyActiveUserCountsByDateBetween(startDate, endDateInclusive)
);

List<Long> signupCounts = months.stream().map(m -> signupMap.getOrDefault(m, 0L)).toList();
List<Long> assignedCounts = months.stream().map(m -> assignedMap.getOrDefault(m, 0L)).toList();
List<Long> completedDailyCounts = months.stream().map(m -> completedDailyMap.getOrDefault(m, 0L)).toList();
List<Long> completedCounts = months.stream().map(m -> completedMap.getOrDefault(m, 0L)).toList();
List<Long> mauCounts = months.stream().map(m -> mauMap.getOrDefault(m, 0L)).toList();

long inProgressNow = repo.countInProgressMonthlyReportsNow();

Expand All @@ -73,6 +77,7 @@ public MonthlyStatsViewModel getMonthlyStatsLast5Months() {
assignedCounts,
completedDailyCounts,
completedCounts,
mauCounts,
inProgressNow,
OffsetDateTime.now(SEOUL).format(FMT)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,15 @@ public WeeklyStatsViewModel getWeeklyStatsLast7Weeks() {
Map<LocalDate, Long> completedDailyMap = aggregateByWeekStart(
repo.findCompletedDailyReportCountsByDateBetween(startWeekStart, endDateInclusive)
);
Map<LocalDate, Long> wauMap = toMapByDate(
repo.findWeeklyActiveUserCountsByDateBetween(startWeekStart, endDateInclusive)
);

List<Long> signupCounts = weekStarts.stream().map(d -> signupMap.getOrDefault(d, 0L)).toList();
List<Long> assignedCounts = weekStarts.stream().map(d -> assignedMap.getOrDefault(d, 0L)).toList();
List<Long> completedDailyCounts = weekStarts.stream().map(d -> completedDailyMap.getOrDefault(d, 0L)).toList();
List<Long> completedCounts = weekStarts.stream().map(d -> completedMap.getOrDefault(d, 0L)).toList();
List<Long> wauCounts = weekStarts.stream().map(d -> wauMap.getOrDefault(d, 0L)).toList();

long inProgressNow = repo.countInProgressWeeklyReportsNow();

Expand All @@ -72,6 +76,7 @@ public WeeklyStatsViewModel getWeeklyStatsLast7Weeks() {
assignedCounts,
completedDailyCounts,
completedCounts,
wauCounts,
inProgressNow,
OffsetDateTime.now(SEOUL).format(FMT)
);
Expand All @@ -85,4 +90,12 @@ private Map<LocalDate, Long> aggregateByWeekStart(List<DateCountDto> dailyCounts
}
return map;
}

private Map<LocalDate, Long> toMapByDate(List<DateCountDto> counts) {
Map<LocalDate, Long> map = new HashMap<>();
for (DateCountDto dto : counts) {
map.put(dto.date(), dto.count());
}
return map;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public record MonthlyStatsViewModel(
List<Long> assignedQuestionCounts,
List<Long> completedDailyReportCounts,
List<Long> completedMonthlyReportCounts,
List<Long> mauCounts,
long inProgressMonthlyReportCount,
String refreshedAt
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public record WeeklyStatsViewModel(
List<Long> assignedQuestionCounts,
List<Long> completedDailyReportCounts,
List<Long> completedWeeklyReportCounts,
List<Long> wauCounts,
long inProgressWeeklyReportCount,
String refreshedAt
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ public List<Object[]> findSignupCountsLast7Days(LocalDate startDate, LocalDate e
return em.createQuery("""
select function('date', u.registeredAt), count(u.id)
from User u
where u.deletedAt is null
and u.signupStatus = com.devkor.ifive.nadab.domain.user.core.entity.SignupStatusType.COMPLETED
where u.signupStatus = com.devkor.ifive.nadab.domain.user.core.entity.SignupStatusType.COMPLETED
and u.registeredAt is not null
and u.registeredAt >= :start
and u.registeredAt < :endExclusive
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.stereotype.Repository;

import java.sql.Date;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneId;
Expand All @@ -25,8 +26,7 @@ public List<DateCountDto> findSignupCountsByDateBetween(LocalDate startDate, Loc
List<Object[]> rows = em.createQuery("""
select function('date', u.registeredAt), count(u.id)
from User u
where u.deletedAt is null
and u.signupStatus = com.devkor.ifive.nadab.domain.user.core.entity.SignupStatusType.COMPLETED
where u.signupStatus = com.devkor.ifive.nadab.domain.user.core.entity.SignupStatusType.COMPLETED
and u.registeredAt is not null
and u.registeredAt >= :start
and u.registeredAt < :endExclusive
Expand Down Expand Up @@ -92,9 +92,40 @@ select count(mr.id)
.getSingleResult();
}

public List<DateCountDto> findMonthlyActiveUserCountsByDateBetween(LocalDate startDate, LocalDate endDateInclusive) {
List<Object[]> rows = em.createQuery("""
select function('date_trunc', 'month', dr.date), count(distinct dr.answerEntry.user.id)
from DailyReport dr
where dr.date between :startDate and :endDate
and dr.status = com.devkor.ifive.nadab.domain.dailyreport.core.entity.DailyReportStatus.COMPLETED
group by function('date_trunc', 'month', dr.date)
order by function('date_trunc', 'month', dr.date)
""", Object[].class)
.setParameter("startDate", startDate)
.setParameter("endDate", endDateInclusive)
.getResultList();

return rows.stream()
.map(MonthlyStatsRepository::toDateCountDtoFromDateTrunc)
.toList();
}

private static DateCountDto toDateCountDto(Object[] row) {
LocalDate date = ((Date) row[0]).toLocalDate();
long count = (Long) row[1];
return new DateCountDto(date, count);
}

private static DateCountDto toDateCountDtoFromDateTrunc(Object[] row) {
LocalDate date;
if (row[0] instanceof Timestamp timestamp) {
date = timestamp.toLocalDateTime().toLocalDate();
} else if (row[0] instanceof Date sqlDate) {
date = sqlDate.toLocalDate();
} else {
date = LocalDate.parse(row[0].toString().substring(0, 10));
}
long count = (Long) row[1];
return new DateCountDto(date, count);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.stereotype.Repository;

import java.sql.Date;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneId;
Expand All @@ -25,8 +26,7 @@ public List<DateCountDto> findSignupCountsByDateBetween(LocalDate startDate, Loc
List<Object[]> rows = em.createQuery("""
select function('date', u.registeredAt), count(u.id)
from User u
where u.deletedAt is null
and u.signupStatus = com.devkor.ifive.nadab.domain.user.core.entity.SignupStatusType.COMPLETED
where u.signupStatus = com.devkor.ifive.nadab.domain.user.core.entity.SignupStatusType.COMPLETED
and u.registeredAt is not null
and u.registeredAt >= :start
and u.registeredAt < :endExclusive
Expand Down Expand Up @@ -98,9 +98,40 @@ select count(wr.id)
.getSingleResult();
}

public List<DateCountDto> findWeeklyActiveUserCountsByDateBetween(LocalDate startDate, LocalDate endDateInclusive) {
List<Object[]> rows = em.createQuery("""
select function('date_trunc', 'week', dr.date), count(distinct dr.answerEntry.user.id)
from DailyReport dr
where dr.date between :startDate and :endDate
and dr.status = com.devkor.ifive.nadab.domain.dailyreport.core.entity.DailyReportStatus.COMPLETED
group by function('date_trunc', 'week', dr.date)
order by function('date_trunc', 'week', dr.date)
""", Object[].class)
.setParameter("startDate", startDate)
.setParameter("endDate", endDateInclusive)
.getResultList();

return rows.stream()
.map(WeeklyStatsRepository::toDateCountDtoFromDateTrunc)
.toList();
}

private static DateCountDto toDateCountDto(Object[] row) {
LocalDate date = ((Date) row[0]).toLocalDate();
long count = (Long) row[1];
return new DateCountDto(date, count);
}

private static DateCountDto toDateCountDtoFromDateTrunc(Object[] row) {
LocalDate date;
if (row[0] instanceof Timestamp timestamp) {
date = timestamp.toLocalDateTime().toLocalDate();
} else if (row[0] instanceof Date sqlDate) {
date = sqlDate.toLocalDate();
} else {
date = LocalDate.parse(row[0].toString().substring(0, 10));
}
long count = (Long) row[1];
return new DateCountDto(date, count);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public enum ErrorCode {
IMAGE_SIZE_EXCEEDED(HttpStatus.BAD_REQUEST, "이미지 크기가 제한을 초과했습니다 (최대 5MB)"),
IMAGE_METADATA_INVALID(HttpStatus.BAD_REQUEST, "파일 메타데이터를 읽을 수 없습니다. 다시 시도해주세요."),
IMAGE_INVALID_KEY(HttpStatus.BAD_REQUEST, "잘못된 이미지 key입니다."),
IMAGE_WEBP_KEY_REQUIRED(HttpStatus.BAD_REQUEST, "objectKey가 존재할 때 webpKey도 함께 제공되어야 합니다"),

// ==================== QUESTION (질문) ====================
// 400 Bad Request
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/stats/daily.html
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@
<div class="chart-card chart-card-wide">
<div class="chart-card-header">
<div class="chart-card-title">
<span class="dot dot-teal"></span>생성된 일간 리포트 개수
<span class="dot dot-teal"></span>생성된 일간 리포트 개수 (DAU)
</div>
<div class="chart-card-subtitle">최근 7일</div>
</div>
Expand Down
11 changes: 11 additions & 0 deletions src/main/resources/templates/stats/monthly.html
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,15 @@
<canvas id="completedMonthlyChart" height="140"></canvas>
</div>
</div>
<div class="chart-card">
<div class="chart-card-header">
<div class="chart-card-title"><span class="dot dot-blue"></span>MAU</div>
<div class="chart-card-subtitle">활성 사용자 기준 : 일간 리포트를 작성한 고유 사용자 수</div>
</div>
<div class="chart-card-body">
<canvas id="mauChart" height="140"></canvas>
</div>
</div>
</div>

<script th:inline="javascript">
Expand All @@ -317,6 +326,7 @@
const assignedCounts = [[${vm.assignedQuestionCounts}]].map(v => parseInt(v, 10));
const completedDailyCounts = [[${vm.completedDailyReportCounts}]].map(v => parseInt(v, 10));
const completedMonthlyCounts = [[${vm.completedMonthlyReportCounts}]].map(v => parseInt(v, 10));
const mauCounts = [[${vm.mauCounts}]].map(v => parseInt(v, 10));

Chart.defaults.color = '#8b91a8';
Chart.defaults.font.family = "'DM Mono', monospace";
Expand All @@ -333,6 +343,7 @@
}

const configs = [
{ id: 'mauChart', label: 'MAU', data: mauCounts, color: 'rgba(108,143,255,1)' },
{ id: 'signupChart', label: '가입자 수', data: signupCounts, color: 'rgba(108,143,255,1)' },
{ id: 'assignedChart', label: '할당 질문 수', data: assignedCounts, color: 'rgba(255,126,179,1)' },
{ id: 'completedDailyChart', label: '일간 리포트 생성 수', data: completedDailyCounts, color: 'rgba(77,232,194,1)' },
Expand Down
12 changes: 12 additions & 0 deletions src/main/resources/templates/stats/weekly.html
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,15 @@
<canvas id="completedWeeklyChart" height="140"></canvas>
</div>
</div>
<div class="chart-card">
<div class="chart-card-header">
<div class="chart-card-title"><span class="dot dot-blue"></span>WAU</div>
<div class="chart-card-subtitle">활성 사용자 기준 : 일간 리포트를 작성한 고유 사용자 수</div>
</div>
<div class="chart-card-body">
<canvas id="wauChart" height="140"></canvas>
</div>
</div>
</div>

<script th:inline="javascript">
Expand All @@ -317,6 +326,7 @@
const rawAssignedCounts = [[${vm.assignedQuestionCounts}]].map(v => parseInt(v, 10));
const rawCompletedDailyCounts = [[${vm.completedDailyReportCounts}]].map(v => parseInt(v, 10));
const rawCompletedWeeklyCounts = [[${vm.completedWeeklyReportCounts}]].map(v => parseInt(v, 10));
const rawWauCounts = [[${vm.wauCounts}]].map(v => parseInt(v, 10));

const slicedRawLabels = rawLabels.slice(-5);
const labels = slicedRawLabels.map(label => {
Expand All @@ -327,6 +337,7 @@
const assignedCounts = rawAssignedCounts.slice(-5);
const completedDailyCounts = rawCompletedDailyCounts.slice(-5);
const completedWeeklyCounts = rawCompletedWeeklyCounts.slice(-5);
const wauCounts = rawWauCounts.slice(-5);

Chart.defaults.color = '#8b91a8';
Chart.defaults.font.family = "'DM Mono', monospace";
Expand All @@ -347,6 +358,7 @@
{ id: 'assignedChart', label: '할당 질문 수', data: assignedCounts, color: 'rgba(255,126,179,1)' },
{ id: 'completedDailyChart', label: '일간 리포트 생성 수', data: completedDailyCounts, color: 'rgba(77,232,194,1)' },
{ id: 'completedWeeklyChart', label: '주간 리포트 생성 수', data: completedWeeklyCounts, color: 'rgba(245,165,36,1)' },
{ id: 'wauChart', label: 'WAU', data: wauCounts, color: 'rgba(108,143,255,1)' },
];

configs.forEach(({ id, label, data, color }) => {
Expand Down
Loading