From f9ce1388eb1f9cd646fd82935d124e49bc37a18a Mon Sep 17 00:00:00 2001 From: Kim EunSu <88280787+rladmstn@users.noreply.github.com> Date: Sun, 10 Nov 2024 18:59:39 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat=20:=20=EB=82=98=EC=9D=98=20=ED=92=80?= =?UTF-8?q?=EC=9D=B4=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#156)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat : 나의 풀이 목록 조회 API 추가 * feat : 엔드포인트 복수형 통일 * test : SolutionControllerTest 엔드포인트 수정 --- .../controller/SolutionController.java | 21 +++++++++++++++---- .../solution/service/SolutionService.java | 2 -- .../controller/SolutionControllerTest.java | 16 +++++++------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/gamzabat/algohub/feature/solution/controller/SolutionController.java b/src/main/java/com/gamzabat/algohub/feature/solution/controller/SolutionController.java index 4423e565..9041a4da 100644 --- a/src/main/java/com/gamzabat/algohub/feature/solution/controller/SolutionController.java +++ b/src/main/java/com/gamzabat/algohub/feature/solution/controller/SolutionController.java @@ -27,12 +27,12 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/solution") +@RequestMapping("/api") @Tag(name = "풀이 API", description = "문제 풀이 관련 API") public class SolutionController { private final SolutionService solutionService; - @GetMapping + @GetMapping("/solutions") @Operation(summary = "풀이 목록 조회 API", description = "특정 문제에 대한 풀이를 모두 조회하는 API") public ResponseEntity> getSolutionList(@AuthedUser User user, @RequestParam Long problemId, @@ -47,7 +47,7 @@ public ResponseEntity> getSolutionList(@AuthedUser Use return ResponseEntity.ok().body(response); } - @GetMapping("/{solutionId}") + @GetMapping("/solutions/{solutionId}") @Operation(summary = "풀이 하나 조회 API", description = "특정 풀이 하나를 조회하는 API") public ResponseEntity getSolution(@AuthedUser User user, @PathVariable Long solutionId) { @@ -55,7 +55,7 @@ public ResponseEntity getSolution(@AuthedUser User user, return ResponseEntity.ok().body(response); } - @PostMapping + @PostMapping("/solutions") @Operation(summary = "풀이 생성 API") public ResponseEntity createSolution(@Valid @RequestBody CreateSolutionRequest request, Errors errors) { if (errors.hasErrors()) @@ -64,4 +64,17 @@ public ResponseEntity createSolution(@Valid @RequestBody CreateSolutionReq return ResponseEntity.ok().build(); } + @GetMapping("/my-solutions") + @Operation(summary = "나의 풀이 목록 조회 API", description = "나의 풀이를 모두 조회하는 API") + public ResponseEntity> getSolutionList(@AuthedUser User user, + @RequestParam Long problemId, + @RequestParam(required = false) String language, + @RequestParam(required = false) String result, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + Pageable pageable = PageRequest.of(page, size); + Page response = solutionService.getSolutionList(user, problemId, user.getNickname(), + language, result, pageable); + return ResponseEntity.ok().body(response); + } } diff --git a/src/main/java/com/gamzabat/algohub/feature/solution/service/SolutionService.java b/src/main/java/com/gamzabat/algohub/feature/solution/service/SolutionService.java index 5acda9d7..8f5164e9 100644 --- a/src/main/java/com/gamzabat/algohub/feature/solution/service/SolutionService.java +++ b/src/main/java/com/gamzabat/algohub/feature/solution/service/SolutionService.java @@ -23,7 +23,6 @@ import com.gamzabat.algohub.feature.group.studygroup.repository.GroupMemberRepository; import com.gamzabat.algohub.feature.group.studygroup.repository.StudyGroupRepository; import com.gamzabat.algohub.feature.notification.enums.NotificationCategory; -import com.gamzabat.algohub.feature.notification.repository.NotificationSettingRepository; import com.gamzabat.algohub.feature.notification.service.NotificationService; import com.gamzabat.algohub.feature.problem.domain.Problem; import com.gamzabat.algohub.feature.problem.repository.ProblemRepository; @@ -54,7 +53,6 @@ public class SolutionService { private final SolutionCommentRepository commentRepository; private final RankingService rankingService; private final RankingUpdateService rankingUpdateService; - private final NotificationSettingRepository notificationSettingRepository; public Page getSolutionList(User user, Long problemId, String nickname, String language, String result, Pageable pageable) { diff --git a/src/test/java/com/gamzabat/algohub/feature/solution/controller/SolutionControllerTest.java b/src/test/java/com/gamzabat/algohub/feature/solution/controller/SolutionControllerTest.java index 96ad0f3a..a770c585 100644 --- a/src/test/java/com/gamzabat/algohub/feature/solution/controller/SolutionControllerTest.java +++ b/src/test/java/com/gamzabat/algohub/feature/solution/controller/SolutionControllerTest.java @@ -90,7 +90,7 @@ void getSolutionList_1() throws Exception { when(solutionService.getSolutionList(any(User.class), anyLong(), isNull(), isNull(), isNull(), any(Pageable.class))).thenReturn(pagedResponse); // when, then - mockMvc.perform(get("/api/solution") + mockMvc.perform(get("/api/solutions") .header("Authorization", token) .param("problemId", String.valueOf(problemId))) .andExpect(status().isOk()) @@ -107,7 +107,7 @@ void getSolutionListFailed_1() throws Exception { any(Pageable.class))) .thenThrow(new ProblemValidationException(HttpStatus.NOT_FOUND.value(), "존재하지 않는 문제 입니다.")); // when, then - mockMvc.perform(get("/api/solution") + mockMvc.perform(get("/api/solutions") .header("Authorization", token) .param("problemId", String.valueOf(problemId))) .andExpect(status().isNotFound()) @@ -124,7 +124,7 @@ void getSolutionListFailed_2() throws Exception { any(Pageable.class))) .thenThrow(new StudyGroupValidationException(HttpStatus.NOT_FOUND.value(), "존재하지 않는 그룹 입니다.")); // when, then - mockMvc.perform(get("/api/solution") + mockMvc.perform(get("/api/solutions") .header("Authorization", token) .param("problemId", String.valueOf(problemId))) .andExpect(status().isNotFound()) @@ -141,7 +141,7 @@ void getSolutionListFailed_3() throws Exception { any(Pageable.class))) .thenThrow(new GroupMemberValidationException(HttpStatus.FORBIDDEN.value(), "참여하지 않은 그룹 입니다.")); // when, then - mockMvc.perform(get("/api/solution") + mockMvc.perform(get("/api/solutions") .header("Authorization", token) .param("problemId", String.valueOf(problemId))) .andExpect(status().isForbidden()) @@ -156,7 +156,7 @@ void getSolution_1() throws Exception { GetSolutionResponse response = GetSolutionResponse.builder().build(); when(solutionService.getSolution(any(User.class), anyLong())).thenReturn(response); // when, then - mockMvc.perform(get("/api/solution/{solutionId}", solutionId) + mockMvc.perform(get("/api/solutions/{solutionId}", solutionId) .header("Authorization", token)) .andExpect(status().isOk()) .andExpect(content().string(objectMapper.writeValueAsString(response))); @@ -170,7 +170,7 @@ void getSolutionFailed_1() throws Exception { when(solutionService.getSolution(any(User.class), eq(solutionId))) .thenThrow(new CannotFoundSolutionException("존재하지 않는 풀이 입니다.")); // when, then - mockMvc.perform(get("/api/solution/{solutionId}", solutionId) + mockMvc.perform(get("/api/solutions/{solutionId}", solutionId) .header("Authorization", token)) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.error").value("존재하지 않는 풀이 입니다.")); @@ -184,7 +184,7 @@ void getSolutionFailed_2() throws Exception { when(solutionService.getSolution(any(User.class), eq(solutionId))) .thenThrow(new CannotFoundSolutionException("존재하지 않는 풀이 입니다.")); // when, then - mockMvc.perform(get("/api/solution/{solutionId}", solutionId) + mockMvc.perform(get("/api/solutions/{solutionId}", solutionId) .header("Authorization", token)) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.error").value("존재하지 않는 풀이 입니다.")); @@ -198,7 +198,7 @@ void getSolutionFailed_3() throws Exception { when(solutionService.getSolution(any(User.class), eq(solutionId))) .thenThrow(new UserValidationException("해당 풀이를 확인 할 권한이 없습니다.")); // when, then - mockMvc.perform(get("/api/solution/{solutionId}", solutionId) + mockMvc.perform(get("/api/solutions/{solutionId}", solutionId) .header("Authorization", token)) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.error").value("해당 풀이를 확인 할 권한이 없습니다.")); From c7f80430a31545a8beffaaea97fa6aa6f2970713 Mon Sep 17 00:00:00 2001 From: Kim EunSu <88280787+rladmstn@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:27:01 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat=20:=20=EA=B3=B5=EC=A7=80=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=ED=91=9C=EC=8B=9C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20response=20=EC=88=98=EC=A0=95=20(#151)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat : 공지 조회 API에서 읽음 표시 로직 추가 * feat : 공지 목록 조회 시 읽음 여부 필드 추가 * feat : @JoinColumn 누락 추가 * refactor : 공지 읽음 표시 메소드 리네이밍 및 코멘트 반영 --- .../feature/notice/domain/NoticeRead.java | 35 +++++++++++++++++++ .../feature/notice/dto/GetNoticeResponse.java | 7 ++-- .../repository/NoticeReadRepository.java | 11 ++++++ .../feature/notice/service/NoticeService.java | 18 +++++++++- .../algohub/service/NoticeServiceTest.java | 6 ++++ 5 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/gamzabat/algohub/feature/notice/domain/NoticeRead.java create mode 100644 src/main/java/com/gamzabat/algohub/feature/notice/repository/NoticeReadRepository.java diff --git a/src/main/java/com/gamzabat/algohub/feature/notice/domain/NoticeRead.java b/src/main/java/com/gamzabat/algohub/feature/notice/domain/NoticeRead.java new file mode 100644 index 00000000..70ee20f5 --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/feature/notice/domain/NoticeRead.java @@ -0,0 +1,35 @@ +package com.gamzabat.algohub.feature.notice.domain; + +import com.gamzabat.algohub.feature.user.domain.User; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class NoticeRead { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "notice_id") + private Notice notice; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Builder + public NoticeRead(Notice notice, User user) { + this.notice = notice; + this.user = user; + } +} diff --git a/src/main/java/com/gamzabat/algohub/feature/notice/dto/GetNoticeResponse.java b/src/main/java/com/gamzabat/algohub/feature/notice/dto/GetNoticeResponse.java index 6eff899b..e6e0cbd3 100644 --- a/src/main/java/com/gamzabat/algohub/feature/notice/dto/GetNoticeResponse.java +++ b/src/main/java/com/gamzabat/algohub/feature/notice/dto/GetNoticeResponse.java @@ -11,9 +11,10 @@ public record GetNoticeResponse(String author, String content, String title, String category, - String createAt) { + String createAt, + boolean isRead) { - public static GetNoticeResponse toDTO(Notice notice) { + public static GetNoticeResponse toDTO(Notice notice, boolean isRead) { return GetNoticeResponse.builder() .author(notice.getAuthor().getNickname()) .noticeId(notice.getId()) @@ -21,7 +22,7 @@ public static GetNoticeResponse toDTO(Notice notice) { .content(notice.getContent()) .category(notice.getCategory()) .createAt(DateFormatUtil.formatDate(notice.getCreatedAt().toLocalDate())) + .isRead(isRead) .build(); - } } diff --git a/src/main/java/com/gamzabat/algohub/feature/notice/repository/NoticeReadRepository.java b/src/main/java/com/gamzabat/algohub/feature/notice/repository/NoticeReadRepository.java new file mode 100644 index 00000000..7161a670 --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/feature/notice/repository/NoticeReadRepository.java @@ -0,0 +1,11 @@ +package com.gamzabat.algohub.feature.notice.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.gamzabat.algohub.feature.notice.domain.Notice; +import com.gamzabat.algohub.feature.notice.domain.NoticeRead; +import com.gamzabat.algohub.feature.user.domain.User; + +public interface NoticeReadRepository extends JpaRepository { + boolean existsByNoticeAndUser(Notice notice, User user); +} diff --git a/src/main/java/com/gamzabat/algohub/feature/notice/service/NoticeService.java b/src/main/java/com/gamzabat/algohub/feature/notice/service/NoticeService.java index 784e15d3..db7abd25 100644 --- a/src/main/java/com/gamzabat/algohub/feature/notice/service/NoticeService.java +++ b/src/main/java/com/gamzabat/algohub/feature/notice/service/NoticeService.java @@ -18,11 +18,13 @@ import com.gamzabat.algohub.feature.group.studygroup.repository.GroupMemberRepository; import com.gamzabat.algohub.feature.group.studygroup.repository.StudyGroupRepository; import com.gamzabat.algohub.feature.notice.domain.Notice; +import com.gamzabat.algohub.feature.notice.domain.NoticeRead; import com.gamzabat.algohub.feature.notice.dto.CreateNoticeRequest; import com.gamzabat.algohub.feature.notice.dto.GetNoticeResponse; import com.gamzabat.algohub.feature.notice.dto.UpdateNoticeRequest; import com.gamzabat.algohub.feature.notice.exception.NoticeValidationException; import com.gamzabat.algohub.feature.notice.repository.NoticeCommentRepository; +import com.gamzabat.algohub.feature.notice.repository.NoticeReadRepository; import com.gamzabat.algohub.feature.notice.repository.NoticeRepository; import com.gamzabat.algohub.feature.user.domain.User; @@ -38,6 +40,7 @@ public class NoticeService { private final NoticeCommentRepository noticeCommentRepository; private final StudyGroupRepository studyGroupRepository; private final GroupMemberRepository groupMemberRepository; + private final NoticeReadRepository noticeReadRepository; @Transactional public void createNotice(@AuthedUser User user, CreateNoticeRequest request) { @@ -68,6 +71,8 @@ public GetNoticeResponse getNotice(@AuthedUser User user, Long noticeId) { if (!groupMemberRepository.existsByUserAndStudyGroup(user, notice.getStudyGroup())) throw new StudyGroupValidationException(HttpStatus.FORBIDDEN.value(), "참여하지 않은 스터디 그룹 입니다."); + markNoticeAsRead(user, notice); + log.info("success to get notice"); return GetNoticeResponse.builder() .author(notice.getAuthor().getNickname()) @@ -76,6 +81,7 @@ public GetNoticeResponse getNotice(@AuthedUser User user, Long noticeId) { .content(notice.getContent()) .category(notice.getCategory()) .createAt(DateFormatUtil.formatDate(notice.getCreatedAt().toLocalDate())) + .isRead(true) .build(); } @@ -87,7 +93,9 @@ public List getNoticeList(@AuthedUser User user, Long studyGr throw new GroupMemberValidationException(HttpStatus.FORBIDDEN.value(), "참여하지 않은 스터디 그룹입니다"); List list = noticeRepository.findAllByStudyGroup(studyGroup); - List result = list.stream().map(GetNoticeResponse::toDTO).toList(); + List result = list.stream().map( + notice -> GetNoticeResponse.toDTO(notice, noticeReadRepository.existsByNoticeAndUser(notice, user)) + ).toList(); log.info("success to get notice list"); return result; } @@ -123,4 +131,12 @@ private void validateStudyGroupExists(Notice notice) { .orElseThrow(() -> new StudyGroupValidationException(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 스터디 그룹입니다")); } + private void markNoticeAsRead(User user, Notice notice) { + if (!noticeReadRepository.existsByNoticeAndUser(notice, user)) { + noticeReadRepository.save( + NoticeRead.builder().notice(notice).user(user).build() + ); + } + log.info("success to read notice. userId: {}, noticeId: {}", user.getId(), notice.getId()); + } } diff --git a/src/test/java/com/gamzabat/algohub/service/NoticeServiceTest.java b/src/test/java/com/gamzabat/algohub/service/NoticeServiceTest.java index a79eb091..44833bca 100644 --- a/src/test/java/com/gamzabat/algohub/service/NoticeServiceTest.java +++ b/src/test/java/com/gamzabat/algohub/service/NoticeServiceTest.java @@ -31,11 +31,13 @@ import com.gamzabat.algohub.feature.group.studygroup.repository.GroupMemberRepository; import com.gamzabat.algohub.feature.group.studygroup.repository.StudyGroupRepository; import com.gamzabat.algohub.feature.notice.domain.Notice; +import com.gamzabat.algohub.feature.notice.domain.NoticeRead; import com.gamzabat.algohub.feature.notice.dto.CreateNoticeRequest; import com.gamzabat.algohub.feature.notice.dto.GetNoticeResponse; import com.gamzabat.algohub.feature.notice.dto.UpdateNoticeRequest; import com.gamzabat.algohub.feature.notice.exception.NoticeValidationException; import com.gamzabat.algohub.feature.notice.repository.NoticeCommentRepository; +import com.gamzabat.algohub.feature.notice.repository.NoticeReadRepository; import com.gamzabat.algohub.feature.notice.repository.NoticeRepository; import com.gamzabat.algohub.feature.notice.service.NoticeService; import com.gamzabat.algohub.feature.user.domain.User; @@ -52,6 +54,8 @@ public class NoticeServiceTest { GroupMemberRepository groupMemberRepository; @Mock private NoticeCommentRepository noticeCommentRepository; + @Mock + private NoticeReadRepository noticeReadRepository; @Captor private ArgumentCaptor noticeCaptor; @@ -183,6 +187,7 @@ void getNoticeSuccess_1() { assertThat(response.category()).isEqualTo("category"); assertThat(response.createAt()).isEqualTo(DateFormatUtil.formatDate(LocalDateTime.now().toLocalDate())); assertThat(response.noticeId()).isEqualTo(1000L); + verify(noticeReadRepository, times(1)).save(any(NoticeRead.class)); } @Test @@ -247,6 +252,7 @@ void getNoticeListSuccess_1() { assertThat(result.get(i).content()).isEqualTo("content" + i); assertThat(result.get(i).title()).isEqualTo("title" + i); assertThat(result.get(i).category()).isEqualTo("category" + i); + assertThat(result.get(i).isRead()).isFalse(); } }