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/#136 통계 관련 기능 구현 #144

Merged
merged 22 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8906151
refactor: News Category directory 변경 (service -> entity)
yeonjy Jun 4, 2024
3f879b6
Merge branch 'Dev-backend' into feat/#136-category-statistics
yeonjy Jun 17, 2024
8eba20b
feat: QueryDsl 관련 설정
yeonjy Jun 17, 2024
34db6e9
feat: ReadNews 날짜별 읽은 뉴스 개수 조회 구현
yeonjy Jun 17, 2024
e4635f5
Merge branch 'fix/#141-backend-kakao-login' into feat/#136-category-s…
yeonjy Jun 17, 2024
6dbdbf8
chore: Querydsl 관련 설정
yeonjy Jun 17, 2024
671c70b
feat: 날짜별 뉴스 조회수 조회 api 구현
yeonjy Jun 17, 2024
eac9ae8
feat: QuerydslConfig
yeonjy Jun 17, 2024
15fdc6d
feat: getViewOfDates 로직 구현
yeonjy Jun 17, 2024
104eb4e
feat: QuerydslConfig 컨텍스트 등록
yeonjy Jun 17, 2024
152ccd9
test: StatisticsController 추가
yeonjy Jun 17, 2024
4448411
rename: test package 명 controller -> api
yeonjy Jun 17, 2024
921bfb0
style: 사용하지 않는 import 제거
yeonjy Jun 17, 2024
c3427ec
test: StatisticsServiceTest 구현
yeonjy Jun 17, 2024
c32c4fe
test: ReadNewsRepositoryTest 구현
yeonjy Jun 17, 2024
dbdc4d9
feat: statistics.dto에 response 패키지 추가
yeonjy Jun 17, 2024
b199b0f
feat: getCategoryStatistics API 구현
yeonjy Jun 17, 2024
a53117c
feat: getCountOfReadNewsByCategory querydsl 로직 구현
yeonjy Jun 17, 2024
ca64137
feat: getCategoryStatistics service 로직 구현
yeonjy Jun 17, 2024
f8b8136
test: 카테고리별 조회한 뉴스 개수 관련 테스트 구현
yeonjy Jun 17, 2024
e3217d2
refactor: 날짜별 읽은 뉴스 조회수 반환 로직에 로그인 유저 파라미터 추가
yeonjy Jun 17, 2024
cfd4c6b
fix: NewsCategory directory 변경 적용
yeonjy Jun 17, 2024
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
10 changes: 10 additions & 0 deletions backend/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ repositories {
mavenCentral()
}

ext {
queryDslVersion = "5.0.0"
}

