Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 포스트 통계 조회 기능 구현 #106

Merged
merged 11 commits into from
Nov 28, 2023
Merged
12 changes: 6 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ def excludeCoverage = [
'**/common/log/**',
'**/statistics/**/*Repository*',
'**/statistics/*History*',
'**/statistics/job/**/*Execution*',
'**/statistics/job/**/*JobHistoryRecorder*',
'**/statistics/job/**/*Scheduler*',
'**/statistics/batch/**/*Execution*',
'**/statistics/batch/**/*JobHistoryRecorder*',
'**/statistics/batch/**/*Scheduler*',
'**/statistics/statistic/collector/**/*PointCuts*',
] + Qdomains

Expand Down Expand Up @@ -159,9 +159,9 @@ jacocoTestCoverageVerification {
'*.common.log.*',
'*.statistics.*Repository',
'*.statistics.*History',
'*.statistics.job.*Execution*',
'*.statistics.job.*JobHistoryRecorder*',
'*.statistics.job.*Scheduler*',
'*.statistics.batch.*Execution*',
'*.statistics.batch.*JobHistoryRecorder*',
'*.statistics.batch.*Scheduler*',
'*.statistics.statistic.collector.*PointCuts*',
] + QdomainsInVerification

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/mallang/post/query/PostDataProtector.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public PostDetailResponse protectIfRequired(
return postDetailResponse;
}
return new PostDetailResponse(
postDetailResponse.id(),
postDetailResponse.postId(),
postDetailResponse.blogId(),
postDetailResponse.blogName(),
postDetailResponse.title(),
"보호되어 있는 글입니다. 내용을 보시려면 비밀번호를 입력하세요.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@

@Builder
public record PostDetailResponse(
Long id,
Long postId,
Long blogId,
String blogName,
String title,
String content,
Expand All @@ -33,7 +34,8 @@ public static PostDetailResponse from(Post post) {

public static PostDetailResponse withLiked(Post post, boolean isLiked) {
return PostDetailResponse.builder()
.id(post.getPostId().getId())
.postId(post.getPostId().getId())
.blogId(post.getPostId().getBlogId())
.blogName(post.getBlog().getName())
.title(post.getTitle())
.content(post.getContent())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.mallang.statistics.api.presentation;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RequestMapping("/statistics")
@RestController
public class StatisticsController {


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.mallang.statistics.api.presentation;

import com.mallang.auth.presentation.support.Auth;
import com.mallang.statistics.api.presentation.request.StatisticConditionRequest;
import com.mallang.statistics.api.query.StatisticQueryService;
import com.mallang.statistics.api.query.response.PostViewStatisticResponse;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RequestMapping("/manage/statistics")
@RestController
public class StatisticsManageController {

private final StatisticQueryService statisticQueryService;

@GetMapping("/posts/{blogName}/{id}")
public ResponseEntity<List<PostViewStatisticResponse>> getPostViewStatistics(
@Auth Long memberId,
@PathVariable("blogName") String blogName,
@PathVariable("id") Long id,
@ModelAttribute StatisticConditionRequest request
) {
return ResponseEntity.ok(
statisticQueryService.getPostStatistics(request.toDto(memberId, blogName, id))
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.mallang.statistics.api.presentation.request;

import com.mallang.statistics.api.query.dto.PostViewStatisticQueryDto;
import com.mallang.statistics.api.query.support.PeriodType;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;

public record StatisticConditionRequest(
@NotNull PeriodType periodType,
@NotNull LocalDate lastDay,
int count
) {

public PostViewStatisticQueryDto toDto(Long memberId,
String blogName,
Long postId) {
return PostViewStatisticQueryDto.builder()
.memberId(memberId)
.blogName(blogName)
.postId(postId)
.periodType(periodType)
.lastDay(lastDay)
.count(count)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.mallang.statistics.api.query;

import com.mallang.statistics.api.query.support.PeriodType;
import java.time.LocalDate;

public record StatisticCondition(
PeriodType periodType,
LocalDate startDayInclude,
LocalDate lastDayInclude
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.mallang.statistics.api.query;

import com.mallang.statistics.api.query.dao.PostViewStatisticDao;
import com.mallang.statistics.api.query.dto.PostViewStatisticQueryDto;
import com.mallang.statistics.api.query.response.PostViewStatisticResponse;
import com.mallang.statistics.api.query.support.StatisticConditionConverter;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Transactional(readOnly = true)
@Component
public class StatisticQueryService {

private final PostViewStatisticDao postViewStatisticDao;

public List<PostViewStatisticResponse> getPostStatistics(PostViewStatisticQueryDto dto) {
StatisticCondition cond = StatisticConditionConverter.convert(dto.periodType(), dto.lastDay(), dto.count());
return postViewStatisticDao.find(dto.memberId(), dto.blogName(), dto.postId(), cond);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.mallang.statistics.api.query.dao;

import static com.mallang.post.domain.QPost.post;
import static com.mallang.statistics.statistic.QPostViewStatistic.postViewStatistic;

import com.mallang.post.domain.Post;
import com.mallang.post.exception.NotFoundPostException;
import com.mallang.statistics.api.query.StatisticCondition;
import com.mallang.statistics.api.query.response.PostViewStatisticResponse;
import com.mallang.statistics.statistic.PostViewStatistic;
import com.mallang.statistics.statistic.utils.LocalDateUtils;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.time.LocalDate;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class PostViewStatisticDao {

private final JPAQueryFactory query;

public List<PostViewStatisticResponse> find(
Long memberId,
String blogName,
Long postId,
StatisticCondition condition
) {
Post findPost = Optional.ofNullable(query.selectFrom(post)
.where(
post.postId.id.eq(postId),
post.blog.name.value.eq(blogName),
post.writer.id.eq(memberId)
)
.fetchFirst())
.orElseThrow(NotFoundPostException::new);

List<PostViewStatistic> result = query.selectFrom(postViewStatistic)
.where(
postViewStatistic.postId.eq(findPost.getPostId()),
postViewStatistic.statisticDate.between(condition.startDayInclude(), condition.lastDayInclude())
)
.orderBy(postViewStatistic.statisticDate.asc())
.fetch();
List<PostViewStatisticResponse> response = getResponseTemplates(condition);
aggregate(new ArrayDeque<>(result), response);
return response;
}

private List<PostViewStatisticResponse> getResponseTemplates(StatisticCondition condition) {
List<PostViewStatisticResponse> postViewStatisticResponses = new ArrayList<>();
LocalDate current = condition.startDayInclude();
while (current.isBefore(condition.lastDayInclude()) || current.isEqual(condition.lastDayInclude())) {
LocalDate next = current.plus(1, condition.periodType().temporalUnit());
postViewStatisticResponses.add(new PostViewStatisticResponse(current, next.minusDays(1)));
current = next;
}
return postViewStatisticResponses;
}

private void aggregate(Deque<PostViewStatistic> statistics, List<PostViewStatisticResponse> response) {
for (PostViewStatisticResponse postViewStatisticResponse : response) {
LocalDate startDate = postViewStatisticResponse.getStartDateInclude();
LocalDate endDate = postViewStatisticResponse.getEndDateInclude();
while (!statistics.isEmpty()) {
PostViewStatistic postView = statistics.peekFirst();
if (!LocalDateUtils.isBetween(startDate, endDate, postView.getStatisticDate())) {
break;
}
postViewStatisticResponse.addViewCount(postView.getCount());
statistics.pollFirst();
}
if (statistics.isEmpty()) {
return;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.mallang.statistics.api.query.dto;

import com.mallang.statistics.api.query.support.PeriodType;
import java.time.LocalDate;
import lombok.Builder;

@Builder
public record PostViewStatisticQueryDto(
Long memberId,
String blogName,
Long postId,
PeriodType periodType,
LocalDate lastDay,
int count
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.mallang.statistics.api.query.response;

import java.time.LocalDate;
import lombok.Getter;

@Getter
public class PostViewStatisticResponse {

private LocalDate startDateInclude;
private LocalDate endDateInclude;
private int viewCount;

public PostViewStatisticResponse() {
this(null, null);
}

public PostViewStatisticResponse(LocalDate startDateInclude, LocalDate endDateInclude) {
this(startDateInclude, endDateInclude, 0);
}

public PostViewStatisticResponse(LocalDate startDateInclude, LocalDate endDateInclude, int viewCount) {
this.startDateInclude = startDateInclude;
this.endDateInclude = endDateInclude;
this.viewCount = viewCount;
}

public void addViewCount(int count) {
this.viewCount += count;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.mallang.statistics.api.query.support;

import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;

public enum PeriodType {

DAY(ChronoUnit.DAYS),
WEEK(ChronoUnit.WEEKS),
MONTH(ChronoUnit.MONTHS),
YEAR(ChronoUnit.YEARS);

private final TemporalUnit temporalUnit;

PeriodType(TemporalUnit temporalUnit) {
this.temporalUnit = temporalUnit;
}

public TemporalUnit temporalUnit() {
return temporalUnit;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.mallang.statistics.api.query.support;

import static java.time.DayOfWeek.MONDAY;
import static java.time.DayOfWeek.SUNDAY;
import static java.time.temporal.TemporalAdjusters.lastDayOfMonth;
import static java.time.temporal.TemporalAdjusters.lastDayOfYear;
import static java.time.temporal.TemporalAdjusters.previousOrSame;

import com.mallang.statistics.api.query.StatisticCondition;
import java.time.LocalDate;

public final class StatisticConditionConverter {

private StatisticConditionConverter() {
}

public static StatisticCondition convert(
PeriodType periodType,
LocalDate lastDay,
int count
) {
LocalDate lastDateOfPeriod = getLastDate(lastDay, periodType);
LocalDate startDataOfPeriod = getStartData(lastDateOfPeriod, periodType, count);
return new StatisticCondition(periodType, startDataOfPeriod, lastDateOfPeriod);
}

private static LocalDate getLastDate(LocalDate lastDay, PeriodType periodType) {
return switch (periodType) {
case DAY -> lastDay;
case WEEK -> lastDay.plusDays((long) SUNDAY.getValue() - lastDay.getDayOfWeek().getValue());
case MONTH -> lastDay.with(lastDayOfMonth());
case YEAR -> lastDay.with(lastDayOfYear());
};
}

private static LocalDate getStartData(LocalDate lastDateOfPeriod, PeriodType periodType, int count) {
LocalDate startDateOfLastDateOfPeriod = getStartDateOfLastPeriod(lastDateOfPeriod, periodType);
return startDateOfLastDateOfPeriod.minus((long) count - 1, periodType.temporalUnit());
}

private static LocalDate getStartDateOfLastPeriod(LocalDate lastDateOfPeriod, PeriodType periodType) {
return switch (periodType) {
case DAY -> lastDateOfPeriod;
case WEEK -> lastDateOfPeriod.with(previousOrSame(MONDAY));
case MONTH -> lastDateOfPeriod.withDayOfMonth(1);
case YEAR -> lastDateOfPeriod.withDayOfYear(1);
};
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.mallang.statistics.job;
package com.mallang.statistics.batch;

import static java.util.stream.Collectors.groupingBy;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.mallang.statistics.job;
package com.mallang.statistics.batch;

import com.mallang.statistics.job.exeution.JobExecution;
import com.mallang.statistics.job.exeution.JobHistoryRecorder;
import com.mallang.statistics.batch.exeution.JobExecution;
import com.mallang.statistics.batch.exeution.JobHistoryRecorder;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down
Loading