From 1f518294e3b0f657e2d50707b577843db1c9c450 Mon Sep 17 00:00:00 2001 From: uijin-j Date: Wed, 2 Oct 2024 20:20:31 +0900 Subject: [PATCH 01/19] =?UTF-8?q?chore:=20prod=20=EB=8C=80=EC=8B=A0=20main?= =?UTF-8?q?=20=EB=B8=8C=EB=9E=9C=EC=B9=98=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 99e2ed6e..1fedc293 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -3,7 +3,7 @@ name: SidePeek CD with Gradle on: push: branches: - - prod + - main - dev jobs: From fdf12b4a4fb2be1805e2f34a233336d01faa130c Mon Sep 17 00:00:00 2001 From: uijin-j Date: Sun, 6 Oct 2024 02:51:36 +0900 Subject: [PATCH 02/19] =?UTF-8?q?chore:=20=EC=84=A4=EC=A0=95=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=B3=80=EA=B2=BD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sidepeek_backend_secret | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sidepeek_backend_secret b/sidepeek_backend_secret index da3fcf2e..88e6ca6b 160000 --- a/sidepeek_backend_secret +++ b/sidepeek_backend_secret @@ -1 +1 @@ -Subproject commit da3fcf2e2301270cb7a080a3c0efea32bf1172bd +Subproject commit 88e6ca6b118ff4fd31370ba42964d712cb8b2bad From 1ce82db1d37a8a2b73a3f277bd33d0e0dc18bfa6 Mon Sep 17 00:00:00 2001 From: uijin-j Date: Sun, 6 Oct 2024 18:34:54 +0900 Subject: [PATCH 03/19] =?UTF-8?q?chore:=20=EB=A0=88=EB=94=94=EC=8A=A4=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=A0=95=EB=B3=B4=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sidepeek_backend_secret | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sidepeek_backend_secret b/sidepeek_backend_secret index 88e6ca6b..2dd95985 160000 --- a/sidepeek_backend_secret +++ b/sidepeek_backend_secret @@ -1 +1 @@ -Subproject commit 88e6ca6b118ff4fd31370ba42964d712cb8b2bad +Subproject commit 2dd95985332f89df19c641b6d51744879ac57ce1 From d79ef5ff23248883a522f17bb7649b911b1374f9 Mon Sep 17 00:00:00 2001 From: uijin-j Date: Sun, 6 Oct 2024 18:56:51 +0900 Subject: [PATCH 04/19] =?UTF-8?q?chore:=20=EB=A0=88=EB=94=94=EC=8A=A4=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=A0=95=EB=B3=B4=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sidepeek_backend_secret | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sidepeek_backend_secret b/sidepeek_backend_secret index 2dd95985..e80c9962 160000 --- a/sidepeek_backend_secret +++ b/sidepeek_backend_secret @@ -1 +1 @@ -Subproject commit 2dd95985332f89df19c641b6d51744879ac57ce1 +Subproject commit e80c99625e890bc6c132fcb299cc3a697bb88eda From d2426c86fb25b78d06aea32de5b952b3e1d2e46b Mon Sep 17 00:00:00 2001 From: uijin-j Date: Tue, 8 Oct 2024 15:48:34 +0900 Subject: [PATCH 05/19] =?UTF-8?q?rename:=20getAllPopularThisWeek=20?= =?UTF-8?q?=E2=86=92=20getWeeklyPopular=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getAllPopularThisWeek() 라는 이름이 주간(지난 주) 인기 프로젝트의 의미와 다르기 때문에 포괄적인 이름으로 변경 --- .../sidepeek/common/doc/ProjectControllerDoc.java | 2 +- .../sidepeek/projects/controller/ProjectController.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/sixgaezzang/sidepeek/common/doc/ProjectControllerDoc.java b/src/main/java/sixgaezzang/sidepeek/common/doc/ProjectControllerDoc.java index ece1fd10..290730c3 100644 --- a/src/main/java/sixgaezzang/sidepeek/common/doc/ProjectControllerDoc.java +++ b/src/main/java/sixgaezzang/sidepeek/common/doc/ProjectControllerDoc.java @@ -79,7 +79,7 @@ ResponseEntity> getByCondition( @ApiResponse(responseCode = "200", description = OK_DESCRIPTION, useReturnTypeSchema = true) }) - ResponseEntity> getAllPopularThisWeek(); + ResponseEntity> getWeeklyPopular(); @Operation(summary = "프로젝트 수정", description = "작성자와 등록된 프로젝트 회원 멤버만 수정 가능, 로그인 필수") @ApiResponses({ diff --git a/src/main/java/sixgaezzang/sidepeek/projects/controller/ProjectController.java b/src/main/java/sixgaezzang/sidepeek/projects/controller/ProjectController.java index 4cbb8b42..42315309 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/controller/ProjectController.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/controller/ProjectController.java @@ -72,9 +72,12 @@ public ResponseEntity> getByCondit return ResponseEntity.ok().body(responses); } + /** + * 지난 주 인기 프로젝트 조회 API + */ @Override @GetMapping("/weekly") - public ResponseEntity> getAllPopularThisWeek() { + public ResponseEntity> getWeeklyPopular() { List responses = projectService.findAllPopularLastWeek(); return ResponseEntity.ok(responses); From d0284fcf097e7d6a2e7000693e6a39c98beec2c8 Mon Sep 17 00:00:00 2001 From: uijin-j Date: Tue, 8 Oct 2024 16:56:33 +0900 Subject: [PATCH 06/19] =?UTF-8?q?refactor:=20=EC=B1=85=EC=9E=84=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=B4=20=EC=9D=B8?= =?UTF-8?q?=EA=B8=B0(=ED=86=B5=EA=B3=84)=20=EA=B4=80=EB=A0=A8=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EB=94=B0=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?(PopularProjectRepository)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/PopularProjectRepository.java | 11 ++++ .../project/ProjectRepositoryCustom.java | 5 -- .../project/ProjectRepositoryCustomImpl.java | 24 -------- .../QuerydslPopularProjectRepository.java | 55 +++++++++++++++++++ .../projects/service/ProjectService.java | 4 +- 5 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 src/main/java/sixgaezzang/sidepeek/projects/repository/project/PopularProjectRepository.java create mode 100644 src/main/java/sixgaezzang/sidepeek/projects/repository/project/QuerydslPopularProjectRepository.java diff --git a/src/main/java/sixgaezzang/sidepeek/projects/repository/project/PopularProjectRepository.java b/src/main/java/sixgaezzang/sidepeek/projects/repository/project/PopularProjectRepository.java new file mode 100644 index 00000000..b2eb8182 --- /dev/null +++ b/src/main/java/sixgaezzang/sidepeek/projects/repository/project/PopularProjectRepository.java @@ -0,0 +1,11 @@ +package sixgaezzang.sidepeek.projects.repository.project; + +import java.time.LocalDate; +import java.util.List; +import sixgaezzang.sidepeek.projects.dto.response.ProjectBannerResponse; + +public interface PopularProjectRepository { + + List findRankBetweenPeriod(LocalDate startDate, LocalDate endDate, + int count); +} diff --git a/src/main/java/sixgaezzang/sidepeek/projects/repository/project/ProjectRepositoryCustom.java b/src/main/java/sixgaezzang/sidepeek/projects/repository/project/ProjectRepositoryCustom.java index 1a331864..cb9ac8cd 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/repository/project/ProjectRepositoryCustom.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/repository/project/ProjectRepositoryCustom.java @@ -1,12 +1,10 @@ package sixgaezzang.sidepeek.projects.repository.project; -import java.time.LocalDate; import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import sixgaezzang.sidepeek.projects.dto.request.FindProjectRequest; import sixgaezzang.sidepeek.projects.dto.response.CursorPaginationResponse; -import sixgaezzang.sidepeek.projects.dto.response.ProjectBannerResponse; import sixgaezzang.sidepeek.projects.dto.response.ProjectListResponse; import sixgaezzang.sidepeek.users.domain.User; @@ -16,9 +14,6 @@ CursorPaginationResponse findByCondition( List likedProjectIds, FindProjectRequest request); - List findAllPopularOfPeriod(LocalDate startDate, LocalDate endDate, - int count); - Page findAllByUserJoined(List likedProjectIds, User user, Pageable pageable); diff --git a/src/main/java/sixgaezzang/sidepeek/projects/repository/project/ProjectRepositoryCustomImpl.java b/src/main/java/sixgaezzang/sidepeek/projects/repository/project/ProjectRepositoryCustomImpl.java index a1591071..adf66fcc 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/repository/project/ProjectRepositoryCustomImpl.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/repository/project/ProjectRepositoryCustomImpl.java @@ -8,13 +8,11 @@ import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.dsl.BooleanExpression; -import com.querydsl.core.types.dsl.DateTemplate; import com.querydsl.core.types.dsl.EntityPathBase; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; -import java.time.LocalDate; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -27,7 +25,6 @@ import sixgaezzang.sidepeek.projects.dto.request.FindProjectRequest; import sixgaezzang.sidepeek.projects.dto.request.SortType; import sixgaezzang.sidepeek.projects.dto.response.CursorPaginationResponse; -import sixgaezzang.sidepeek.projects.dto.response.ProjectBannerResponse; import sixgaezzang.sidepeek.projects.dto.response.ProjectListResponse; import sixgaezzang.sidepeek.users.domain.User; @@ -175,27 +172,6 @@ private List toProjectListResponseList(List likedProj .toList(); } - @Override - public List findAllPopularOfPeriod(LocalDate startDate, - LocalDate endDate, int count) { - DateTemplate createdAt = Expressions.dateTemplate( - LocalDate.class, "DATE_FORMAT({0}, {1})", like.createdAt, "%Y-%m-%d"); - - List projects = queryFactory - .select(project) - .from(like) - .join(like.project, project) - .where(createdAt.between(startDate, endDate)) - .groupBy(project) - .orderBy(like.count().desc()) - .limit(count) - .fetch(); - - return projects.stream() - .map(ProjectBannerResponse::from) - .toList(); - } - private BooleanExpression getCursorCondition(SortType sort, Long lastProjectId, Long lastOrderCount) { if (lastProjectId == null && lastOrderCount == null) { // 첫 번째 페이지 diff --git a/src/main/java/sixgaezzang/sidepeek/projects/repository/project/QuerydslPopularProjectRepository.java b/src/main/java/sixgaezzang/sidepeek/projects/repository/project/QuerydslPopularProjectRepository.java new file mode 100644 index 00000000..7401342b --- /dev/null +++ b/src/main/java/sixgaezzang/sidepeek/projects/repository/project/QuerydslPopularProjectRepository.java @@ -0,0 +1,55 @@ +package sixgaezzang.sidepeek.projects.repository.project; + +import static sixgaezzang.sidepeek.like.domain.QLike.like; +import static sixgaezzang.sidepeek.projects.domain.QProject.project; + +import com.querydsl.core.types.dsl.DateTemplate; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import java.time.LocalDate; +import java.util.List; +import org.springframework.stereotype.Repository; +import sixgaezzang.sidepeek.projects.domain.Project; +import sixgaezzang.sidepeek.projects.dto.response.ProjectBannerResponse; + +@Repository +public class QuerydslPopularProjectRepository implements PopularProjectRepository { + + private final JPAQueryFactory queryFactory; + + public QuerydslPopularProjectRepository(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + /** + * 특정 기간 동안 인기 프로젝트를 조회합니다. + * - 기준 + * 1. 해당 기간 동안 받은 좋아요 수 + * 2. 전체 좋아요 수 + * 3. 최신 순 + */ + @Override + public List findRankBetweenPeriod( + LocalDate startDate, + LocalDate endDate, + int count + ) { + DateTemplate createdAt = Expressions.dateTemplate( + LocalDate.class, "DATE_FORMAT({0}, {1})", like.createdAt, "%Y-%m-%d"); + + List projects = queryFactory + .select(project) + .from(like) + .join(like.project, project) + .where(createdAt.between(startDate, endDate)) + .groupBy(project) + .orderBy(like.count().desc()) + .limit(count) + .fetch(); + + return projects.stream() + .map(ProjectBannerResponse::from) + .toList(); + } +} diff --git a/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java b/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java index 4fd43f33..0546efbc 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java @@ -40,6 +40,7 @@ import sixgaezzang.sidepeek.projects.dto.response.ProjectListResponse; import sixgaezzang.sidepeek.projects.dto.response.ProjectResponse; import sixgaezzang.sidepeek.projects.dto.response.ProjectSkillSummary; +import sixgaezzang.sidepeek.projects.repository.project.PopularProjectRepository; import sixgaezzang.sidepeek.projects.repository.project.ProjectRepository; import sixgaezzang.sidepeek.users.domain.User; import sixgaezzang.sidepeek.users.service.UserService; @@ -119,7 +120,8 @@ public List findAllPopularLastWeek() { LocalDate startDate = getStartDayOfLastWeek(today); LocalDate endDate = getEndDayOfLastWeek(today); - return projectRepository.findAllPopularOfPeriod(startDate, endDate, BANNER_PROJECT_COUNT); + return popularProjectRepository.findRankBetweenPeriod(startDate, endDate, + BANNER_PROJECT_COUNT); } public Page findByUser(Long userId, Long loginId, From 6492f797f2fd61c9c67df109d7b97c4644410125 Mon Sep 17 00:00:00 2001 From: uijin-j Date: Tue, 8 Oct 2024 16:57:33 +0900 Subject: [PATCH 07/19] =?UTF-8?q?docs:=20UI=EC=97=90=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=A0=81=EC=9D=B8=20=EC=84=A4=EB=AA=85=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?('=EB=B0=B0=EB=84=88=EC=9A=A9'=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=9D=91=EB=8B=B5=20=E2=86=92=20=EC=9D=B8=EA=B8=B0?= =?UTF-8?q?=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=9D=91=EB=8B=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sidepeek/projects/dto/response/ProjectBannerResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/sixgaezzang/sidepeek/projects/dto/response/ProjectBannerResponse.java b/src/main/java/sixgaezzang/sidepeek/projects/dto/response/ProjectBannerResponse.java index 7051908a..428fd21d 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/dto/response/ProjectBannerResponse.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/dto/response/ProjectBannerResponse.java @@ -4,7 +4,7 @@ import lombok.Builder; import sixgaezzang.sidepeek.projects.domain.Project; -@Schema(description = "배너용 프로젝트 응답") +@Schema(description = "인기 프로젝트 응답") @Builder public record ProjectBannerResponse( @Schema(description = "프로젝트 식별자", example = "1") From aee43cc38df664131ac2dd5cde94af2b8e9c86d6 Mon Sep 17 00:00:00 2001 From: uijin-j Date: Tue, 8 Oct 2024 16:58:12 +0900 Subject: [PATCH 08/19] =?UTF-8?q?refactor:=20public=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=EA=B0=80=20private=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=B3=B4=EB=8B=A4=20=EB=A8=BC=EC=A0=80=20=EB=82=98=EC=98=A4?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/ProjectRepositoryCustomImpl.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/java/sixgaezzang/sidepeek/projects/repository/project/ProjectRepositoryCustomImpl.java b/src/main/java/sixgaezzang/sidepeek/projects/repository/project/ProjectRepositoryCustomImpl.java index adf66fcc..8d3e0a3f 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/repository/project/ProjectRepositoryCustomImpl.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/repository/project/ProjectRepositoryCustomImpl.java @@ -113,6 +113,24 @@ public Page findAllByUserCommented(List likedProjectI likedProjectIds); } + public BooleanExpression getSkillCondition(List skillNames) { + if (Objects.isNull(skillNames) || skillNames.isEmpty()) { + return null; + } + + // 프로젝트 ID 서브쿼리를 생성하여 스킬을 모두 포함하는 프로젝트를 찾기 + JPAQuery projectHasSkillsSubQuery = queryFactory + .select(projectSkill.project.id) + .from(projectSkill) + .join(projectSkill.skill) + .where(projectSkill.skill.name.in(skillNames)) + .groupBy(projectSkill.project.id) + .having(projectSkill.project.id.count().eq(Expressions.constant(skillNames.size()))); + + // 프로젝트 ID 서브쿼리와 매칭되는 프로젝트를 찾는 조건을 반환합니다. + return project.id.in(projectHasSkillsSubQuery); + } + private Page findPageByCondition(EntityPathBase from, QProject join, BooleanExpression condition, Pageable pageable, List likedProjectIds) { @@ -202,24 +220,6 @@ private BooleanExpression getSearchCondition(String search) { .or(member.nickname.likeIgnoreCase(keyword)); } - public BooleanExpression getSkillCondition(List skillNames) { - if (Objects.isNull(skillNames) || skillNames.isEmpty()) { - return null; - } - - // 프로젝트 ID 서브쿼리를 생성하여 스킬을 모두 포함하는 프로젝트를 찾기 - JPAQuery projectHasSkillsSubQuery = queryFactory - .select(projectSkill.project.id) - .from(projectSkill) - .join(projectSkill.skill) - .where(projectSkill.skill.name.in(skillNames)) - .groupBy(projectSkill.project.id) - .having(projectSkill.project.id.count().eq(Expressions.constant(skillNames.size()))); - - // 프로젝트 ID 서브쿼리와 매칭되는 프로젝트를 찾는 조건을 반환합니다. - return project.id.in(projectHasSkillsSubQuery); - } - private OrderSpecifier getOrderSpecifier(SortType sort) { switch (sort) { case like: From c1d2b9db2f2f4242655323b80f1d91702e451a49 Mon Sep 17 00:00:00 2001 From: uijin-j Date: Tue, 8 Oct 2024 16:58:54 +0900 Subject: [PATCH 09/19] =?UTF-8?q?refactor:=20=EC=9C=A0=ED=8B=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=82=AC=EC=9A=A9=20=EC=8B=9C=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=EB=AA=85=EC=9D=84=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C=ED=95=B4=20=EA=B0=80=EB=8F=85=EC=84=B1=20=ED=96=A5?= =?UTF-8?q?=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sidepeek/projects/service/ProjectService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java b/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java index 0546efbc..9a2d28a7 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java @@ -5,8 +5,6 @@ import static sixgaezzang.sidepeek.projects.exception.message.ProjectErrorMessage.ONLY_OWNER_AND_FELLOW_MEMBER_CAN_UPDATE; import static sixgaezzang.sidepeek.projects.exception.message.ProjectErrorMessage.PROJECT_NOT_EXISTING; import static sixgaezzang.sidepeek.projects.exception.message.ProjectErrorMessage.USER_PROJECT_SEARCH_TYPE_IS_INVALID; -import static sixgaezzang.sidepeek.projects.util.DateUtils.getEndDayOfLastWeek; -import static sixgaezzang.sidepeek.projects.util.DateUtils.getStartDayOfLastWeek; import static sixgaezzang.sidepeek.projects.util.ProjectConstant.BANNER_PROJECT_COUNT; import static sixgaezzang.sidepeek.users.util.validation.UserValidator.validateLoginIdEqualsUserId; @@ -42,6 +40,7 @@ import sixgaezzang.sidepeek.projects.dto.response.ProjectSkillSummary; import sixgaezzang.sidepeek.projects.repository.project.PopularProjectRepository; import sixgaezzang.sidepeek.projects.repository.project.ProjectRepository; +import sixgaezzang.sidepeek.projects.util.DateUtils; import sixgaezzang.sidepeek.users.domain.User; import sixgaezzang.sidepeek.users.service.UserService; @@ -52,6 +51,7 @@ public class ProjectService { private final DateTimeProvider dateTimeProvider; private final ProjectRepository projectRepository; + private final PopularProjectRepository popularProjectRepository; private final UserService userService; private final ProjectSkillService projectSkillService; private final MemberService memberService; @@ -117,8 +117,8 @@ public ProjectResponse findById(String ip, Long loginId, Long projectId) { public List findAllPopularLastWeek() { LocalDate today = dateTimeProvider.getCurrentDate(); - LocalDate startDate = getStartDayOfLastWeek(today); - LocalDate endDate = getEndDayOfLastWeek(today); + LocalDate startDate = DateUtils.getStartDayOfLastWeek(today); + LocalDate endDate = DateUtils.getEndDayOfLastWeek(today); return popularProjectRepository.findRankBetweenPeriod(startDate, endDate, BANNER_PROJECT_COUNT); From e1319cb88eb1541d345a26f93c10ed5cf193789e Mon Sep 17 00:00:00 2001 From: uijin-j Date: Tue, 8 Oct 2024 16:59:29 +0900 Subject: [PATCH 10/19] =?UTF-8?q?test:=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=A2=8B=EC=95=84=EC=9A=94=EA=B0=80=20?= =?UTF-8?q?=EB=A7=8E=EC=9D=80=20=EC=88=9C=EC=84=9C=EB=8C=80=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=EA=B8=B0=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8?= =?UTF-8?q?=EA=B0=80=20=EC=A1=B0=ED=9A=8C=EB=90=98=EB=8A=94=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../projects/service/ProjectServiceTest.java | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/test/java/sixgaezzang/sidepeek/projects/service/ProjectServiceTest.java b/src/test/java/sixgaezzang/sidepeek/projects/service/ProjectServiceTest.java index 109b41d1..9a3473d2 100644 --- a/src/test/java/sixgaezzang/sidepeek/projects/service/ProjectServiceTest.java +++ b/src/test/java/sixgaezzang/sidepeek/projects/service/ProjectServiceTest.java @@ -41,6 +41,7 @@ import java.time.YearMonth; import java.time.temporal.TemporalAdjusters; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Optional; import net.datafaker.Faker; @@ -159,6 +160,7 @@ private Comment createAndSaveComment(User user, Project project, Comment comment private Like createAndSaveLike(Project project, User user) { Like newLike = createLike(user, project); + project.increaseLikeCount(); return likeRepository.save(newLike); } @@ -288,14 +290,16 @@ void setUp() { void 최대_5개로_지난_주_인기_프로젝트_조회를_성공한다() { // given int overBannerProjectCount = BANNER_PROJECT_COUNT * 2; - for (int i = 0; i < overBannerProjectCount; i++) { + for (int i = 0; i < overBannerProjectCount; + i++) { // 오늘 날짜로 프로젝트 생성 및 각 프로젝트 당 좋아요 1개 생성 Project project = createAndSaveProject(user); User newUser = createAndSaveUser(); createAndSaveLike(project, newUser); } + given(dateTimeProvider.getCurrentDate()).willReturn(nextSunday); // 조회 날짜를 다음 주 일요일로 설정 + // when - given(dateTimeProvider.getCurrentDate()).willReturn(nextSunday); List responses = projectService.findAllPopularLastWeek(); // then @@ -304,28 +308,31 @@ void setUp() { @Test void 좋아요를_많이_받은_순으로_지난_주_인기_프로젝트_조회를_성공한다() { - // TODO: project likeCount 반영 후 테스트 예정 - // // given - // List projects = new ArrayList<>(); - // for (int i = 0; i < BANNER_PROJECT_COUNT; i++) { - // Project project = createAndSaveProject(user); - // projects.add(project); - // for (int j = 0; j < i + 1; j++) { - // User newUser = createAndSaveUser(); - // createAndSaveLike(project, newUser); - // } - // } - // projects.sort(Comparator.comparing(Project::getLikeCount)); - // List expectResponses = projects.stream() - // .map(ProjectBannerResponse::from) - // .toList(); - // - // // when - // given(dateTimeProvider.getCurrentDate()).willReturn(nextSunday); - // List responses = projectService.findAllPopularLastWeek(); - // - // // then - // assertThat(responses).isEqualTo(expectResponses); + // given + List projects = new ArrayList<>(); + for (int i = 0; i < BANNER_PROJECT_COUNT; + i++) { // 오늘 날짜로 프로젝트 생성 및 각 프로젝트 당 좋아요 i(1, 2...)개 생성 + Project project = createAndSaveProject(user); + projects.add(project); + for (int j = 0; j < i + 1; j++) { + User newUser = createAndSaveUser(); + createAndSaveLike(project, newUser); + } + } + + projects.sort(Comparator.comparing(Project::getLikeCount).reversed()); + + List expectResponses = projects.stream() // 좋아요가 많은 순서대로 응답될 것을 예상 + .map(ProjectBannerResponse::from) + .toList(); + + given(dateTimeProvider.getCurrentDate()).willReturn(nextSunday); + + // when + List responses = projectService.findAllPopularLastWeek(); + + // then + assertThat(responses).isEqualTo(expectResponses); } @Test @@ -337,7 +344,6 @@ void setUp() { // then assertThat(responses).isEmpty(); } - } @Nested From 1494665c0f81ee84d0196579929f9e51878159a3 Mon Sep 17 00:00:00 2001 From: uijin-j Date: Tue, 8 Oct 2024 17:37:07 +0900 Subject: [PATCH 11/19] =?UTF-8?q?chore:=20=EB=8D=94=EB=AF=B8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../db/data/afterMigrate__insert6_users.sql | 91 +++++++++++++++++++ .../db/data/afterMigrate__insert8_likes.sql | 20 ++++ .../db/data/afterMigrate__update1_project.sql | 3 + .../db/data/afterMigrate__update2_likes.sql | 2 + 4 files changed, 116 insertions(+) create mode 100644 src/main/resources/db/data/afterMigrate__insert6_users.sql create mode 100644 src/main/resources/db/data/afterMigrate__insert8_likes.sql create mode 100644 src/main/resources/db/data/afterMigrate__update1_project.sql create mode 100644 src/main/resources/db/data/afterMigrate__update2_likes.sql diff --git a/src/main/resources/db/data/afterMigrate__insert6_users.sql b/src/main/resources/db/data/afterMigrate__insert6_users.sql new file mode 100644 index 00000000..bb7b2cfc --- /dev/null +++ b/src/main/resources/db/data/afterMigrate__insert6_users.sql @@ -0,0 +1,91 @@ +-- 13번부터 100번까지 사용자를 추가하는 SQL 문 +INSERT INTO users(id, nickname, email, password, introduction, profile_image_url, job, career, github_url, blog_url) +VALUES + (13, 'user13', 'user13@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (14, 'user14', 'user14@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (15, 'user15', 'user15@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (16, 'user16', 'user16@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (17, 'user17', 'user17@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (18, 'user18', 'user18@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (19, 'user19', 'user19@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (20, 'user20', 'user20@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (21, 'user21', 'user21@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (22, 'user22', 'user22@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (23, 'user23', 'user23@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (24, 'user24', 'user24@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (25, 'user25', 'user25@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (26, 'user26', 'user26@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (27, 'user27', 'user27@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (28, 'user28', 'user28@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (29, 'user29', 'user29@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (30, 'user30', 'user30@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (31, 'user31', 'user31@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (32, 'user32', 'user32@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (33, 'user33', 'user33@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (34, 'user34', 'user34@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (35, 'user35', 'user35@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (36, 'user36', 'user36@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (37, 'user37', 'user37@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (38, 'user38', 'user38@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (39, 'user39', 'user39@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (40, 'user40', 'user40@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (41, 'user41', 'user41@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (42, 'user42', 'user42@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (43, 'user43', 'user43@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (44, 'user44', 'user44@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (45, 'user45', 'user45@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (46, 'user46', 'user46@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (47, 'user47', 'user47@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (48, 'user48', 'user48@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (49, 'user49', 'user49@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (50, 'user50', 'user50@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (51, 'user51', 'user51@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (52, 'user52', 'user52@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (53, 'user53', 'user53@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (54, 'user54', 'user54@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (55, 'user55', 'user55@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (56, 'user56', 'user56@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (57, 'user57', 'user57@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (58, 'user58', 'user58@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (59, 'user59', 'user59@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (60, 'user60', 'user60@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (61, 'user61', 'user61@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (62, 'user62', 'user62@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (63, 'user63', 'user63@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (64, 'user64', 'user64@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (65, 'user65', 'user65@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (66, 'user66', 'user66@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (67, 'user67', 'user67@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (68, 'user68', 'user68@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (69, 'user69', 'user69@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (70, 'user70', 'user70@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (71, 'user71', 'user71@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (72, 'user72', 'user72@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (73, 'user73', 'user73@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (74, 'user74', 'user74@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (75, 'user75', 'user75@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (76, 'user76', 'user76@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (77, 'user77', 'user77@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (78, 'user78', 'user78@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (79, 'user79', 'user79@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (80, 'user80', 'user80@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (81, 'user81', 'user81@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (82, 'user82', 'user82@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (83, 'user83', 'user83@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (84, 'user84', 'user84@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (85, 'user85', 'user85@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (86, 'user86', 'user86@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (87, 'user87', 'user87@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (88, 'user88', 'user88@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (89, 'user89', 'user89@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (90, 'user90', 'user90@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (91, 'user91', 'user91@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (92, 'user92', 'user92@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (93, 'user93', 'user93@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (94, 'user94', 'user94@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (95, 'user95', 'user95@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (96, 'user96', 'user96@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (97, 'user97', 'user97@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (98, 'user98', 'user98@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (99, 'user99', 'user99@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), + (100, 'user100', 'user100@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL); diff --git a/src/main/resources/db/data/afterMigrate__insert8_likes.sql b/src/main/resources/db/data/afterMigrate__insert8_likes.sql new file mode 100644 index 00000000..84be362d --- /dev/null +++ b/src/main/resources/db/data/afterMigrate__insert8_likes.sql @@ -0,0 +1,20 @@ +-- likes 테이블에 랜덤한 좋아요 추가 +-- 중복 방지를 위해 (user_id, project_id) 조합이 유일해야 함 +-- 총 300개의 랜덤 좋아요 추가 + +INSERT INTO likes (user_id, project_id) +SELECT DISTINCT + FLOOR(RAND() * (100 - 13 + 1)) + 13 AS user_id, -- 유저 ID: 13 ~ 100 사이 + FLOOR(RAND() * (50 - 1 + 1)) + 1 AS project_id -- 프로젝트 ID: 1 ~ 50 사이 +FROM + (SELECT 1 AS n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 + UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 10) t1, + (SELECT 1 AS n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 + UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 10) t2 +-- 중복된 user_id와 project_id의 조합을 피하기 위해 WHERE 조건 사용 +WHERE NOT EXISTS ( + SELECT 1 FROM likes l + WHERE l.user_id = FLOOR(RAND() * (100 - 13 + 1)) + 13 + AND l.project_id = FLOOR(RAND() * (50 - 1 + 1)) + 1 +) +LIMIT 300; -- 최소 300개의 좋아요 생성 diff --git a/src/main/resources/db/data/afterMigrate__update1_project.sql b/src/main/resources/db/data/afterMigrate__update1_project.sql new file mode 100644 index 00000000..ec87e1ab --- /dev/null +++ b/src/main/resources/db/data/afterMigrate__update1_project.sql @@ -0,0 +1,3 @@ +UPDATE project +SET created_at = DATE_SUB(NOW(), INTERVAL 7 DAY) +WHERE id BETWEEN 1 AND 50; diff --git a/src/main/resources/db/data/afterMigrate__update2_likes.sql b/src/main/resources/db/data/afterMigrate__update2_likes.sql new file mode 100644 index 00000000..607f8405 --- /dev/null +++ b/src/main/resources/db/data/afterMigrate__update2_likes.sql @@ -0,0 +1,2 @@ +UPDATE likes +SET created_at = DATE_SUB(NOW(), INTERVAL 7 DAY); From 063cd646d2b37c4f88853cffd2476ca412db4fb6 Mon Sep 17 00:00:00 2001 From: uijin-j Date: Wed, 9 Oct 2024 01:24:54 +0900 Subject: [PATCH 12/19] =?UTF-8?q?rename:=20List=EB=A5=BC=20=EB=B0=94?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=98=ED=99=98=ED=95=98=EB=8A=94=20=EA=B2=83?= =?UTF-8?q?=EC=9D=B4=20=EC=95=84=EB=8B=8C=20=EA=B0=9D=EC=B2=B4=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20ProjectBannerRe?= =?UTF-8?q?sponse=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/doc/ProjectControllerDoc.java | 3 +-- .../controller/ProjectController.java | 5 ++-- .../dto/response/ProjectBannerResponse.java | 21 ++++----------- .../projects/dto/response/ProjectSummary.java | 27 +++++++++++++++++++ .../project/PopularProjectRepository.java | 4 +-- .../QuerydslPopularProjectRepository.java | 6 ++--- .../projects/service/ProjectService.java | 12 ++++++--- .../projects/service/ProjectServiceTest.java | 17 ++++++------ 8 files changed, 58 insertions(+), 37 deletions(-) create mode 100644 src/main/java/sixgaezzang/sidepeek/projects/dto/response/ProjectSummary.java diff --git a/src/main/java/sixgaezzang/sidepeek/common/doc/ProjectControllerDoc.java b/src/main/java/sixgaezzang/sidepeek/common/doc/ProjectControllerDoc.java index 290730c3..b2e7375e 100644 --- a/src/main/java/sixgaezzang/sidepeek/common/doc/ProjectControllerDoc.java +++ b/src/main/java/sixgaezzang/sidepeek/common/doc/ProjectControllerDoc.java @@ -24,7 +24,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import sixgaezzang.sidepeek.projects.dto.request.FindProjectRequest; @@ -79,7 +78,7 @@ ResponseEntity> getByCondition( @ApiResponse(responseCode = "200", description = OK_DESCRIPTION, useReturnTypeSchema = true) }) - ResponseEntity> getWeeklyPopular(); + ResponseEntity getWeeklyPopular(); @Operation(summary = "프로젝트 수정", description = "작성자와 등록된 프로젝트 회원 멤버만 수정 가능, 로그인 필수") @ApiResponses({ diff --git a/src/main/java/sixgaezzang/sidepeek/projects/controller/ProjectController.java b/src/main/java/sixgaezzang/sidepeek/projects/controller/ProjectController.java index 42315309..1830ecfd 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/controller/ProjectController.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/controller/ProjectController.java @@ -2,7 +2,6 @@ import jakarta.validation.Valid; import java.net.URI; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -77,8 +76,8 @@ public ResponseEntity> getByCondit */ @Override @GetMapping("/weekly") - public ResponseEntity> getWeeklyPopular() { - List responses = projectService.findAllPopularLastWeek(); + public ResponseEntity getWeeklyPopular() { + ProjectBannerResponse responses = projectService.findAllPopularLastWeek(); return ResponseEntity.ok(responses); } diff --git a/src/main/java/sixgaezzang/sidepeek/projects/dto/response/ProjectBannerResponse.java b/src/main/java/sixgaezzang/sidepeek/projects/dto/response/ProjectBannerResponse.java index 428fd21d..1a6cae3e 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/dto/response/ProjectBannerResponse.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/dto/response/ProjectBannerResponse.java @@ -1,29 +1,18 @@ package sixgaezzang.sidepeek.projects.dto.response; import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; import lombok.Builder; -import sixgaezzang.sidepeek.projects.domain.Project; -@Schema(description = "인기 프로젝트 응답") +@Schema(description = "인기 프로젝트 리스트 응답") @Builder public record ProjectBannerResponse( - @Schema(description = "프로젝트 식별자", example = "1") - Long id, - @Schema(description = "프로젝트 제목", example = "사이드픽👀") - String name, - @Schema(description = "프로젝트 부제목, 없으면 빈 문자열 반환", example = "요즘 사이드 플젝 뭐함? 사이드픽 \uD83D\uDC40") - String subName, - @Schema(description = "프로젝트 썸네일 이미지 URL, 없으면 빈 문자열 반환", example = "https://sidepeek.image/imageeUrl") - String thumbnailUrl + List projects ) { - public static ProjectBannerResponse from(Project project) { + public static ProjectBannerResponse from(List projects) { return ProjectBannerResponse.builder() - .id(project.getId()) - .name(project.getName()) - .subName(project.getSubName()) - .thumbnailUrl(project.getThumbnailUrl()) + .projects(projects) .build(); } - } diff --git a/src/main/java/sixgaezzang/sidepeek/projects/dto/response/ProjectSummary.java b/src/main/java/sixgaezzang/sidepeek/projects/dto/response/ProjectSummary.java new file mode 100644 index 00000000..00bb4a36 --- /dev/null +++ b/src/main/java/sixgaezzang/sidepeek/projects/dto/response/ProjectSummary.java @@ -0,0 +1,27 @@ +package sixgaezzang.sidepeek.projects.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import sixgaezzang.sidepeek.projects.domain.Project; + +@Schema(description = "인기 프로젝트 정보") +public record ProjectSummary( + @Schema(description = "프로젝트 식별자", example = "1") + Long id, + @Schema(description = "프로젝트 제목", example = "사이드픽👀") + String name, + @Schema(description = "프로젝트 부제목, 없으면 빈 문자열 반환", example = "요즘 사이드 플젝 뭐함? 사이드픽 \uD83D\uDC40") + String subName, + @Schema(description = "프로젝트 썸네일 이미지 URL, 없으면 빈 문자열 반환", example = "https://sidepeek.image/imageeUrl") + String thumbnailUrl +) { + + public static ProjectSummary from(Project project) { + return new ProjectSummary( + project.getId(), + project.getName(), + project.getSubName(), + project.getThumbnailUrl() + ); + } + +} diff --git a/src/main/java/sixgaezzang/sidepeek/projects/repository/project/PopularProjectRepository.java b/src/main/java/sixgaezzang/sidepeek/projects/repository/project/PopularProjectRepository.java index b2eb8182..ce306009 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/repository/project/PopularProjectRepository.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/repository/project/PopularProjectRepository.java @@ -2,10 +2,10 @@ import java.time.LocalDate; import java.util.List; -import sixgaezzang.sidepeek.projects.dto.response.ProjectBannerResponse; +import sixgaezzang.sidepeek.projects.dto.response.ProjectSummary; public interface PopularProjectRepository { - List findRankBetweenPeriod(LocalDate startDate, LocalDate endDate, + List findRankBetweenPeriod(LocalDate startDate, LocalDate endDate, int count); } diff --git a/src/main/java/sixgaezzang/sidepeek/projects/repository/project/QuerydslPopularProjectRepository.java b/src/main/java/sixgaezzang/sidepeek/projects/repository/project/QuerydslPopularProjectRepository.java index 7401342b..3e8b71c6 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/repository/project/QuerydslPopularProjectRepository.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/repository/project/QuerydslPopularProjectRepository.java @@ -11,7 +11,7 @@ import java.util.List; import org.springframework.stereotype.Repository; import sixgaezzang.sidepeek.projects.domain.Project; -import sixgaezzang.sidepeek.projects.dto.response.ProjectBannerResponse; +import sixgaezzang.sidepeek.projects.dto.response.ProjectSummary; @Repository public class QuerydslPopularProjectRepository implements PopularProjectRepository { @@ -30,7 +30,7 @@ public QuerydslPopularProjectRepository(EntityManager em) { * 3. 최신 순 */ @Override - public List findRankBetweenPeriod( + public List findRankBetweenPeriod( LocalDate startDate, LocalDate endDate, int count @@ -49,7 +49,7 @@ public List findRankBetweenPeriod( .fetch(); return projects.stream() - .map(ProjectBannerResponse::from) + .map(ProjectSummary::from) .toList(); } } diff --git a/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java b/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java index 9a2d28a7..29cea886 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java @@ -115,13 +115,19 @@ public ProjectResponse findById(String ip, Long loginId, Long projectId) { return ProjectResponse.from(project, overviewImages, techStacks, members, comments, likeId); } - public List findAllPopularLastWeek() { + /** + * 지난 주 인기 프로젝트 조회 + */ + public ProjectBannerResponse findAllPopularLastWeek() { LocalDate today = dateTimeProvider.getCurrentDate(); LocalDate startDate = DateUtils.getStartDayOfLastWeek(today); LocalDate endDate = DateUtils.getEndDayOfLastWeek(today); - return popularProjectRepository.findRankBetweenPeriod(startDate, endDate, - BANNER_PROJECT_COUNT); + List projects = popularProjectRepository.findRankBetweenPeriod( + startDate, endDate, BANNER_PROJECT_COUNT); + ProjectBannerResponse response = ProjectBannerResponse.from(projects); + + return response; } public Page findByUser(Long userId, Long loginId, diff --git a/src/test/java/sixgaezzang/sidepeek/projects/service/ProjectServiceTest.java b/src/test/java/sixgaezzang/sidepeek/projects/service/ProjectServiceTest.java index 9a3473d2..6da60697 100644 --- a/src/test/java/sixgaezzang/sidepeek/projects/service/ProjectServiceTest.java +++ b/src/test/java/sixgaezzang/sidepeek/projects/service/ProjectServiceTest.java @@ -77,6 +77,7 @@ import sixgaezzang.sidepeek.projects.dto.response.ProjectBannerResponse; import sixgaezzang.sidepeek.projects.dto.response.ProjectListResponse; import sixgaezzang.sidepeek.projects.dto.response.ProjectResponse; +import sixgaezzang.sidepeek.projects.dto.response.ProjectSummary; import sixgaezzang.sidepeek.projects.repository.FileRepository; import sixgaezzang.sidepeek.projects.repository.MemberRepository; import sixgaezzang.sidepeek.projects.repository.ProjectSkillRepository; @@ -300,10 +301,10 @@ void setUp() { given(dateTimeProvider.getCurrentDate()).willReturn(nextSunday); // 조회 날짜를 다음 주 일요일로 설정 // when - List responses = projectService.findAllPopularLastWeek(); + ProjectBannerResponse responses = projectService.findAllPopularLastWeek(); // then - assertThat(responses).hasSize(BANNER_PROJECT_COUNT); + assertThat(responses.projects()).hasSize(BANNER_PROJECT_COUNT); } @Test @@ -322,27 +323,27 @@ void setUp() { projects.sort(Comparator.comparing(Project::getLikeCount).reversed()); - List expectResponses = projects.stream() // 좋아요가 많은 순서대로 응답될 것을 예상 - .map(ProjectBannerResponse::from) + List expectResponses = projects.stream() // 좋아요가 많은 순서대로 응답될 것을 예상 + .map(ProjectSummary::from) .toList(); given(dateTimeProvider.getCurrentDate()).willReturn(nextSunday); // when - List responses = projectService.findAllPopularLastWeek(); + ProjectBannerResponse responses = projectService.findAllPopularLastWeek(); // then - assertThat(responses).isEqualTo(expectResponses); + assertThat(responses.projects()).isEqualTo(expectResponses); } @Test void 지난_주에_좋아요_기록이_없어_빈_배열로_지난_주_인기_프로젝트_조회를_성공한다() { // given, when given(dateTimeProvider.getCurrentDate()).willReturn(nextSunday); - List responses = projectService.findAllPopularLastWeek(); + ProjectBannerResponse responses = projectService.findAllPopularLastWeek(); // then - assertThat(responses).isEmpty(); + assertThat(responses.projects()).isEmpty(); } } From 36fb27a27ceec8c716577bc6bc8e4719a27a4ab7 Mon Sep 17 00:00:00 2001 From: uijin-j Date: Wed, 9 Oct 2024 01:29:35 +0900 Subject: [PATCH 13/19] =?UTF-8?q?feat:=20DateUtils=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EC=97=90=20getLastDayOfWeek()=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getLastDayOfWeek(LocalDate date): date를 기준으로 지난 일요일 반환 (date가 일요일이면 그래도 date 반환) --- .../sidepeek/projects/util/DateUtils.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/sixgaezzang/sidepeek/projects/util/DateUtils.java b/src/main/java/sixgaezzang/sidepeek/projects/util/DateUtils.java index b88757e0..8d974ece 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/util/DateUtils.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/util/DateUtils.java @@ -34,6 +34,18 @@ public static LocalDate getEndDayOfLastWeek(LocalDate localDate) { return getNearestPastDayOfWeek(localDate, DayOfWeek.SUNDAY); } + public static LocalDate getLastDayOfWeek(LocalDate date) { + DayOfWeek dayOfWeek = date.getDayOfWeek(); + + // 오늘이 일요일이면 그대로 반환 + if (dayOfWeek == DayOfWeek.SUNDAY) { + return date; + } + + int daysUntilSunday = DayOfWeek.SUNDAY.getValue() - dayOfWeek.getValue(); + return date.plusDays(daysUntilSunday); // 오늘로부터 일요일까지의 날짜를 더한다. + } + /** * {@code localDate} 기준으로 {@code localDate}에 해당하는 요일인 * 가장 가까운 과거 날짜를 찾는 메서드 @@ -44,5 +56,4 @@ public static LocalDate getEndDayOfLastWeek(LocalDate localDate) { public static LocalDate getNearestPastDayOfWeek(LocalDate localDate, DayOfWeek dayOfWeek) { return localDate.with(TemporalAdjusters.previous(dayOfWeek)); } - } From b084f3556ab90e8d3b759592ec9b1a16758d4fdb Mon Sep 17 00:00:00 2001 From: uijin-j Date: Wed, 9 Oct 2024 01:29:47 +0900 Subject: [PATCH 14/19] =?UTF-8?q?chore:=20RedisConfig=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/sixgaezzang/sidepeek/config/RedisConfig.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/sixgaezzang/sidepeek/config/RedisConfig.java b/src/main/java/sixgaezzang/sidepeek/config/RedisConfig.java index bead95ee..03ff5a23 100644 --- a/src/main/java/sixgaezzang/sidepeek/config/RedisConfig.java +++ b/src/main/java/sixgaezzang/sidepeek/config/RedisConfig.java @@ -26,11 +26,16 @@ public RedisConnectionFactory redisConnectionFactory() { } @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { - RedisTemplate template = new RedisTemplate<>(); + public RedisTemplate redisTemplate( + RedisConnectionFactory redisConnectionFactory + ) { + RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new StringRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new StringRedisSerializer()); + return template; } From 28f96743e26962cfb7a8c95de1393bcca9d44124 Mon Sep 17 00:00:00 2001 From: uijin-j Date: Wed, 9 Oct 2024 01:30:47 +0900 Subject: [PATCH 15/19] =?UTF-8?q?refactore:=20=EC=9D=B8=EA=B8=B0=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=97=90=20=EC=BA=90=EC=8B=9C=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/PopularProjectCacheService.java | 62 +++++++++++++++++++ .../projects/service/ProjectService.java | 34 +++++++--- 2 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 src/main/java/sixgaezzang/sidepeek/projects/service/PopularProjectCacheService.java diff --git a/src/main/java/sixgaezzang/sidepeek/projects/service/PopularProjectCacheService.java b/src/main/java/sixgaezzang/sidepeek/projects/service/PopularProjectCacheService.java new file mode 100644 index 00000000..a259d327 --- /dev/null +++ b/src/main/java/sixgaezzang/sidepeek/projects/service/PopularProjectCacheService.java @@ -0,0 +1,62 @@ +package sixgaezzang.sidepeek.projects.service; + +import static org.apache.commons.lang3.StringUtils.isBlank; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; +import sixgaezzang.sidepeek.projects.dto.response.ProjectBannerResponse; + +@Service +@RequiredArgsConstructor +@Slf4j +public class PopularProjectCacheService { + + private static final String WEEKLY_POPULAR_CACHE_KEY = "popularProjectsLastWeek"; + private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + // 캐시에서 인기 프로젝트를 조회 + public Optional getPopularProjects() { + String value = redisTemplate.opsForValue().get(WEEKLY_POPULAR_CACHE_KEY); + + if (isBlank(value)) { + return Optional.empty(); + } + + try { + return Optional.of(objectMapper.readValue(value.trim(), ProjectBannerResponse.class)); + } catch (Exception e) { + throw new RuntimeException("캐시에서 데이터를 가져오는데 실패하였습니다."); + } + } + + // 캐시에 인기 프로젝트를 저장하고 TTL 설정 + public void putPopularProjects(ProjectBannerResponse data, LocalDateTime expireTime) { + ValueOperations valueOps = redisTemplate.opsForValue(); + + try { + String value = objectMapper.writeValueAsString(data); + valueOps.set(WEEKLY_POPULAR_CACHE_KEY, value, calculateTTL(expireTime)); + } catch (Exception e) { + throw new RuntimeException("캐시에 데이터를 저장하는데 실패하였습니다."); + } + } + + private long calculateTTL(LocalDateTime expireTime) { + LocalDateTime now = LocalDateTime.now(); // 현재 시각 + + // 현재 시각과 lastDayOfWeek 자정 사이의 차이를 계산 + Duration duration = Duration.between(now, expireTime); + + log.info("캐시 만료 시간: {}", duration.getSeconds()); + + return duration.getSeconds(); + } +} diff --git a/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java b/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java index 29cea886..a5defdc4 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java @@ -11,6 +11,8 @@ import jakarta.persistence.EntityNotFoundException; import java.time.Duration; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -38,6 +40,7 @@ import sixgaezzang.sidepeek.projects.dto.response.ProjectListResponse; import sixgaezzang.sidepeek.projects.dto.response.ProjectResponse; import sixgaezzang.sidepeek.projects.dto.response.ProjectSkillSummary; +import sixgaezzang.sidepeek.projects.dto.response.ProjectSummary; import sixgaezzang.sidepeek.projects.repository.project.PopularProjectRepository; import sixgaezzang.sidepeek.projects.repository.project.ProjectRepository; import sixgaezzang.sidepeek.projects.util.DateUtils; @@ -59,6 +62,8 @@ public class ProjectService { private final LikeRepository likeRepository; private final CommentService commentService; private final RedisTemplate redisTemplate; + private final PopularProjectCacheService popularProjectCacheService; + @Transactional public ProjectResponse save(Long loginId, SaveProjectRequest request) { @@ -119,15 +124,9 @@ public ProjectResponse findById(String ip, Long loginId, Long projectId) { * 지난 주 인기 프로젝트 조회 */ public ProjectBannerResponse findAllPopularLastWeek() { - LocalDate today = dateTimeProvider.getCurrentDate(); - LocalDate startDate = DateUtils.getStartDayOfLastWeek(today); - LocalDate endDate = DateUtils.getEndDayOfLastWeek(today); - - List projects = popularProjectRepository.findRankBetweenPeriod( - startDate, endDate, BANNER_PROJECT_COUNT); - ProjectBannerResponse response = ProjectBannerResponse.from(projects); - - return response; + // Step 1. 캐시 조회 -> Step 2. (캐시가 존재하지 않다면) DB에서 조회 후 캐시 + return popularProjectCacheService.getPopularProjects() + .orElseGet(this::updateWeeklyPopularProjectsCache); } public Page findByUser(Long userId, Long loginId, @@ -227,4 +226,21 @@ private void increaseViewCount(String ip, Project project) { redisTemplate.opsForValue().set(viewCountKey, "ON", Duration.ofDays(1)); } } + + private ProjectBannerResponse updateWeeklyPopularProjectsCache() { + // Step 1-2. 캐시가 존재하지 않거나 비어있다면 DB에서 조회 + LocalDate today = dateTimeProvider.getCurrentDate(); + LocalDate startDate = DateUtils.getStartDayOfLastWeek(today); + LocalDate endDate = DateUtils.getEndDayOfLastWeek(today); + + List projects = popularProjectRepository.findRankBetweenPeriod( + startDate, endDate, BANNER_PROJECT_COUNT); + ProjectBannerResponse response = ProjectBannerResponse.from(projects); + + // Step 2. 응답 결과를 캐시에 저장 (이번주 일요일 자정까지 캐시 유효) + LocalDateTime expireTime = DateUtils.getLastDayOfWeek(today).atTime(LocalTime.MAX); + popularProjectCacheService.putPopularProjects(response, expireTime); + + return response; + } } From c53368aaec11cf60d2eefa414018f653102fd468 Mon Sep 17 00:00:00 2001 From: uijin-j Date: Wed, 9 Oct 2024 03:08:15 +0900 Subject: [PATCH 16/19] =?UTF-8?q?chore:=20=EB=8D=94=EB=AF=B8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../db/data/afterMigrate__insert6_users.sql | 113 ++++-------------- .../db/data/afterMigrate__insert7_likes.sql | 18 +++ .../db/data/afterMigrate__insert8_likes.sql | 20 ---- 3 files changed, 40 insertions(+), 111 deletions(-) create mode 100644 src/main/resources/db/data/afterMigrate__insert7_likes.sql delete mode 100644 src/main/resources/db/data/afterMigrate__insert8_likes.sql diff --git a/src/main/resources/db/data/afterMigrate__insert6_users.sql b/src/main/resources/db/data/afterMigrate__insert6_users.sql index bb7b2cfc..8d69906b 100644 --- a/src/main/resources/db/data/afterMigrate__insert6_users.sql +++ b/src/main/resources/db/data/afterMigrate__insert6_users.sql @@ -1,91 +1,22 @@ --- 13번부터 100번까지 사용자를 추가하는 SQL 문 -INSERT INTO users(id, nickname, email, password, introduction, profile_image_url, job, career, github_url, blog_url) -VALUES - (13, 'user13', 'user13@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (14, 'user14', 'user14@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (15, 'user15', 'user15@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (16, 'user16', 'user16@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (17, 'user17', 'user17@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (18, 'user18', 'user18@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (19, 'user19', 'user19@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (20, 'user20', 'user20@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (21, 'user21', 'user21@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (22, 'user22', 'user22@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (23, 'user23', 'user23@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (24, 'user24', 'user24@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (25, 'user25', 'user25@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (26, 'user26', 'user26@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (27, 'user27', 'user27@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (28, 'user28', 'user28@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (29, 'user29', 'user29@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (30, 'user30', 'user30@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (31, 'user31', 'user31@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (32, 'user32', 'user32@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (33, 'user33', 'user33@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (34, 'user34', 'user34@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (35, 'user35', 'user35@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (36, 'user36', 'user36@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (37, 'user37', 'user37@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (38, 'user38', 'user38@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (39, 'user39', 'user39@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (40, 'user40', 'user40@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (41, 'user41', 'user41@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (42, 'user42', 'user42@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (43, 'user43', 'user43@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (44, 'user44', 'user44@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (45, 'user45', 'user45@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (46, 'user46', 'user46@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (47, 'user47', 'user47@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (48, 'user48', 'user48@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (49, 'user49', 'user49@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (50, 'user50', 'user50@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (51, 'user51', 'user51@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (52, 'user52', 'user52@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (53, 'user53', 'user53@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (54, 'user54', 'user54@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (55, 'user55', 'user55@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (56, 'user56', 'user56@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (57, 'user57', 'user57@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (58, 'user58', 'user58@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (59, 'user59', 'user59@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (60, 'user60', 'user60@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (61, 'user61', 'user61@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (62, 'user62', 'user62@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (63, 'user63', 'user63@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (64, 'user64', 'user64@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (65, 'user65', 'user65@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (66, 'user66', 'user66@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (67, 'user67', 'user67@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (68, 'user68', 'user68@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (69, 'user69', 'user69@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (70, 'user70', 'user70@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (71, 'user71', 'user71@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (72, 'user72', 'user72@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (73, 'user73', 'user73@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (74, 'user74', 'user74@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (75, 'user75', 'user75@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (76, 'user76', 'user76@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (77, 'user77', 'user77@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (78, 'user78', 'user78@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (79, 'user79', 'user79@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (80, 'user80', 'user80@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (81, 'user81', 'user81@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (82, 'user82', 'user82@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (83, 'user83', 'user83@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (84, 'user84', 'user84@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (85, 'user85', 'user85@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (86, 'user86', 'user86@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (87, 'user87', 'user87@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (88, 'user88', 'user88@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (89, 'user89', 'user89@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (90, 'user90', 'user90@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (91, 'user91', 'user91@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (92, 'user92', 'user92@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (93, 'user93', 'user93@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (94, 'user94', 'user94@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (95, 'user95', 'user95@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (96, 'user96', 'user96@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (97, 'user97', 'user97@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (98, 'user98', 'user98@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (99, 'user99', 'user99@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL), - (100, 'user100', 'user100@naver.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL); +-- 1000명의 사용자 추가 생성 +INSERT INTO users (id, nickname, email, password, introduction, profile_image_url, job, career, github_url, blog_url) +SELECT + t.n AS id, + CONCAT('user', t.n) AS nickname, + CONCAT('user', t.n, '@naver.com') AS email, + NULL AS password, + NULL AS introduction, + NULL AS profile_image_url, + NULL AS job, + NULL AS career, + NULL AS github_url, + NULL AS blog_url +FROM ( + SELECT @row := @row + 1 AS n + FROM (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a, + (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b, + (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) c, + (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) d, + (SELECT @row := 12) AS init -- 초기값 설정 + ) t +WHERE t.n <= 1000; diff --git a/src/main/resources/db/data/afterMigrate__insert7_likes.sql b/src/main/resources/db/data/afterMigrate__insert7_likes.sql new file mode 100644 index 00000000..327b0aed --- /dev/null +++ b/src/main/resources/db/data/afterMigrate__insert7_likes.sql @@ -0,0 +1,18 @@ +-- 모든 유저가 모든 프로젝트에 좋아요를 하도록 변경 +-- 중복 방지를 위해 (user_id, project_id) 조합이 유일해야 함 +-- 총 100 * 50 (5000)개의 좋아요 추가 +INSERT INTO likes (user_id, project_id) +SELECT + u.id AS user_id, + p.id AS project_id +FROM + (SELECT DISTINCT id FROM users) u -- 모든 사용자 + CROSS JOIN + (SELECT DISTINCT id FROM project WHERE deleted_at IS NULL) p -- 모든 프로젝트 +WHERE + NOT EXISTS ( + SELECT 1 + FROM likes l + WHERE l.user_id = u.id + AND l.project_id = p.id + ); diff --git a/src/main/resources/db/data/afterMigrate__insert8_likes.sql b/src/main/resources/db/data/afterMigrate__insert8_likes.sql deleted file mode 100644 index 84be362d..00000000 --- a/src/main/resources/db/data/afterMigrate__insert8_likes.sql +++ /dev/null @@ -1,20 +0,0 @@ --- likes 테이블에 랜덤한 좋아요 추가 --- 중복 방지를 위해 (user_id, project_id) 조합이 유일해야 함 --- 총 300개의 랜덤 좋아요 추가 - -INSERT INTO likes (user_id, project_id) -SELECT DISTINCT - FLOOR(RAND() * (100 - 13 + 1)) + 13 AS user_id, -- 유저 ID: 13 ~ 100 사이 - FLOOR(RAND() * (50 - 1 + 1)) + 1 AS project_id -- 프로젝트 ID: 1 ~ 50 사이 -FROM - (SELECT 1 AS n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 - UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 10) t1, - (SELECT 1 AS n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 - UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 10) t2 --- 중복된 user_id와 project_id의 조합을 피하기 위해 WHERE 조건 사용 -WHERE NOT EXISTS ( - SELECT 1 FROM likes l - WHERE l.user_id = FLOOR(RAND() * (100 - 13 + 1)) + 13 - AND l.project_id = FLOOR(RAND() * (50 - 1 + 1)) + 1 -) -LIMIT 300; -- 최소 300개의 좋아요 생성 From de963fe3baacc4fc2800e68ef80ec9077755ae0a Mon Sep 17 00:00:00 2001 From: uijin-j Date: Wed, 9 Oct 2024 03:08:42 +0900 Subject: [PATCH 17/19] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=A1=9C=EA=B7=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sidepeek/projects/service/PopularProjectCacheService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/sixgaezzang/sidepeek/projects/service/PopularProjectCacheService.java b/src/main/java/sixgaezzang/sidepeek/projects/service/PopularProjectCacheService.java index a259d327..a97e1608 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/service/PopularProjectCacheService.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/service/PopularProjectCacheService.java @@ -55,8 +55,6 @@ private long calculateTTL(LocalDateTime expireTime) { // 현재 시각과 lastDayOfWeek 자정 사이의 차이를 계산 Duration duration = Duration.between(now, expireTime); - log.info("캐시 만료 시간: {}", duration.getSeconds()); - return duration.getSeconds(); } } From 9c71cc3a4bbdafd59481215e4dc8d0018976becf Mon Sep 17 00:00:00 2001 From: uijin-j Date: Wed, 9 Oct 2024 03:09:39 +0900 Subject: [PATCH 18/19] =?UTF-8?q?style:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EA=B3=B5=EB=B0=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sixgaezzang/sidepeek/projects/service/ProjectService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java b/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java index a5defdc4..f9df257e 100644 --- a/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java +++ b/src/main/java/sixgaezzang/sidepeek/projects/service/ProjectService.java @@ -63,8 +63,7 @@ public class ProjectService { private final CommentService commentService; private final RedisTemplate redisTemplate; private final PopularProjectCacheService popularProjectCacheService; - - + @Transactional public ProjectResponse save(Long loginId, SaveProjectRequest request) { validateLoginId(loginId); From 71daacaf392e7dd7b9b4dce8d6e0b9e1a88b7162 Mon Sep 17 00:00:00 2001 From: uijin-j Date: Wed, 9 Oct 2024 03:22:37 +0900 Subject: [PATCH 19/19] =?UTF-8?q?test:=20=EB=8B=A8=EC=9C=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EA=B0=80=20=EB=81=9D=EB=82=A0=20=EB=95=8C?= =?UTF-8?q?=EB=A7=88=EB=8B=A4=20=EC=BA=90=EC=8B=9C=EB=A5=BC=20=EB=B9=84?= =?UTF-8?q?=EC=9A=B0=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../projects/service/ProjectServiceTest.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/test/java/sixgaezzang/sidepeek/projects/service/ProjectServiceTest.java b/src/test/java/sixgaezzang/sidepeek/projects/service/ProjectServiceTest.java index 6da60697..823257bc 100644 --- a/src/test/java/sixgaezzang/sidepeek/projects/service/ProjectServiceTest.java +++ b/src/test/java/sixgaezzang/sidepeek/projects/service/ProjectServiceTest.java @@ -46,6 +46,7 @@ import java.util.Optional; import net.datafaker.Faker; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -57,6 +58,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.Pageable; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.transaction.annotation.Transactional; @@ -135,6 +137,9 @@ class ProjectServiceTest { @Autowired FileRepository fileRepository; + @Autowired + RedisTemplate redisTemplate; + User user; private User createAndSaveUser() { @@ -287,18 +292,25 @@ void setUp() { nextSunday = today.with(TemporalAdjusters.next(DayOfWeek.SUNDAY)); } + @AfterEach + void tearDown() { + redisTemplate.delete("popularProjectsLastWeek"); + } + @Test void 최대_5개로_지난_주_인기_프로젝트_조회를_성공한다() { // given int overBannerProjectCount = BANNER_PROJECT_COUNT * 2; - for (int i = 0; i < overBannerProjectCount; - i++) { // 오늘 날짜로 프로젝트 생성 및 각 프로젝트 당 좋아요 1개 생성 + + // 오늘 날짜로 프로젝트 생성 및 각 프로젝트 당 좋아요 1개 생성 + for (int i = 0; i < overBannerProjectCount; i++) { Project project = createAndSaveProject(user); User newUser = createAndSaveUser(); createAndSaveLike(project, newUser); } - given(dateTimeProvider.getCurrentDate()).willReturn(nextSunday); // 조회 날짜를 다음 주 일요일로 설정 + // 조회 날짜를 다음 주 일요일로 설정 + given(dateTimeProvider.getCurrentDate()).willReturn(nextSunday); // when ProjectBannerResponse responses = projectService.findAllPopularLastWeek();