diff --git a/src/main/java/com/dissonance/itit/common/exception/ErrorCode.java b/src/main/java/com/dissonance/itit/common/exception/ErrorCode.java index c65db57..36dabbb 100644 --- a/src/main/java/com/dissonance/itit/common/exception/ErrorCode.java +++ b/src/main/java/com/dissonance/itit/common/exception/ErrorCode.java @@ -24,6 +24,9 @@ public enum ErrorCode { NON_EXISTENT_INFO_POST_ID(HttpStatus.NOT_FOUND, "해당 id의 공고 게시글이 존재하지 않습니다."), REPORTED_INFO_POST_ID(HttpStatus.NOT_FOUND, "해당 id의 게시글은 신고 처리되었습니다."), + // 409 + ALREADY_REPORTED_POST(HttpStatus.CONFLICT, "이미 신고한 공고입니다."), + // 500 IO_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "파일 입출력 에러"), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 에러"); diff --git a/src/main/java/com/dissonance/itit/controller/InfoPostController.java b/src/main/java/com/dissonance/itit/controller/InfoPostController.java index cd965ae..5a60e2b 100644 --- a/src/main/java/com/dissonance/itit/controller/InfoPostController.java +++ b/src/main/java/com/dissonance/itit/controller/InfoPostController.java @@ -20,6 +20,7 @@ import com.dissonance.itit.dto.response.InfoPostDetailRes; import com.dissonance.itit.dto.response.InfoPostRes; import com.dissonance.itit.service.InfoPostService; +import com.dissonance.itit.service.ReportService; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; @@ -30,6 +31,7 @@ @RequestMapping("/info-posts") public class InfoPostController { private final InfoPostService infoPostService; + private final ReportService reportService; @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) @Operation(summary = "공고 게시글 등록", description = "공고 게시글을 등록합니다.") @@ -47,9 +49,9 @@ public ResponseEntity getInfoPostDetail(@PathVariable Long in } @PatchMapping("/{infoPostId}/reports") - @Operation(summary = "공고 게시글 신고", description = "공고 게시글을 신고 처리합니다. (즉시 반영)") - public ResponseEntity reportedInfoPost(@PathVariable Long infoPostId) { - Long resultId = infoPostService.reportedInfoPost(infoPostId); + @Operation(summary = "공고 게시글 신고", description = "공고 게시글을 신고 처리합니다.") + public ResponseEntity reportedInfoPost(@PathVariable Long infoPostId, @CurrentUser User loginUser) { + Long resultId = reportService.reportedInfoPost(infoPostId, loginUser); return ResponseEntity.ok(resultId + "번 게시글의 신고가 성공적으로 접수되었습니다."); } diff --git a/src/main/java/com/dissonance/itit/domain/entity/Report.java b/src/main/java/com/dissonance/itit/domain/entity/Report.java index 1e92ae9..2b0f677 100644 --- a/src/main/java/com/dissonance/itit/domain/entity/Report.java +++ b/src/main/java/com/dissonance/itit/domain/entity/Report.java @@ -1,8 +1,20 @@ package com.dissonance.itit.domain.entity; -import jakarta.persistence.*; +import jakarta.persistence.Column; +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 jakarta.persistence.Table; import jakarta.validation.constraints.Size; -import lombok.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @Builder @@ -11,20 +23,20 @@ @Entity @Table(name = "report") public class Report { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id") - private String id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - private User user; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "info_post_id") - private InfoPost infoPost; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "info_post_id") + private InfoPost infoPost; - @Size(max = 255) - @Column(name = "content") - private String content; + @Size(max = 255) + @Column(name = "content") + private String content; } \ No newline at end of file diff --git a/src/main/java/com/dissonance/itit/repository/ReportRepository.java b/src/main/java/com/dissonance/itit/repository/ReportRepository.java new file mode 100644 index 0000000..f8f898d --- /dev/null +++ b/src/main/java/com/dissonance/itit/repository/ReportRepository.java @@ -0,0 +1,9 @@ +package com.dissonance.itit.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.dissonance.itit.domain.entity.Report; + +public interface ReportRepository extends JpaRepository { + boolean existsByInfoPostIdAndUserId(Long infoPostId, Long UserId); +} diff --git a/src/main/java/com/dissonance/itit/service/InfoPostService.java b/src/main/java/com/dissonance/itit/service/InfoPostService.java index 362cb8b..bfe6971 100644 --- a/src/main/java/com/dissonance/itit/service/InfoPostService.java +++ b/src/main/java/com/dissonance/itit/service/InfoPostService.java @@ -66,12 +66,10 @@ public InfoPostDetailRes getInfoPostDetailById(Long infoPostId) { return InfoPostDetailRes.of(infoPostInfo, positionInfos); } - @Transactional - public Long reportedInfoPost(Long infoPostId) { - InfoPost infoPost = infoPostRepository.findById(infoPostId) + @Transactional(readOnly = true) + public InfoPost findById(Long infoPostId) { + return infoPostRepository.findById(infoPostId) .orElseThrow(() -> new CustomException(ErrorCode.NON_EXISTENT_INFO_POST_ID)); - infoPost.updateReported(); - return infoPost.getId(); } @Transactional(readOnly = true) diff --git a/src/main/java/com/dissonance/itit/service/ReportService.java b/src/main/java/com/dissonance/itit/service/ReportService.java new file mode 100644 index 0000000..485fcc0 --- /dev/null +++ b/src/main/java/com/dissonance/itit/service/ReportService.java @@ -0,0 +1,38 @@ +package com.dissonance.itit.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dissonance.itit.common.exception.CustomException; +import com.dissonance.itit.common.exception.ErrorCode; +import com.dissonance.itit.domain.entity.InfoPost; +import com.dissonance.itit.domain.entity.Report; +import com.dissonance.itit.domain.entity.User; +import com.dissonance.itit.repository.ReportRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ReportService { + private final ReportRepository reportRepository; + + private final InfoPostService infoPostService; + + @Transactional + public Long reportedInfoPost(Long infoPostId, User user) { + InfoPost infoPost = infoPostService.findById(infoPostId); + + if (reportRepository.existsByInfoPostIdAndUserId(infoPostId, user.getId())) { + throw new CustomException(ErrorCode.ALREADY_REPORTED_POST); + } + + Report report = Report.builder() + .infoPost(infoPost) + .user(user) + .build(); + reportRepository.save(report); + + return infoPost.getId(); + } +} diff --git a/src/test/java/com/dissonance/itit/service/InfoPostServiceTest.java b/src/test/java/com/dissonance/itit/service/InfoPostServiceTest.java index 70dad5f..c36e350 100644 --- a/src/test/java/com/dissonance/itit/service/InfoPostServiceTest.java +++ b/src/test/java/com/dissonance/itit/service/InfoPostServiceTest.java @@ -136,40 +136,6 @@ void getInfoPostDetailById_throwCustomException_givenReportedInfoPostId() { .hasMessage(ErrorCode.REPORTED_INFO_POST_ID.getMessage()); } - @Test - @DisplayName("게시글 신고") - void reportedInfoPost_returnInfoPostId() { - // given - Long infoPostId = 1L; - InfoPostReq infoPostReq = TestFixture.createInfoPostReq(); - User author = TestFixture.createUser(); - Image image = TestFixture.createImage(); - Category category = TestFixture.createCategory(); - InfoPost infoPost = TestFixture.createInfoPost(infoPostReq, author, image, category); - - given(infoPostRepository.findById(infoPostId)).willReturn(Optional.of(infoPost)); - - // when - Long result = infoPostService.reportedInfoPost(infoPostId); - - // then - assertThat(result).isEqualTo(infoPostId); - verify(infoPostRepository).findById(infoPostId); - } - - @Test - @DisplayName("게시글 신고시 존재하지 않는 ID로 조회하여 exception 발생") - void reportedInfoPost_throwCustomException_givenNonExistentId() { - // given - Long infoPostId = 999L; - given(infoPostRepository.findById(infoPostId)).willReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> infoPostService.reportedInfoPost(infoPostId)) - .isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.NON_EXISTENT_INFO_POST_ID.getMessage()); - } - @Test @DisplayName("공고 게시글 목록 page 조회") void getInfoPostsByCategoryId_returnInfoPostResPage() { @@ -195,4 +161,17 @@ void getInfoPostsByCategoryId_returnInfoPostResPage() { () -> assertThat(content.get(2).getRemainingDays()).isEqualTo("D+3") ); } + + @Test + @DisplayName("존재하지 않는 ID로 조회하여 exception 발생") + void findById_throwCustomException_givenNonExistentId() { + // given + Long infoPostId = 999L; + given(infoPostRepository.findById(infoPostId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> infoPostService.findById(infoPostId)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.NON_EXISTENT_INFO_POST_ID.getMessage()); + } } diff --git a/src/test/java/com/dissonance/itit/service/ReportServiceTest.java b/src/test/java/com/dissonance/itit/service/ReportServiceTest.java new file mode 100644 index 0000000..92a9fc2 --- /dev/null +++ b/src/test/java/com/dissonance/itit/service/ReportServiceTest.java @@ -0,0 +1,69 @@ +package com.dissonance.itit.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.dissonance.itit.common.exception.CustomException; +import com.dissonance.itit.common.exception.ErrorCode; +import com.dissonance.itit.domain.entity.Category; +import com.dissonance.itit.domain.entity.Image; +import com.dissonance.itit.domain.entity.InfoPost; +import com.dissonance.itit.domain.entity.User; +import com.dissonance.itit.dto.request.InfoPostReq; +import com.dissonance.itit.fixture.TestFixture; +import com.dissonance.itit.repository.ReportRepository; + +@ExtendWith(MockitoExtension.class) +public class ReportServiceTest { + @InjectMocks + private ReportService reportService; + @Mock + private ReportRepository reportRepository; + @Mock + private InfoPostService infoPostService; + + @Test + @DisplayName("게시글 신고") + void reportedInfoPost_returnInfoPostId() { + // given + Long infoPostId = 1L; + InfoPostReq infoPostReq = TestFixture.createInfoPostReq(); + User author = TestFixture.createUser(); + Image image = TestFixture.createImage(); + Category category = TestFixture.createCategory(); + InfoPost infoPost = TestFixture.createInfoPost(infoPostReq, author, image, category); + + given(infoPostService.findById(infoPostId)).willReturn(infoPost); + given(reportRepository.existsByInfoPostIdAndUserId(infoPostId, author.getId())) + .willReturn(false); + + // when + Long result = reportService.reportedInfoPost(infoPostId, author); + + // then + assertThat(result).isEqualTo(infoPostId); + verify(reportRepository, times(1)).save(any()); + } + + @Test + @DisplayName("게시글 신고시 중복 신고로 인한 exception 발생") + void reportedInfoPost_throwCustomException_givenDuplicateReports() { + // given + Long infoPostId = 999L; + User loginUser = TestFixture.createUser(); + given(reportRepository.existsByInfoPostIdAndUserId(infoPostId, loginUser.getId())) + .willReturn(true); + + // when & then + assertThatThrownBy(() -> reportService.reportedInfoPost(infoPostId, loginUser)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.ALREADY_REPORTED_POST.getMessage()); + } +}