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 a6c7d063..b629bc48 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.formatDateTimeForNotice(notice.getCreatedAt())) + .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 99643e07..ca423653 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.formatDateTimeForNotice(notice.getCreatedAt())) + .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/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("해당 풀이를 확인 할 권한이 없습니다.")); diff --git a/src/test/java/com/gamzabat/algohub/service/NoticeServiceTest.java b/src/test/java/com/gamzabat/algohub/service/NoticeServiceTest.java index b4096f23..fc57ebdf 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.formatDateTimeForNotice(notice.getCreatedAt())); 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(); } }