Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 신고 API 추가 #83

Merged
merged 4 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/main/java/com/join/core/common/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ public enum ErrorCode {
BOOKMARK_NOT_FOUND(HttpStatus.BAD_REQUEST, "B-002", "취소할 북마크를 찾을 수 없습니다."),

/**
* 신고 관련 오류
*/
REPORT_TIME_LIMIT(HttpStatus.BAD_REQUEST, "R-001", "신고자는 30분 내에 동일한 신고를 할 수 없습니다."),

/**
* 평가 관련 오류
*/
EVALUATION_PERIOD_INVALID(HttpStatus.BAD_REQUEST, "EV-001", "스터디원 평가 기간이 아닙니다."),
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/join/core/report/constant/ReportType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.join.core.report.constant;

public enum ReportType {
STUDY_CONTENT, // 스터디 모집 게시글이 아니에요
ENROLLMENT_PROCESS, // 스터디 가입 과정에서 문제가 발생했어요
SCAM, // 사기인 것 같아요
MISCONDUCT, // 기타 부적절한 행위가 있어요
WRITER // 작성자 신고하기

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.join.core.report.controller;

import com.join.core.auth.domain.UserPrincipal;
import com.join.core.common.response.ApiResponse;
import com.join.core.report.controller.specification.ReportApiSpecification;
import com.join.core.report.domain.ReportService;
import com.join.core.report.dto.request.ReportRequest;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@RestController
@RequestMapping("${api.prefix}/report")
public class ReportController implements ReportApiSpecification {

private final ReportService reportService;

@Override
@PreAuthorize("isAuthenticated()")
@PostMapping
public ApiResponse<Void> createReport(
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody ReportRequest reportRequest
) {
reportService.createReport(principal.getAvatarId(), reportRequest.getStudyId(), reportRequest);
return ApiResponse.ok();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.join.core.report.controller.specification;

import com.join.core.auth.domain.UserPrincipal;
import com.join.core.common.response.ApiResponse;
import com.join.core.report.dto.request.ReportRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.core.annotation.AuthenticationPrincipal;

public interface ReportApiSpecification {
@Tag(name = "${swagger.tag.report}")
@Operation(summary = "신고 API - 인증 필수",
description = "신고 API - 인증 필수",
security = {@SecurityRequirement(name = "session-token")})
ApiResponse<Void> createReport(
@AuthenticationPrincipal UserPrincipal principal,
ReportRequest reportRequest
);

}
48 changes: 48 additions & 0 deletions src/main/java/com/join/core/report/domain/Report.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.join.core.report.domain;

import com.join.core.avatar.domain.Avatar;
import com.join.core.report.constant.ReportType;
import com.join.core.study.domain.Study;
import com.join.core.common.domain.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Report extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ReportType reportType;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "study_id")
private Study study;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "writer_id")
private Avatar writer;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "reporter_id")
private Avatar reporter;

@Column(nullable = false, length = 300)
private String reason;

public Report(ReportType reportType, Study study, Avatar writer, Avatar reporter, String reason) {
this.reportType = reportType;
this.study = study;
this.writer = writer;
this.reporter = reporter;
this.reason = reason;
}

}
9 changes: 9 additions & 0 deletions src/main/java/com/join/core/report/domain/ReportReader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.join.core.report.domain;

import com.join.core.avatar.domain.Avatar;
import com.join.core.study.domain.Study;

public interface ReportReader {
void validateReportTimeLimit(Avatar reporter, Study study);

}
32 changes: 32 additions & 0 deletions src/main/java/com/join/core/report/domain/ReportService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.join.core.report.domain;