dependencies {
if (isAppleSilicon()) {
runtimeOnly("io.netty:netty-resolver-dns-native-macos:4.1.94.Final:osx-aarch_64")
Expand All @@ -41,6 +45,12 @@ dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.3.1.RELEASE'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'com.fasterxml.jackson.core:jackson-core:2.17.0'

implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta"
annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

annotationProcessor 'org.projectlombok:lombok'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.rollthedice.backend.domain.news.dto.NewsUrlDto;
import com.rollthedice.backend.domain.news.entity.News;
import com.rollthedice.backend.domain.news.service.NewsCategory;
import com.rollthedice.backend.domain.news.entity.NewsCategory;
import com.rollthedice.backend.domain.news.service.NewsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -11,7 +11,6 @@
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -32,7 +31,7 @@ public class NewsCrawlingService {
private final NewsService newsService;

@Transactional
// @Scheduled(cron = CRON, zone = ZONE)
// @Scheduled(cron = CRON, zone = ZONE)
public void scrap() throws IOException {
for (NewsCategory category : NewsCategory.values()) {
String categoryUrl = MAIN_URL + category.getNum();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.rollthedice.backend.domain.news.service;
package com.rollthedice.backend.domain.news.entity;

import lombok.Getter;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

import com.rollthedice.backend.domain.member.entity.Member;
import com.rollthedice.backend.domain.news.entity.ReadNews;
import com.rollthedice.backend.domain.statistics.repository.ReadNewsCustomRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ReadNewsRepository extends JpaRepository<ReadNews, Long> {
public interface ReadNewsRepository extends JpaRepository<ReadNews, Long>, ReadNewsCustomRepository {
List<ReadNews> getTop3ByMemberOrderByCreatedAtDesc(Member member);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.rollthedice.backend.domain.statistics.api;

import com.rollthedice.backend.domain.statistics.dto.response.CategoryStatisticsResponse;
import com.rollthedice.backend.domain.statistics.dto.response.DateViewStatisticsResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;

import java.util.List;

public interface StatisticsApi {
@Operation(
summary = "최근 일주일 날짜별 뉴스 조회수 조회",
description = "최근 일주일간 날짜별로 뉴스 조회수를 조회합니다.",
security = {@SecurityRequirement(name = "access_token")},
tags = {"Statistics"}
)
@ApiResponse(
responseCode = "200",
description = "요청에 성공하였습니다."
)
List<DateViewStatisticsResponse> getViewsOfDates();

@Operation(
summary = "카테고리별 조회수 조회",
description = "카테고리별 조회수를 조회합니다.",
security = {@SecurityRequirement(name = "access_token")},
tags = {"Statistics"}
)
@ApiResponse(
responseCode = "200",
description = "요청에 성공하였습니다."
)
List<CategoryStatisticsResponse> getCategoryStatistics();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.rollthedice.backend.domain.statistics.api;

import com.rollthedice.backend.domain.statistics.dto.response.CategoryStatisticsResponse;
import com.rollthedice.backend.domain.statistics.dto.response.DateViewStatisticsResponse;
import com.rollthedice.backend.domain.statistics.service.StatisticsService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("statistics")
public class StatisticsController implements StatisticsApi {
private final StatisticsService statisticsService;

@ResponseStatus(HttpStatus.OK)
@GetMapping("/per-dates")
@Override
public List<DateViewStatisticsResponse> getViewsOfDates() {
return statisticsService.getViewsOfDates();
}

@ResponseStatus(HttpStatus.OK)
@GetMapping("/categories")
@Override
public List<CategoryStatisticsResponse> getCategoryStatistics() {
return statisticsService.getCategoryStatistics();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.rollthedice.backend.domain.statistics.dto.response;

import lombok.*;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CategoryStatisticsResponse {
private Long views;
private String category;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.rollthedice.backend.domain.statistics.dto.response;

import lombok.*;

import java.time.LocalDate;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DateViewStatisticsResponse {
private Long views;
private LocalDate dateTime;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.rollthedice.backend.domain.statistics.repository;

import com.rollthedice.backend.domain.member.entity.Member;

import java.time.LocalDate;

public interface ReadNewsCustomRepository {
Long getCountOfReadNewsByDate(Member member, LocalDate date);

Long getCountOfReadNewsByCategory(Member member, String category);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.rollthedice.backend.domain.statistics.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import com.rollthedice.backend.domain.member.entity.Member;
import lombok.RequiredArgsConstructor;

import java.time.LocalDate;

import static com.rollthedice.backend.domain.news.entity.QNews.news;
import static com.rollthedice.backend.domain.news.entity.QReadNews.readNews;

@RequiredArgsConstructor
public class ReadNewsCustomRepositoryImpl implements ReadNewsCustomRepository {
private final JPAQueryFactory queryFactory;

@Override
public Long getCountOfReadNewsByDate(Member member, LocalDate date) {
return queryFactory
.select(readNews.count())
.from(readNews)
.where(readNews.member.eq(member)
.and(readNews.createdAt.between(
date.atStartOfDay(),
date.plusDays(1).atStartOfDay().minusNanos(1))))
.fetchOne();
}

@Override
public Long getCountOfReadNewsByCategory(Member member, String category) {
return queryFactory
.select(readNews.count())
.from(readNews)
.join(readNews.news, news)
.where(readNews.member.eq(member)
.and(news.category.eq(category))
).fetchOne();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.rollthedice.backend.domain.statistics.service;

import com.rollthedice.backend.domain.member.entity.Member;
import com.rollthedice.backend.domain.news.entity.NewsCategory;
import com.rollthedice.backend.domain.news.repository.ReadNewsRepository;
import com.rollthedice.backend.domain.statistics.dto.response.CategoryStatisticsResponse;
import com.rollthedice.backend.domain.statistics.dto.response.DateViewStatisticsResponse;
import com.rollthedice.backend.global.oauth2.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;

@Service
@RequiredArgsConstructor
public class StatisticsService {
private final ReadNewsRepository readNewsRepository;
private final AuthService authService;

public List<DateViewStatisticsResponse> getViewsOfDates() {
Member member = authService.getMember();
List<DateViewStatisticsResponse> responses = new ArrayList<>();
IntStream.range(0, 7)
.forEach(day -> responses.add(DateViewStatisticsResponse.builder()
.views(readNewsRepository.getCountOfReadNewsByDate(member, LocalDate.now().minusDays(day)))
.dateTime(LocalDate.now().minusDays(day)).build()));
return responses;
}

public List<CategoryStatisticsResponse> getCategoryStatistics() {
Member member = authService.getMember();
List<CategoryStatisticsResponse> responses = new ArrayList<>();
for (NewsCategory category : NewsCategory.values()) {
long views = readNewsRepository.getCountOfReadNewsByCategory(member, category.getName());
CategoryStatisticsResponse response = CategoryStatisticsResponse.builder()
.category(category.getName())
.views(views).build();
responses.add(response);
}
return responses;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.rollthedice.backend.global.config;


import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class QuerydslConfig {

private final EntityManager em;

@Autowired
public QuerydslConfig(EntityManager em) {
this.em = em;
}

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.rollthedice.backend.domain.news.controller;
package com.rollthedice.backend.domain.news.api;

import com.rollthedice.backend.domain.news.api.NewsController;
import com.rollthedice.backend.domain.news.exception.NewsNotFoundException;
import com.rollthedice.backend.domain.news.repository.NewsRepository;
import com.rollthedice.backend.domain.news.service.NewsService;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.rollthedice.backend.domain.news.repository;

import com.rollthedice.backend.domain.member.entity.Member;
import com.rollthedice.backend.domain.member.repository.MemberRepository;
import com.rollthedice.backend.domain.news.entity.News;
import com.rollthedice.backend.domain.news.entity.NewsCategory;
import com.rollthedice.backend.domain.news.entity.ReadNews;
import com.rollthedice.backend.support.RepositoryTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

import java.time.LocalDate;

import static com.rollthedice.backend.domain.member.MemberFixture.MEMBER;
import static org.assertj.core.api.Assertions.assertThat;

@DisplayName("ReadNewsRepository의 ")
@RepositoryTest
public class ReadNewsRepositoryTest {
@Autowired
private ReadNewsRepository readNewsRepository;
@Autowired
private NewsRepository newsRepository;
@Autowired
private MemberRepository memberRepository;

private Member member;

@BeforeEach
void setUp() {
member = memberRepository.save(MEMBER());
}

@Test
@DisplayName("하루동안 조회한 뉴스 개수를 반환할 수 있는가")
void getCountOfReadNewsByDate() {
//given
int expect = 5;
for (int i = 0; i < expect; i++) {
readNewsRepository.save(ReadNews.builder().member(member).news(newsRepository.save(News.builder().id((long) i).build())).build());
}

//when
Long result = readNewsRepository.getCountOfReadNewsByDate(member, LocalDate.now());

//then
assertThat(result).isEqualTo(expect);
}

@Test
@DisplayName("회원이 카테고리별로 조회한 뉴스 개수를 반환할 수 있는가")
void getCountOfReadNewsByCategory() {
//given
int expect = 5;
String category = NewsCategory.SCIENCE.getName();
for (int i = 0; i < expect; i++) {
readNewsRepository.save(ReadNews.builder().member(member)
.news(newsRepository.save(
News.builder().id((long) i).category(category).build())).build());
}

//when
Long result = readNewsRepository.getCountOfReadNewsByCategory(member, category);

//then
assertThat(result).isEqualTo(expect);
}
}
Loading
Loading