diff --git a/clokey-api/src/main/java/org/clokey/domain/report/controller/ReportController.java b/clokey-api/src/main/java/org/clokey/domain/report/controller/ReportController.java index 25a54d8f..6a7f3888 100644 --- a/clokey-api/src/main/java/org/clokey/domain/report/controller/ReportController.java +++ b/clokey-api/src/main/java/org/clokey/domain/report/controller/ReportController.java @@ -8,6 +8,7 @@ import org.clokey.code.GlobalBaseSuccessCode; import org.clokey.domain.report.dto.request.ReportCreateRequest; import org.clokey.domain.report.dto.response.ReportCreateResponse; +import org.clokey.domain.report.dto.response.ReportedCheckResponse; import org.clokey.domain.report.service.ReportService; import org.clokey.response.BaseResponse; import org.springframework.web.bind.annotation.*; @@ -28,4 +29,15 @@ public BaseResponse createNewReport( ReportCreateResponse response = reportService.createReport(reportCreatRequest); return BaseResponse.onSuccess(GlobalBaseSuccessCode.CREATED, response); } + + @GetMapping("/received") + @Operation( + operationId = "Report_checkReportReceived", + summary = "사용자에 접수된 신고 확인", + description = "사용자에 접수된 신고(미확인 상태)가 있는지 확인합니다.") + public BaseResponse checkReportReceived() { + ReportedCheckResponse response = reportService.checkReportReceived(); + + return BaseResponse.onSuccess(GlobalBaseSuccessCode.OK, response); + } } diff --git a/clokey-api/src/main/java/org/clokey/domain/report/dto/response/ReportedCheckResponse.java b/clokey-api/src/main/java/org/clokey/domain/report/dto/response/ReportedCheckResponse.java new file mode 100644 index 00000000..7679ee05 --- /dev/null +++ b/clokey-api/src/main/java/org/clokey/domain/report/dto/response/ReportedCheckResponse.java @@ -0,0 +1,13 @@ +package org.clokey.domain.report.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.clokey.report.enums.TargetType; + +public record ReportedCheckResponse( + @Schema(description = "UNCHECKED 상태의 신고가 존재하는지", example = "true") boolean isReported, + @Schema(description = "신고의 타입", example = "TargetType.HISTORY") TargetType targetType) { + + public static ReportedCheckResponse of(boolean isReported, TargetType targetType) { + return new ReportedCheckResponse(isReported, targetType); + } +} diff --git a/clokey-api/src/main/java/org/clokey/domain/report/repository/ReportRepository.java b/clokey-api/src/main/java/org/clokey/domain/report/repository/ReportRepository.java index 0d3e1890..ff45aab5 100644 --- a/clokey-api/src/main/java/org/clokey/domain/report/repository/ReportRepository.java +++ b/clokey-api/src/main/java/org/clokey/domain/report/repository/ReportRepository.java @@ -1,5 +1,6 @@ package org.clokey.domain.report.repository; +import java.util.Optional; import org.clokey.report.entity.Report; import org.clokey.report.enums.ReportStatus; import org.clokey.report.enums.TargetType; @@ -8,4 +9,7 @@ public interface ReportRepository extends JpaRepository { boolean existsByTargetTypeAndTargetIdAndReportStatusIsNot( TargetType targetType, Long TargetId, ReportStatus reportStatus); + + Optional findTopByReported_IdAndReportStatusOrderByCreatedAtDesc( + Long memberId, ReportStatus reportStatus); } diff --git a/clokey-api/src/main/java/org/clokey/domain/report/service/ReportService.java b/clokey-api/src/main/java/org/clokey/domain/report/service/ReportService.java index 04852e22..5d21dee0 100644 --- a/clokey-api/src/main/java/org/clokey/domain/report/service/ReportService.java +++ b/clokey-api/src/main/java/org/clokey/domain/report/service/ReportService.java @@ -2,8 +2,11 @@ import org.clokey.domain.report.dto.request.ReportCreateRequest; import org.clokey.domain.report.dto.response.ReportCreateResponse; +import org.clokey.domain.report.dto.response.ReportedCheckResponse; public interface ReportService { ReportCreateResponse createReport(ReportCreateRequest request); + + ReportedCheckResponse checkReportReceived(); } diff --git a/clokey-api/src/main/java/org/clokey/domain/report/service/ReportServiceImpl.java b/clokey-api/src/main/java/org/clokey/domain/report/service/ReportServiceImpl.java index 462a362e..2aed862e 100644 --- a/clokey-api/src/main/java/org/clokey/domain/report/service/ReportServiceImpl.java +++ b/clokey-api/src/main/java/org/clokey/domain/report/service/ReportServiceImpl.java @@ -7,6 +7,7 @@ import org.clokey.domain.history.repository.HistoryRepository; import org.clokey.domain.report.dto.request.ReportCreateRequest; import org.clokey.domain.report.dto.response.ReportCreateResponse; +import org.clokey.domain.report.dto.response.ReportedCheckResponse; import org.clokey.domain.report.exception.ReportErrorCode; import org.clokey.domain.report.repository.ReportRepository; import org.clokey.exception.BaseCustomException; @@ -32,14 +33,15 @@ public class ReportServiceImpl implements ReportService { @Override @Transactional public ReportCreateResponse createReport(ReportCreateRequest request) { - final Member reporter = memberUtil.getCurrentMember(); - validateTargetExists(request.targetType(), request.targetId()); + Member reporter = memberUtil.getCurrentMember(); + Member reported = getReportedMember(request.targetType(), request.targetId()); validateDuplicateReport(request); Report report = Report.createReport( request.targetId(), reporter, + reported, request.targetType(), request.reportReason(), request.content()); @@ -49,6 +51,37 @@ public ReportCreateResponse createReport(ReportCreateRequest request) { return ReportCreateResponse.from(report); } + @Override + public ReportedCheckResponse checkReportReceived() { + Member member = memberUtil.getCurrentMember(); + + Report report = + reportRepository + .findTopByReported_IdAndReportStatusOrderByCreatedAtDesc( + member.getId(), ReportStatus.UNCHECKED) + .orElse(null); + + if (report != null) { + return ReportedCheckResponse.of(true, report.getTargetType()); + } + + return ReportedCheckResponse.of(false, null); + } + + private Member getReportedMember(TargetType targetType, Long targetId) { + if (targetType.equals(TargetType.COMMENT)) { + return commentRepository + .findById(targetId) + .orElseThrow(() -> new BaseCustomException(CommentErrorCode.COMMENT_NOT_FOUND)) + .getMember(); + } else { + return historyRepository + .findById(targetId) + .orElseThrow(() -> new BaseCustomException(HistoryErrorCode.HISTORY_NOT_FOUND)) + .getMember(); + } + } + private void validateDuplicateReport(ReportCreateRequest request) { boolean exists = reportRepository.existsByTargetTypeAndTargetIdAndReportStatusIsNot( @@ -58,16 +91,4 @@ private void validateDuplicateReport(ReportCreateRequest request) { throw new BaseCustomException(ReportErrorCode.REPORT_DUPLICATED); } } - - private void validateTargetExists(TargetType targetType, Long targetId) { - if (targetType.equals(TargetType.COMMENT)) { - if (!commentRepository.existsById(targetId)) { - throw new BaseCustomException(CommentErrorCode.COMMENT_NOT_FOUND); - } - } else { - if (!historyRepository.existsById(targetId)) { - throw new BaseCustomException(HistoryErrorCode.HISTORY_NOT_FOUND); - } - } - } } diff --git a/clokey-api/src/test/java/org/clokey/domain/report/controller/ReportControllerTest.java b/clokey-api/src/test/java/org/clokey/domain/report/controller/ReportControllerTest.java index 57637b96..cb77e80f 100644 --- a/clokey-api/src/test/java/org/clokey/domain/report/controller/ReportControllerTest.java +++ b/clokey-api/src/test/java/org/clokey/domain/report/controller/ReportControllerTest.java @@ -1,12 +1,14 @@ package org.clokey.domain.report.controller; import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; import org.clokey.domain.report.dto.request.ReportCreateRequest; import org.clokey.domain.report.dto.response.ReportCreateResponse; +import org.clokey.domain.report.dto.response.ReportedCheckResponse; import org.clokey.domain.report.service.ReportService; import org.clokey.report.enums.ReportReason; import org.clokey.report.enums.TargetType; @@ -149,4 +151,27 @@ class 신고_생성_요청_시 { .value("신고 사유는 비워둘 수 없습니다.")); } } + + @Nested + class 접수된_미확인_신고_확인_요청_시 { + + @Test + void 유효한_요청이면_최신_UNCHECKED_상태의_신고_존재_여부를_반환한다() throws Exception { + // given + ReportedCheckResponse response = new ReportedCheckResponse(true, TargetType.COMMENT); + given(reportService.checkReportReceived()).willReturn(response); + + // when + ResultActions perform = mockMvc.perform(get("/reports/received")); + + // then + perform.andExpect(status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.isSuccess").value(true)) + .andExpect(MockMvcResultMatchers.jsonPath("$.code").value("COMMON200")) + .andExpect(MockMvcResultMatchers.jsonPath("$.result.isReported").value(true)) + .andExpect( + MockMvcResultMatchers.jsonPath("$.result.targetType") + .value(TargetType.COMMENT.name())); + } + } } diff --git a/clokey-api/src/test/java/org/clokey/domain/report/service/ReportServiceImplTest.java b/clokey-api/src/test/java/org/clokey/domain/report/service/ReportServiceImplTest.java index e35fc80d..0f882dd7 100644 --- a/clokey-api/src/test/java/org/clokey/domain/report/service/ReportServiceImplTest.java +++ b/clokey-api/src/test/java/org/clokey/domain/report/service/ReportServiceImplTest.java @@ -5,6 +5,7 @@ import static org.mockito.BDDMockito.given; import java.time.LocalDate; +import java.util.List; import org.clokey.IntegrationTest; import org.clokey.comment.entitiy.Comment; import org.clokey.domain.comment.exception.CommentErrorCode; @@ -14,6 +15,7 @@ import org.clokey.domain.history.repository.SituationRepository; import org.clokey.domain.member.repository.MemberRepository; import org.clokey.domain.report.dto.request.ReportCreateRequest; +import org.clokey.domain.report.dto.response.ReportedCheckResponse; import org.clokey.domain.report.exception.ReportErrorCode; import org.clokey.domain.report.repository.ReportRepository; import org.clokey.exception.BaseCustomException; @@ -87,11 +89,13 @@ void setUp() { .extracting( "targetId", "reporter.id", + "reported.id", "targetType", "reportReason", "reportStatus", "content") .containsExactly( + 1L, 1L, 1L, TargetType.HISTORY, @@ -182,4 +186,66 @@ void setUp() { .hasMessage(ReportErrorCode.REPORT_DUPLICATED.getMessage()); } } + + @Nested + class 접수된_미확인_신고_조회할_때 { + + @BeforeEach + void setUp() { + Member member1 = + Member.createMember( + "testEmail1", + "testClokeyId1", + "testNickName1", + OauthInfo.createOauthInfo("testOauthId1", OauthProvider.KAKAO)); + Member member2 = + Member.createMember( + "testEmail2", + "testClokeyId2", + "testNickName2", + OauthInfo.createOauthInfo("testOauthId2", OauthProvider.KAKAO)); + memberRepository.saveAll(List.of(member1, member2)); + given(memberUtil.getCurrentMember()).willReturn(member2); + + Situation situation = Situation.createSituation("testSituation"); + situationRepository.save(situation); + + History history1 = + History.createHistory( + LocalDate.of(2026, 1, 1), "testContent1", member2, situation); + historyRepository.save(history1); + + Report report = + Report.createReport( + 1L, + member1, + member2, + TargetType.HISTORY, + ReportReason.VIOLENT, + "Test Report"); + reportRepository.save(report); + } + + @Test + void 유효한_요청이면_미확인_신고_여부를_반환한다() { + // when & then + ReportedCheckResponse response = reportService.checkReportReceived(); + + assertThat(response.isReported()).isTrue(); + assertThat(response.targetType()).isEqualTo(TargetType.HISTORY); + } + + @Test + void 접수된_신고가_없으면_false를_반환한다() { + // given + Member member1 = memberRepository.findById(1L).orElse(null); + given(memberUtil.getCurrentMember()).willReturn(member1); + + // when & then + ReportedCheckResponse response = reportService.checkReportReceived(); + + assertThat(response.isReported()).isFalse(); + assertThat(response.targetType()).isEqualTo(null); + } + } } diff --git a/clokey-domain/src/main/java/org/clokey/report/entity/Report.java b/clokey-domain/src/main/java/org/clokey/report/entity/Report.java index 1699406d..1f47342c 100644 --- a/clokey-domain/src/main/java/org/clokey/report/entity/Report.java +++ b/clokey-domain/src/main/java/org/clokey/report/entity/Report.java @@ -24,10 +24,15 @@ public class Report extends BaseEntity { @NotNull private Long targetId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") + @JoinColumn(name = "reporter_member_id") @NotNull private Member reporter; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reported_member_id") + @NotNull + private Member reported; + @Enumerated(EnumType.STRING) @NotNull private TargetType targetType; @@ -46,12 +51,14 @@ public class Report extends BaseEntity { private Report( Long targetId, Member reporter, + Member reported, TargetType targetType, ReportReason reportReason, String content, ReportStatus reportStatus) { this.targetId = targetId; this.reporter = reporter; + this.reported = reported; this.targetType = targetType; this.reportReason = reportReason; this.content = content; @@ -61,6 +68,7 @@ private Report( public static Report createReport( Long targetId, Member reporter, + Member reported, TargetType targetType, ReportReason reportReason, String content) { @@ -68,6 +76,7 @@ public static Report createReport( Report.builder() .targetId(targetId) .reporter(reporter) + .reported(reported) .targetType(targetType) .reportReason(reportReason) .content(content) diff --git a/clokey-domain/src/main/resources/db/migration/V1__init.sql b/clokey-domain/src/main/resources/db/migration/V1__init.sql index f6a2f5e6..c26ef725 100644 --- a/clokey-domain/src/main/resources/db/migration/V1__init.sql +++ b/clokey-domain/src/main/resources/db/migration/V1__init.sql @@ -274,7 +274,9 @@ CREATE TABLE report ( id BIGINT AUTO_INCREMENT PRIMARY KEY, target_id BIGINT NOT NULL, - member_id BIGINT NOT NULL, + + reporter_member_id BIGINT NOT NULL, + reported_member_id BIGINT NOT NULL, report_reason VARCHAR(255) NOT NULL CHECK ( report_reason IN ( @@ -297,7 +299,8 @@ CREATE TABLE report ( created_at DATETIME(6) NOT NULL, updated_at DATETIME(6) NOT NULL, - CONSTRAINT fk_report_member FOREIGN KEY (member_id) REFERENCES member(id) + CONSTRAINT fk_report_reporter FOREIGN KEY (reporter_member_id) REFERENCES member(id), + CONSTRAINT fk_report_reported FOREIGN KEY (reported_member_id) REFERENCES member(id) ); CREATE TABLE member_term (