import com.join.core.avatar.domain.AvatarReader;
import com.join.core.report.dto.request.ReportRequest;
import com.join.core.study.domain.Study;
import com.join.core.avatar.domain.Avatar;
import com.join.core.study.service.StudyReader;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class ReportService {

private final StudyReader studyReader;
private final AvatarReader avatarReader;
private final ReportReader reportReader;
private final ReportStore reportStore;

@Transactional
public void createReport(Long reporterId, Long studyId, ReportRequest reportRequest) {
Study study = studyReader.getStudyById(studyId);
Avatar reporter = avatarReader.getAvatarById(reporterId);
Avatar writer = study.getWriter();

uuujini marked this conversation as resolved.
Show resolved Hide resolved
reportReader.validateReportTimeLimit(reporter, study);

reportStore.store(reportRequest, study, writer, reporter);
}

}
10 changes: 10 additions & 0 deletions src/main/java/com/join/core/report/domain/ReportStore.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.join.core.report.domain;

import com.join.core.report.dto.request.ReportRequest;
import com.join.core.study.domain.Study;
import com.join.core.avatar.domain.Avatar;

public interface ReportStore {
void store(ReportRequest reportRequest, Study study, Avatar writer, Avatar reporter);

}
31 changes: 31 additions & 0 deletions src/main/java/com/join/core/report/dto/request/ReportRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.join.core.report.dto.request;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.join.core.report.constant.ReportType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class ReportRequest {

@NotNull
@Schema(description = "신고 유형", example = "STUDY_CONTENT")
private ReportType reportType;

@NotNull
@Schema(description = "스터디 ID", example = "1")
private Long studyId;

@NotBlank
@Size(min = 10, max = 300)
@Schema(description = "신고 사유", example = "스터디 모집 게시글이 아니에요.")
private String reason;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.join.core.report.repository;

import com.join.core.avatar.domain.Avatar;
import com.join.core.common.exception.ErrorCode;
import com.join.core.common.exception.impl.BadRequestException;
import com.join.core.report.domain.Report;
import com.join.core.report.domain.ReportReader;
import com.join.core.study.domain.Study;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@RequiredArgsConstructor
@Component
public class ReportReaderImpl implements ReportReader {

private final ReportRepository reportRepository;

@Override
public void validateReportTimeLimit(Avatar reporter, Study study) {
LocalDateTime timeLimit = LocalDateTime.now().minusMinutes(30);
Report recentReport = reportRepository.findRecentReport(reporter, study, timeLimit);

if (recentReport != null) {
throw new BadRequestException(ErrorCode.REPORT_TIME_LIMIT);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.join.core.report.repository;

import com.join.core.report.domain.Report;
import com.join.core.avatar.domain.Avatar;
import com.join.core.study.domain.Study;
import io.lettuce.core.dynamic.annotation.Param;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.time.LocalDateTime;

public interface ReportRepository extends JpaRepository<Report, Long> {

@Query("SELECT r FROM Report r WHERE r.reporter = :reporter AND r.study = :study AND r.createdDate > :timeLimit ORDER BY r.createdDate DESC")
Report findRecentReport(@Param("reporter") Avatar reporter, @Param("study") Study study, @Param("timeLimit") LocalDateTime timeLimit);

}
28 changes: 28 additions & 0 deletions src/main/java/com/join/core/report/repository/ReportStoreImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.join.core.report.repository;

import com.join.core.report.constant.ReportType;
import com.join.core.report.domain.Report;
import com.join.core.report.domain.ReportStore;
import com.join.core.study.domain.Study;
import com.join.core.avatar.domain.Avatar;
import com.join.core.report.dto.request.ReportRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class ReportStoreImpl implements ReportStore {

private final ReportRepository reportRepository;

@Override
public void store(ReportRequest reportRequest, Study study, Avatar writer, Avatar reporter) {
ReportType reportType = reportRequest.getReportType();
String reason = reportRequest.getReason();

Report report = new Report(reportType, study, writer, reporter, reason);

reportRepository.save(report);
}

}
1 change: 1 addition & 0 deletions src/main/resources/application-swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ swagger:
evaluation: '12. 평가'
block: '13. 차단'
my-page: '14. 마이페이지'
report: '15. 신고'