From 321ab3ef5f6ffbfcbc278741904abd9bacc73d6a Mon Sep 17 00:00:00 2001 From: sckwon770 Date: Mon, 6 May 2024 17:22:41 +0900 Subject: [PATCH] =?UTF-8?q?[OING-325]=20feat:=20=EA=B0=80=EC=A1=B1?= =?UTF-8?q?=EA=B5=AC=EC=84=B1=EC=9B=90=20=EB=9E=AD=ED=82=B9=20API=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#246)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add getFamilyMembersMonthlySurvivalRanking API in PostController * feat: Add mvc test code for new API(getFamilyMembersMonthlySurvivalRanking) with nested test class * feat: Impl getFamilyMemberMonthlyRanking API at MainViewController * feat: Add mvc test code for new API(getFamilyMemberMonthlyRanking) at MainViewApiTest * fix: Add stream filter to exclude familyMember who didnt upload post at that month from getFamilyMembersMonthlySurvivalRanking API of PostController and Add test code to validate it from PostApiTest and MainViewApiTest * fix: Fix failed broken test code * fix: Fix failed broken test code2 * refactor: Refactor compareTo method of PostRankerDTO for better readability * refactor: Add blank line at that last of MainViewApiTest * refactor: Extract duplicated mapping code from getFamilyMemberMonrhlyRanking method of MainViewController * fix: Fix IndexOutOfBoundsException from getFamilyMemberMonthlyRanking API --- .../oing/controller/MainViewController.java | 42 ++- .../FamilyMemberMonthlyRankingResponse.java | 6 +- .../response/FamilyMemberRankerResponse.java | 1 - .../repository/PostRepositoryCustomImpl.java | 44 ++- .../com/oing/restapi/MainViewApiTest.java | 296 ++++++++++++++++++ .../java/com/oing/restapi/PostApiTest.java | 193 +++++++++++- .../com/oing/controller/PostController.java | 29 ++ .../java/com/oing/dto/dto/PostRankerDTO.java | 44 +++ .../oing/dto/response/PostRankerResponse.java | 14 + .../repository/CommentRepositoryCustom.java | 2 + .../CommentRepositoryCustomImpl.java | 11 + .../oing/repository/PostRepositoryCustom.java | 2 + .../oing/repository/ReactionRepository.java | 2 +- .../repository/ReactionRepositoryCustom.java | 6 + .../ReactionRepositoryCustomImpl.java | 24 ++ .../main/java/com/oing/restapi/PostApi.java | 11 + .../java/com/oing/service/CommentService.java | 9 + .../java/com/oing/service/PostService.java | 7 + .../com/oing/service/ReactionService.java | 8 + 19 files changed, 725 insertions(+), 26 deletions(-) create mode 100644 gateway/src/test/java/com/oing/restapi/MainViewApiTest.java create mode 100644 post/src/main/java/com/oing/dto/dto/PostRankerDTO.java create mode 100644 post/src/main/java/com/oing/dto/response/PostRankerResponse.java create mode 100644 post/src/main/java/com/oing/repository/ReactionRepositoryCustom.java create mode 100644 post/src/main/java/com/oing/repository/ReactionRepositoryCustomImpl.java diff --git a/gateway/src/main/java/com/oing/controller/MainViewController.java b/gateway/src/main/java/com/oing/controller/MainViewController.java index ba88988f..ff8c998f 100644 --- a/gateway/src/main/java/com/oing/controller/MainViewController.java +++ b/gateway/src/main/java/com/oing/controller/MainViewController.java @@ -26,6 +26,7 @@ public class MainViewController implements MainViewApi { private final PostService postService; private final MemberService memberService; private final MemberPickService memberPickService; + private final MemberController memberController; private final MemberBridge memberBridge; private final MissionBridge missionBridge; private final PostController postController; @@ -164,13 +165,44 @@ public NighttimePageResponse getNighttimePage(String loginMemberId, String login @Override public FamilyMemberMonthlyRankingResponse getFamilyMemberMonthlyRanking(String loginMemberId, String loginFamilyId) { - // TODO: API Response Mocking 입니다. + List ranking = postController.getFamilyMembersMonthlySurvivalRanking(loginFamilyId).results().stream().toList(); + + FamilyMemberRankerResponse first = null; + if (ranking.size() >= 1) { + first = getFamilyMemberRankerResponse(ranking.get(0), loginFamilyId); + } + + FamilyMemberRankerResponse second = null; + if (ranking.size() >= 2) { + second = getFamilyMemberRankerResponse(ranking.get(1), loginFamilyId); + } - FamilyMemberRankerResponse first = new FamilyMemberRankerResponse("https://static01.nyt.com/images/2016/09/28/us/28xp-pepefrog/28xp-pepefrog-superJumbo.jpg", "정신적 지주", 24); - FamilyMemberRankerResponse second = new FamilyMemberRankerResponse("https://static01.nyt.com/images/2016/09/28/us/28xp-pepefrog/28xp-pepefrog-superJumbo.jpg", "권순찬", 23); FamilyMemberRankerResponse third = null; - LocalDate mostRecentSurvivalPostDate = LocalDate.now().minusDays(1); + if (ranking.size() >= 3) { + third = getFamilyMemberRankerResponse(ranking.get(2), loginFamilyId); + } - return new FamilyMemberMonthlyRankingResponse(4, first, second, third, mostRecentSurvivalPostDate); + LocalDate mostRecentSurvivalPostDate = null; + List mostRecentPosts = postController.fetchDailyFeeds(1, 1, null, null, "desc", PostType.SURVIVAL, loginMemberId, false).results().stream().toList(); + if (!mostRecentPosts.isEmpty()) { + mostRecentSurvivalPostDate = mostRecentPosts.get(0).createdAt().toLocalDate(); + } + + return new FamilyMemberMonthlyRankingResponse( + ZonedDateTime.now().getMonthValue(), + first, + second, + third, + mostRecentSurvivalPostDate + ); + } + + private FamilyMemberRankerResponse getFamilyMemberRankerResponse(PostRankerResponse postRankerResponse, String loginFamilyId) { + MemberResponse postRankerMember = memberController.getMember(postRankerResponse.memberId(), loginFamilyId); + return new FamilyMemberRankerResponse( + postRankerMember.imageUrl(), + postRankerMember.name(), + postRankerResponse.postCount() + ); } } diff --git a/gateway/src/main/java/com/oing/dto/response/FamilyMemberMonthlyRankingResponse.java b/gateway/src/main/java/com/oing/dto/response/FamilyMemberMonthlyRankingResponse.java index 91c380d8..f5a2b190 100644 --- a/gateway/src/main/java/com/oing/dto/response/FamilyMemberMonthlyRankingResponse.java +++ b/gateway/src/main/java/com/oing/dto/response/FamilyMemberMonthlyRankingResponse.java @@ -12,13 +12,13 @@ public record FamilyMemberMonthlyRankingResponse( @Schema(description = "랭킹 기준 월") Integer month, - @Schema(description = "(응답모킹됨) 1등 랭커 Dto") + @Schema(description = "1등 랭커 Dto") FamilyMemberRankerResponse firstRanker, - @Schema(description = "(응답모킹됨) 2등 랭커 Dto") + @Schema(description = "2등 랭커 Dto") FamilyMemberRankerResponse secondRanker, - @Schema(description = "(응답모킹됨) 3등 랭커 Dto") + @Schema(description = "3등 랭커 Dto") FamilyMemberRankerResponse thirdRanker, @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) diff --git a/gateway/src/main/java/com/oing/dto/response/FamilyMemberRankerResponse.java b/gateway/src/main/java/com/oing/dto/response/FamilyMemberRankerResponse.java index 69c6d912..51e4a39b 100644 --- a/gateway/src/main/java/com/oing/dto/response/FamilyMemberRankerResponse.java +++ b/gateway/src/main/java/com/oing/dto/response/FamilyMemberRankerResponse.java @@ -4,7 +4,6 @@ @Schema(description = "가족구성원 랭커 응답") public record FamilyMemberRankerResponse( - @Schema(description = "랭커의 프로필 사진 Url", example = "https://no5ing.com/profile/1.jpg") String profileImageUrl, diff --git a/gateway/src/main/java/com/oing/repository/PostRepositoryCustomImpl.java b/gateway/src/main/java/com/oing/repository/PostRepositoryCustomImpl.java index 88fe8f6c..a77ee237 100644 --- a/gateway/src/main/java/com/oing/repository/PostRepositoryCustomImpl.java +++ b/gateway/src/main/java/com/oing/repository/PostRepositoryCustomImpl.java @@ -2,6 +2,7 @@ import com.oing.domain.Post; import com.oing.domain.PostType; +import com.querydsl.core.BooleanBuilder; import com.querydsl.core.QueryResults; import com.querydsl.core.types.Ops; import com.querydsl.core.types.dsl.BooleanExpression; @@ -78,15 +79,15 @@ public long countMonthlyPostByFamilyId(int year, int month, String familyId) { .fetchFirst(); } - private BooleanExpression eqDate(LocalDate date) { - DateTimeTemplate createdAtDate = Expressions.dateTimeTemplate(LocalDate.class, - "DATE({0})", post.createdAt); - - return date == null ? null : createdAtDate.eq(date); - } - - private BooleanExpression eqMemberId(String memberId) { - return memberId == null ? null : post.memberId.eq(memberId); + @Override + public long countMonthlyPostByMemberId(int year, int month, String memberId) { + return queryFactory + .select(post.count()) + .from(post) + .where(post.memberId.eq(memberId), + post.createdAt.year().eq(year), + post.createdAt.month().eq(month)) + .fetchFirst(); } @Override @@ -163,11 +164,32 @@ public int countTodaySurvivalPostsByFamilyId(String familyId) { return count.intValue(); } - private BooleanExpression isActiveMember() { - return member.deletedAt.isNull(); + private BooleanExpression eqDate(LocalDate date) { + DateTimeTemplate createdAtDate = Expressions.dateTimeTemplate(LocalDate.class, + "DATE({0})", post.createdAt); + + return date == null ? null : createdAtDate.eq(date); + } + + private BooleanBuilder eqMonth(LocalDate date) { + return date == null ? null : new BooleanBuilder() + .and(post.createdAt.year().eq(date.getYear())) + .and(post.createdAt.month().eq(date.getMonthValue())); } private DateTimeTemplate dateExpr(DateTimePath localDateTime) { return Expressions.dateTimeTemplate(LocalDate.class, "DATE({0})", localDateTime); } + + private BooleanExpression eqMemberId(String memberId) { + return memberId == null ? null : post.memberId.eq(memberId); + } + + private BooleanExpression isActiveMember() { + return member.deletedAt.isNull(); + } + + private BooleanExpression eqPostType(PostType postType) { + return postType == null ? null : post.type.eq(postType); + } } diff --git a/gateway/src/test/java/com/oing/restapi/MainViewApiTest.java b/gateway/src/test/java/com/oing/restapi/MainViewApiTest.java new file mode 100644 index 00000000..67be7526 --- /dev/null +++ b/gateway/src/test/java/com/oing/restapi/MainViewApiTest.java @@ -0,0 +1,296 @@ +package com.oing.restapi; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oing.domain.*; +import com.oing.repository.CommentRepository; +import com.oing.repository.MemberRepository; +import com.oing.repository.PostRepository; +import com.oing.repository.ReactionRepository; +import com.oing.service.TokenGenerator; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +@AutoConfigureMockMvc +class MainViewApiTest { + + @Autowired + private MockMvc mockMvc; + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private TokenGenerator tokenGenerator; + @Autowired + private ObjectMapper objectMapper; + + private String TEST_MEMBER1_ID = "01HGW2N7EHJVJ4CJ999RRS2E91"; + private Member TEST_MEMBER1; + private String TEST_MEMBER2_ID = "01HGW2N7EHJVJ4CJ999RRS2E99"; + private Member TEST_MEMBER2; + private String TEST_MEMBER3_ID = "99999999999999999999999999"; + private String TEST_POST_ID = "01HGW2N7EHJVJ4CJ999RRS2A97"; + private String TEST_FAMILY_ID = "01HGW2N7EHJVJ4CJ999RRS2E44"; + private String TEST_MEMBER1_TOKEN; + private String TEST_MEMBER2_TOKEN; + + @Autowired + private MemberRepository memberRepository; + @Autowired + private PostRepository postRepository; + @Autowired + private CommentRepository commentRepository; + @Autowired + private ReactionRepository reactionRepository; + + @BeforeEach + void setUp() { + TEST_MEMBER1 = memberRepository.save( + new Member( + TEST_MEMBER1_ID, + TEST_FAMILY_ID, + LocalDate.now(), + "member1", "https://profile.co.kr", "profile.co.kr", + LocalDateTime.now() + ) + ); + TEST_MEMBER1_TOKEN = tokenGenerator + .generateTokenPair(TEST_MEMBER1_ID) + .accessToken(); + TEST_MEMBER2 = memberRepository.save( + new Member( + TEST_MEMBER2_ID, + TEST_FAMILY_ID, + LocalDate.now(), + "member1", "https://profile.co.kr", "profile.co.kr", + LocalDateTime.now() + ) + ); + TEST_MEMBER2_TOKEN = tokenGenerator + .generateTokenPair(TEST_MEMBER2_ID) + .accessToken(); + memberRepository.save( + new Member( + TEST_MEMBER3_ID, + "", + LocalDate.now(), + "member1", "https://profile.co.kr", "profile.co.kr", + LocalDateTime.now() + ) + ); + } + + + @Nested + class 금월의_가족구성원_월간_랭킹_조회 { + @Test + void 정상_조회() throws Exception { + // given + Post post1 = postRepository.save(new Post("1", TEST_MEMBER1_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + postRepository.save(new Post("2", TEST_MEMBER1_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + postRepository.save(new Post("3", TEST_MEMBER2_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + + String date = ZonedDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE); + + // when + ResultActions resultActions = mockMvc.perform( + get("/v1/view/main/family-ranking") + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.month").value(ZonedDateTime.now().getMonthValue())) + .andExpect(jsonPath("$.firstRanker.profileImageUrl").value(TEST_MEMBER1.getProfileImgUrl())) + .andExpect(jsonPath("$.firstRanker.name").value(TEST_MEMBER1.getName())) + .andExpect(jsonPath("$.firstRanker.survivalCount").value(2)) + .andExpect(jsonPath("$.secondRanker.profileImageUrl").value(TEST_MEMBER2.getProfileImgUrl())) + .andExpect(jsonPath("$.secondRanker.name").value(TEST_MEMBER2.getName())) + .andExpect(jsonPath("$.secondRanker.survivalCount").value(1)) + .andExpect(jsonPath("$.thirdRanker").doesNotExist()) + .andExpect(jsonPath("$.mostRecentSurvivalPostDate").value(date)); + } + + @Test + void 업로드된_게시물이_없을때는_null로_반환한다() throws Exception { + // given + + String date = ZonedDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE); + + // when + ResultActions resultActions = mockMvc.perform( + get("/v1/view/main/family-ranking") + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.month").value(ZonedDateTime.now().getMonthValue())) + .andExpect(jsonPath("$.firstRanker").doesNotExist()) + .andExpect(jsonPath("$.secondRanker").doesNotExist()) + .andExpect(jsonPath("$.thirdRanker").doesNotExist()) + .andExpect(jsonPath("$.mostRecentSurvivalPostDate").doesNotExist()); + } + + @Test + void 금월에_업로드된_게시물이_없을때는_null로_반환한다() throws Exception { + // given + jdbcTemplate.update("insert into post (post_id, member_id, family_id, mission_id, type, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", + "1", TEST_MEMBER1_ID, TEST_FAMILY_ID, 1, "SURVIVAL", "https://storage.com/images/1", 0, 0, "2023-11-01 14:00:00", "2023-11-02 14:00:00", "post1111", "1"); + jdbcTemplate.update("insert into post (post_id, member_id, family_id, mission_id, type, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", + "2", TEST_MEMBER2_ID, TEST_FAMILY_ID, 2, "SURVIVAL", "https://storage.com/images/2", 0, 0, "2023-11-01 14:00:00", "2023-11-02 14:00:00", "post2222", "2"); + + // when + ResultActions resultActions = mockMvc.perform( + get("/v1/view/main/family-ranking") + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.month").value(ZonedDateTime.now().getMonthValue())) + .andExpect(jsonPath("$.firstRanker").doesNotExist()) + .andExpect(jsonPath("$.secondRanker").doesNotExist()) + .andExpect(jsonPath("$.thirdRanker").doesNotExist()); +// .andExpect(jsonPath("$.mostRecentSurvivalPostDate").doesNotExist()); + } + + @Test + void 게시글의_수가_같으면_댓글의_수를_비교한다() throws Exception { + // given + Post post1 = postRepository.save(new Post("1", TEST_MEMBER1_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + postRepository.save(new Post("2", TEST_MEMBER2_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + + commentRepository.save(new Comment("1", post1, TEST_MEMBER2_ID, "content")); + + reactionRepository.save(new Reaction("1", post1, TEST_MEMBER1_ID, Emoji.EMOJI_1)); + reactionRepository.save(new Reaction("2", post1, TEST_MEMBER1_ID, Emoji.EMOJI_1)); + reactionRepository.save(new Reaction("3", post1, TEST_MEMBER1_ID, Emoji.EMOJI_1)); + + String date = ZonedDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE); + + // when + ResultActions resultActions = mockMvc.perform( + get("/v1/view/main/family-ranking") + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.month").value(ZonedDateTime.now().getMonthValue())) + .andExpect(jsonPath("$.firstRanker.profileImageUrl").value(TEST_MEMBER2.getProfileImgUrl())) + .andExpect(jsonPath("$.firstRanker.name").value(TEST_MEMBER2.getName())) + .andExpect(jsonPath("$.firstRanker.survivalCount").value(1)) + .andExpect(jsonPath("$.secondRanker.profileImageUrl").value(TEST_MEMBER1.getProfileImgUrl())) + .andExpect(jsonPath("$.secondRanker.name").value(TEST_MEMBER1.getName())) + .andExpect(jsonPath("$.secondRanker.survivalCount").value(1)) + .andExpect(jsonPath("$.thirdRanker").doesNotExist()) + .andExpect(jsonPath("$.mostRecentSurvivalPostDate").value(date)); + } + + @Test + void 게시물과_댓글의_수가_같으면_리액션의_수를_비교한다() throws Exception { + // given + Post post1 = postRepository.save(new Post("1", TEST_MEMBER1_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + postRepository.save(new Post("2", TEST_MEMBER2_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + + commentRepository.save(new Comment("1", post1, TEST_MEMBER1_ID, "content")); + commentRepository.save(new Comment("2", post1, TEST_MEMBER2_ID, "content")); + + reactionRepository.save(new Reaction("1", post1, TEST_MEMBER1_ID, Emoji.EMOJI_1)); + reactionRepository.save(new Reaction("2", post1, TEST_MEMBER2_ID, Emoji.EMOJI_1)); + reactionRepository.save(new Reaction("3", post1, TEST_MEMBER2_ID, Emoji.EMOJI_1)); + + String date = ZonedDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE); + + // when + ResultActions resultActions = mockMvc.perform( + get("/v1/view/main/family-ranking") + .param("type", "SURVIVAL") + .param("scope", "FAMILY") + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.month").value(ZonedDateTime.now().getMonthValue())) + .andExpect(jsonPath("$.firstRanker.profileImageUrl").value(TEST_MEMBER2.getProfileImgUrl())) + .andExpect(jsonPath("$.firstRanker.name").value(TEST_MEMBER2.getName())) + .andExpect(jsonPath("$.firstRanker.survivalCount").value(1)) + .andExpect(jsonPath("$.secondRanker.profileImageUrl").value(TEST_MEMBER1.getProfileImgUrl())) + .andExpect(jsonPath("$.secondRanker.name").value(TEST_MEMBER1.getName())) + .andExpect(jsonPath("$.secondRanker.survivalCount").value(1)) + .andExpect(jsonPath("$.thirdRanker").doesNotExist()) + .andExpect(jsonPath("$.mostRecentSurvivalPostDate").value(date)); + } + + @Test + void 게시물과_댓글과_리액션의_수가_같으면_아이디를_비교한다() throws Exception { + // given + Post post1 = postRepository.save(new Post("1", TEST_MEMBER1_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + postRepository.save(new Post("2", TEST_MEMBER2_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + + commentRepository.save(new Comment("1", post1, TEST_MEMBER1_ID, "content")); + commentRepository.save(new Comment("2", post1, TEST_MEMBER2_ID, "content")); + + reactionRepository.save(new Reaction("1", post1, TEST_MEMBER1_ID, Emoji.EMOJI_1)); + reactionRepository.save(new Reaction("2", post1, TEST_MEMBER2_ID, Emoji.EMOJI_1)); + + String date = ZonedDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE); + + // when + ResultActions resultActions = mockMvc.perform( + get("/v1/view/main/family-ranking") + .param("type", "SURVIVAL") + .param("scope", "FAMILY") + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.month").value(ZonedDateTime.now().getMonthValue())) + .andExpect(jsonPath("$.firstRanker.profileImageUrl").value(TEST_MEMBER1.getProfileImgUrl())) + .andExpect(jsonPath("$.firstRanker.name").value(TEST_MEMBER1.getName())) + .andExpect(jsonPath("$.firstRanker.survivalCount").value(1)) + .andExpect(jsonPath("$.secondRanker.profileImageUrl").value(TEST_MEMBER2.getProfileImgUrl())) + .andExpect(jsonPath("$.secondRanker.name").value(TEST_MEMBER2.getName())) + .andExpect(jsonPath("$.secondRanker.survivalCount").value(1)) + .andExpect(jsonPath("$.thirdRanker").doesNotExist()) + .andExpect(jsonPath("$.mostRecentSurvivalPostDate").value(date)); + } + } +} diff --git a/gateway/src/test/java/com/oing/restapi/PostApiTest.java b/gateway/src/test/java/com/oing/restapi/PostApiTest.java index 1681d373..5f079b8e 100644 --- a/gateway/src/test/java/com/oing/restapi/PostApiTest.java +++ b/gateway/src/test/java/com/oing/restapi/PostApiTest.java @@ -1,21 +1,23 @@ package com.oing.restapi; import com.fasterxml.jackson.databind.ObjectMapper; -import com.oing.domain.Member; -import com.oing.domain.Post; -import com.oing.domain.PostType; +import com.oing.domain.*; import com.oing.dto.request.CreatePostRequest; import com.oing.dto.request.PreSignedUrlRequest; +import com.oing.repository.CommentRepository; import com.oing.repository.PostRepository; import com.oing.repository.MemberRepository; +import com.oing.repository.ReactionRepository; import com.oing.service.TokenGenerator; import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; @@ -35,14 +37,17 @@ class PostApiTest { @Autowired private MockMvc mockMvc; + @Autowired + private JdbcTemplate jdbcTemplate; @Autowired private TokenGenerator tokenGenerator; @Autowired private ObjectMapper objectMapper; - private String TEST_MEMBER1_ID = "01HGW2N7EHJVJ4CJ999RRS2E97"; - private String TEST_MEMBER2_ID = "01HGW2N7EHJVJ4CJ99IIFIFE94"; + private String TEST_MEMBER1_ID = "01HGW2N7EHJVJ4CJ999RRS2E91"; + private String TEST_MEMBER2_ID = "01HGW2N7EHJVJ4CJ999RRS2E99"; + private String TEST_MEMBER3_ID = "99999999999999999999999999"; private String TEST_POST_ID = "01HGW2N7EHJVJ4CJ999RRS2A97"; private String TEST_FAMILY_ID = "01HGW2N7EHJVJ4CJ999RRS2E44"; private String TEST_MEMBER1_TOKEN; @@ -52,6 +57,10 @@ class PostApiTest { private MemberRepository memberRepository; @Autowired private PostRepository postRepository; + @Autowired + private CommentRepository commentRepository; + @Autowired + private ReactionRepository reactionRepository; @BeforeEach void setUp() { @@ -79,6 +88,15 @@ void setUp() { TEST_MEMBER2_TOKEN = tokenGenerator .generateTokenPair(TEST_MEMBER2_ID) .accessToken(); + memberRepository.save( + new Member( + TEST_MEMBER3_ID, + "", + LocalDate.now(), + "", "", "", + LocalDateTime.now() + ) + ); } @Test @@ -241,4 +259,169 @@ void setUp() { .andExpect(status().isOk()) .andExpect(jsonPath("$.isMissionUnlocked").value(true)); } + + @Nested + class 가족구성원들의_생존신고_랭킹_조회 { + @Test + void 정상_조회() throws Exception { + // given + Post post1 = postRepository.save(new Post("1", TEST_MEMBER1_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + postRepository.save(new Post("2", TEST_MEMBER1_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + postRepository.save(new Post("3", TEST_MEMBER2_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + + // when + ResultActions resultActions = mockMvc.perform( + get("/v1/posts/ranking") + .param("type", "SURVIVAL") + .param("scope", "FAMILY") + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.results[0].memberId").value(TEST_MEMBER1_ID)) + .andExpect(jsonPath("$.results[0].postCount").value(2)) + .andExpect(jsonPath("$.results[1].memberId").value(TEST_MEMBER2_ID)) + .andExpect(jsonPath("$.results[1].postCount").value(1)) + .andExpect(jsonPath("$.results[2]").doesNotExist()); + } + + @Test + void 업로드된_게시물이_없을때는_빈_결과값을_반환한다() throws Exception { + // given + + // when + ResultActions resultActions = mockMvc.perform( + get("/v1/posts/ranking") + .param("type", "SURVIVAL") + .param("scope", "FAMILY") + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.results").isEmpty()); + } + + @Test + void 금월에_업로드된_게시물이_없을때는_빈_결과값을_반환한다() throws Exception { + // given + jdbcTemplate.update("insert into post (post_id, member_id, family_id, mission_id, type, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", + "1", TEST_MEMBER1_ID, TEST_FAMILY_ID, 1, "SURVIVAL", "https://storage.com/images/1", 0, 0, "2023-11-01 14:00:00", "2023-11-02 14:00:00", "post1111", "1"); + jdbcTemplate.update("insert into post (post_id, member_id, family_id, mission_id, type, post_img_url, comment_cnt, reaction_cnt, created_at, updated_at, content, post_img_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", + "2", TEST_MEMBER2_ID, TEST_FAMILY_ID, 2, "SURVIVAL", "https://storage.com/images/2", 0, 0, "2023-11-01 14:00:00", "2023-11-02 14:00:00", "post2222", "2"); + + // when + ResultActions resultActions = mockMvc.perform( + get("/v1/posts/ranking") + .param("type", "SURVIVAL") + .param("scope", "FAMILY") + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.results").isEmpty()); + } + + @Test + void 게시글의_수가_같으면_댓글의_수를_비교한다() throws Exception { + // given + Post post1 = postRepository.save(new Post("1", TEST_MEMBER1_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + postRepository.save(new Post("2", TEST_MEMBER2_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + + commentRepository.save(new Comment("1", post1, TEST_MEMBER2_ID, "content")); + + reactionRepository.save(new Reaction("1", post1, TEST_MEMBER1_ID, Emoji.EMOJI_1)); + reactionRepository.save(new Reaction("2", post1, TEST_MEMBER1_ID, Emoji.EMOJI_1)); + reactionRepository.save(new Reaction("3", post1, TEST_MEMBER1_ID, Emoji.EMOJI_1)); + + // when + ResultActions resultActions = mockMvc.perform( + get("/v1/posts/ranking") + .param("type", "SURVIVAL") + .param("scope", "FAMILY") + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.results[0].memberId").value(TEST_MEMBER2_ID)) + .andExpect(jsonPath("$.results[0].postCount").value(1)) + .andExpect(jsonPath("$.results[1].memberId").value(TEST_MEMBER1_ID)) + .andExpect(jsonPath("$.results[1].postCount").value(1)) + .andExpect(jsonPath("$.results[2]").doesNotExist()); + } + + @Test + void 게시물과_댓글의_수가_같으면_리액션의_수를_비교한다() throws Exception { + // given + Post post1 = postRepository.save(new Post("1", TEST_MEMBER1_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + postRepository.save(new Post("2", TEST_MEMBER2_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + + commentRepository.save(new Comment("1", post1, TEST_MEMBER1_ID, "content")); + commentRepository.save(new Comment("2", post1, TEST_MEMBER2_ID, "content")); + + reactionRepository.save(new Reaction("1", post1, TEST_MEMBER1_ID, Emoji.EMOJI_1)); + reactionRepository.save(new Reaction("2", post1, TEST_MEMBER2_ID, Emoji.EMOJI_1)); + reactionRepository.save(new Reaction("3", post1, TEST_MEMBER2_ID, Emoji.EMOJI_1)); + + // when + ResultActions resultActions = mockMvc.perform( + get("/v1/posts/ranking") + .param("type", "SURVIVAL") + .param("scope", "FAMILY") + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.results[0].memberId").value(TEST_MEMBER2_ID)) + .andExpect(jsonPath("$.results[0].postCount").value(1)) + .andExpect(jsonPath("$.results[1].memberId").value(TEST_MEMBER1_ID)) + .andExpect(jsonPath("$.results[1].postCount").value(1)) + .andExpect(jsonPath("$.results[2]").doesNotExist()); + } + + @Test + void 게시물과_댓글과_리액션의_수가_같으면_아이디를_비교한다() throws Exception { + // given + Post post1 = postRepository.save(new Post("1", TEST_MEMBER1_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + postRepository.save(new Post("2", TEST_MEMBER2_ID, TEST_FAMILY_ID, PostType.SURVIVAL, "img", "img", "content")); + + commentRepository.save(new Comment("1", post1, TEST_MEMBER1_ID, "content")); + commentRepository.save(new Comment("2", post1, TEST_MEMBER2_ID, "content")); + + reactionRepository.save(new Reaction("1", post1, TEST_MEMBER1_ID, Emoji.EMOJI_1)); + reactionRepository.save(new Reaction("2", post1, TEST_MEMBER2_ID, Emoji.EMOJI_1)); + + // when + ResultActions resultActions = mockMvc.perform( + get("/v1/posts/ranking") + .param("type", "SURVIVAL") + .param("scope", "FAMILY") + .header("X-AUTH-TOKEN", TEST_MEMBER1_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.results[0].memberId").value(TEST_MEMBER1_ID)) + .andExpect(jsonPath("$.results[0].postCount").value(1)) + .andExpect(jsonPath("$.results[1].memberId").value(TEST_MEMBER2_ID)) + .andExpect(jsonPath("$.results[1].postCount").value(1)) + .andExpect(jsonPath("$.results[2]").doesNotExist()); + } + } } diff --git a/post/src/main/java/com/oing/controller/PostController.java b/post/src/main/java/com/oing/controller/PostController.java index bbba214f..fba17a52 100644 --- a/post/src/main/java/com/oing/controller/PostController.java +++ b/post/src/main/java/com/oing/controller/PostController.java @@ -4,6 +4,7 @@ import com.oing.domain.PaginationDTO; import com.oing.domain.Post; import com.oing.domain.PostType; +import com.oing.dto.dto.PostRankerDTO; import com.oing.dto.request.CreatePostRequest; import com.oing.dto.request.PreSignedUrlRequest; import com.oing.dto.response.*; @@ -11,14 +12,17 @@ import com.oing.exception.MissionPostAccessDeniedFamilyException; import com.oing.exception.MissionPostCreateAccessDeniedMemberException; import com.oing.restapi.PostApi; +import com.oing.service.CommentService; import com.oing.service.MemberBridge; import com.oing.service.PostService; +import com.oing.service.ReactionService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import java.time.LocalDate; import java.time.ZonedDateTime; +import java.util.List; /** * no5ing-server @@ -32,6 +36,8 @@ public class PostController implements PostApi { private final PostService postService; + private final CommentService commentService; + private final ReactionService reactionService; private final MemberBridge memberBridge; @Override @@ -130,6 +136,29 @@ public RemainingSurvivalPostCountResponse getRemainingSurvivalPostCount(String m return new RemainingSurvivalPostCountResponse(remainingSurvivalPostCount); } + @Override + public ArrayResponse getFamilyMembersMonthlySurvivalRanking(String loginFamilyId) { + List familyMembersIds = memberBridge.getFamilyMembersIdsByFamilyId(loginFamilyId); + LocalDate dateTime = ZonedDateTime.now().toLocalDate(); + + List postRankerResponses = familyMembersIds.stream().map(familyMemberId -> new PostRankerDTO( + familyMemberId, + postService.countMonthlyPostByMemberId(dateTime, familyMemberId), + commentService.countMonthlyCommentByMemberId(dateTime, familyMemberId), + reactionService.countMonthlyReactionByMemberId(dateTime, familyMemberId) + + )) + .filter(postRankerDTO -> postRankerDTO.getPostCount() > 0) // 게시글이 없는 경우 제외 + .sorted() // 내부 정책에 따라 재정의한 DTO compareTo 메서드를 통해 정렬 + .map(postRankerDTO -> new PostRankerResponse( + postRankerDTO.getMemberId(), + postRankerDTO.getPostCount().intValue()) + ) + .toList(); + + return ArrayResponse.of(postRankerResponses); + } + private void validateFamilyMember(String loginMemberId, String postId) { String postFamilyId = postService.getMemberPostById(postId).getFamilyId(); String loginFamilyId = memberBridge.getFamilyIdByMemberId(loginMemberId); diff --git a/post/src/main/java/com/oing/dto/dto/PostRankerDTO.java b/post/src/main/java/com/oing/dto/dto/PostRankerDTO.java new file mode 100644 index 00000000..8180d55a --- /dev/null +++ b/post/src/main/java/com/oing/dto/dto/PostRankerDTO.java @@ -0,0 +1,44 @@ +package com.oing.dto.dto; + + +import lombok.Getter; + +@Getter +public class PostRankerDTO implements Comparable { + + String memberId; + Long postCount; + Long commentCount; + Long reactionCount; + + public PostRankerDTO(String memberId, Long postCount, Long commentCount, Long reactionCount) { + this.memberId = memberId; + this.postCount = postCount; + this.commentCount = commentCount; + this.reactionCount = reactionCount; + } + + @Override + public int compareTo(PostRankerDTO o) { + // 1. postCount 내림차순 + int postCountCompare = Long.compare(this.postCount, o.postCount); + if (postCountCompare != 0) { + return -1 * postCountCompare; + } + + // 2. commentCount 내림차순 + int commentCountCompare = Long.compare(this.commentCount, o.commentCount); + if (commentCountCompare != 0) { + return -1 * commentCountCompare; + } + + // 3. reactionCount 내림차순 + int reactionCountCompare = Long.compare(this.reactionCount, o.reactionCount); + if (reactionCountCompare != 0) { + return -1 * reactionCountCompare; + } + + // 4. memberId (가입날짜) 오름차순 + return this.memberId.compareTo(o.memberId); + } +} diff --git a/post/src/main/java/com/oing/dto/response/PostRankerResponse.java b/post/src/main/java/com/oing/dto/response/PostRankerResponse.java new file mode 100644 index 00000000..9859041c --- /dev/null +++ b/post/src/main/java/com/oing/dto/response/PostRankerResponse.java @@ -0,0 +1,14 @@ +package com.oing.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "게시글 기반 랭커 응답") +public record PostRankerResponse( + + @Schema(description = "멤버 아이디", example = "01HGW2N7EHJVJ4CJ999RRS2E97") + String memberId, + + @Schema(description = "게시글 갯수") + Integer postCount +) { +} diff --git a/post/src/main/java/com/oing/repository/CommentRepositoryCustom.java b/post/src/main/java/com/oing/repository/CommentRepositoryCustom.java index e855e4c4..a5c6ee6e 100644 --- a/post/src/main/java/com/oing/repository/CommentRepositoryCustom.java +++ b/post/src/main/java/com/oing/repository/CommentRepositoryCustom.java @@ -5,4 +5,6 @@ public interface CommentRepositoryCustom { QueryResults searchPostComments(int page, int size, String postId, boolean asc); + + long countMonthlyCommentByMemberId(int year, int month, String memberId); } diff --git a/post/src/main/java/com/oing/repository/CommentRepositoryCustomImpl.java b/post/src/main/java/com/oing/repository/CommentRepositoryCustomImpl.java index 6d314032..9db98171 100644 --- a/post/src/main/java/com/oing/repository/CommentRepositoryCustomImpl.java +++ b/post/src/main/java/com/oing/repository/CommentRepositoryCustomImpl.java @@ -25,4 +25,15 @@ public QueryResults searchPostComments(int page, int size, String postI .limit(size) .fetchResults(); } + + @Override + public long countMonthlyCommentByMemberId(int year, int month, String memberId) { + return queryFactory + .select(comment.count()) + .from(comment) + .where(comment.memberId.eq(memberId), + comment.createdAt.year().eq(year), + comment.createdAt.month().eq(month)) + .fetchFirst(); + } } diff --git a/post/src/main/java/com/oing/repository/PostRepositoryCustom.java b/post/src/main/java/com/oing/repository/PostRepositoryCustom.java index 85de5c0a..dca15e78 100644 --- a/post/src/main/java/com/oing/repository/PostRepositoryCustom.java +++ b/post/src/main/java/com/oing/repository/PostRepositoryCustom.java @@ -18,6 +18,8 @@ QueryResults searchPosts(int page, int size, LocalDate date, String member long countMonthlyPostByFamilyId(int year, int month, String familyId); + long countMonthlyPostByMemberId(int year, int month, String memberId); + boolean existsByFamilyIdAndCreatedAt(String familyId, LocalDate postDate); boolean existsByMemberIdAndFamilyIdAndTypeAndCreatedAt(String memberId, String familyId, PostType type, LocalDate postDate); diff --git a/post/src/main/java/com/oing/repository/ReactionRepository.java b/post/src/main/java/com/oing/repository/ReactionRepository.java index ec937d6e..596cf7c4 100644 --- a/post/src/main/java/com/oing/repository/ReactionRepository.java +++ b/post/src/main/java/com/oing/repository/ReactionRepository.java @@ -10,7 +10,7 @@ import java.util.List; import java.util.Optional; -public interface ReactionRepository extends JpaRepository { +public interface ReactionRepository extends JpaRepository, ReactionRepositoryCustom { @Lock(LockModeType.PESSIMISTIC_WRITE) boolean existsByPostAndMemberIdAndEmoji(Post post, String memberId, Emoji emoji); diff --git a/post/src/main/java/com/oing/repository/ReactionRepositoryCustom.java b/post/src/main/java/com/oing/repository/ReactionRepositoryCustom.java new file mode 100644 index 00000000..9d542a60 --- /dev/null +++ b/post/src/main/java/com/oing/repository/ReactionRepositoryCustom.java @@ -0,0 +1,6 @@ +package com.oing.repository; + +public interface ReactionRepositoryCustom { + + long countMonthlyReactionByMemberId(int year, int month, String memberId); +} diff --git a/post/src/main/java/com/oing/repository/ReactionRepositoryCustomImpl.java b/post/src/main/java/com/oing/repository/ReactionRepositoryCustomImpl.java new file mode 100644 index 00000000..90317a4a --- /dev/null +++ b/post/src/main/java/com/oing/repository/ReactionRepositoryCustomImpl.java @@ -0,0 +1,24 @@ +package com.oing.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import static com.oing.domain.QReaction.reaction; + +@RequiredArgsConstructor +@Repository +public class ReactionRepositoryCustomImpl implements ReactionRepositoryCustom { + private final JPAQueryFactory queryFactory; + + @Override + public long countMonthlyReactionByMemberId(int year, int month, String memberId) { + return queryFactory + .select(reaction.count()) + .from(reaction) + .where(reaction.memberId.eq(memberId), + reaction.createdAt.year().eq(year), + reaction.createdAt.month().eq(month)) + .fetchFirst(); + } +} diff --git a/post/src/main/java/com/oing/restapi/PostApi.java b/post/src/main/java/com/oing/restapi/PostApi.java index 907ab4b2..7e6d2cd5 100644 --- a/post/src/main/java/com/oing/restapi/PostApi.java +++ b/post/src/main/java/com/oing/restapi/PostApi.java @@ -8,6 +8,7 @@ import com.oing.util.security.LoginMemberId; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.Min; @@ -177,4 +178,14 @@ RemainingSurvivalPostCountResponse getRemainingSurvivalPostCount( @LoginFamilyId String loginFamilyId ); + + @Operation(summary = "가족구성원들의 생존신고 랭킹 조회", description = "가족구성원들의 생존신고 랭킹을 조회합니다.") + @Parameter(required = true, name = "type", description = "게시물 타입", example = "[SURVIVAL]") + @Parameter(required = true, name = "scope", description = "랭킹 범위", example = "[FAMILY]") + @GetMapping(value = "/ranking", params = {"type=SURVIVAL", "scope=FAMILY"}) + ArrayResponse getFamilyMembersMonthlySurvivalRanking( + @Parameter(hidden = true) + @LoginFamilyId + String loginFamilyId + ); } diff --git a/post/src/main/java/com/oing/service/CommentService.java b/post/src/main/java/com/oing/service/CommentService.java index 9528a229..62a6e7bf 100644 --- a/post/src/main/java/com/oing/service/CommentService.java +++ b/post/src/main/java/com/oing/service/CommentService.java @@ -16,6 +16,8 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import java.time.LocalDate; + @Slf4j @RequiredArgsConstructor @Service @@ -91,6 +93,13 @@ private void validateCommentOwner(String memberId, Comment comment) { } } + public long countMonthlyCommentByMemberId(LocalDate date, String memberId) { + int year = date.getYear(); + int month = date.getMonthValue(); + + return commentRepository.countMonthlyCommentByMemberId(year, month, memberId); + } + @EventListener public void deleteAllWhenPostDelete(DeletePostEvent event) { Post post = event.post(); diff --git a/post/src/main/java/com/oing/service/PostService.java b/post/src/main/java/com/oing/service/PostService.java index 2cded876..d6a7c5d3 100644 --- a/post/src/main/java/com/oing/service/PostService.java +++ b/post/src/main/java/com/oing/service/PostService.java @@ -172,4 +172,11 @@ public int calculateRemainingSurvivalPostCountUntilMissionUnlocked(String family return Math.max(requiredSurvivalPostCount - todaySurvivalPostCount, 0); } + + public long countMonthlyPostByMemberId(LocalDate date, String memberId) { + int year = date.getYear(); + int month = date.getMonthValue(); + + return postRepository.countMonthlyPostByMemberId(year, month, memberId); + } } diff --git a/post/src/main/java/com/oing/service/ReactionService.java b/post/src/main/java/com/oing/service/ReactionService.java index 2f00711c..641e87ff 100644 --- a/post/src/main/java/com/oing/service/ReactionService.java +++ b/post/src/main/java/com/oing/service/ReactionService.java @@ -15,6 +15,7 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import java.time.LocalDate; import java.util.List; @Slf4j @@ -74,6 +75,13 @@ public boolean isMemberPostReactionExists(Post post, String memberId, Emoji emoj return reactionRepository.existsByPostAndMemberIdAndEmoji(post, memberId, emoji); } + public long countMonthlyReactionByMemberId(LocalDate date, String memberId) { + int year = date.getYear(); + int month = date.getMonthValue(); + + return reactionRepository.countMonthlyReactionByMemberId(year, month, memberId); + } + @EventListener public void deleteAllWhenPostDelete(DeletePostEvent event) { Post post = event.post();