diff --git a/src/main/java/devkor/com/teamcback/domain/report/controller/AdminReportController.java b/src/main/java/devkor/com/teamcback/domain/report/controller/AdminReportController.java new file mode 100644 index 0000000..091bf2b --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/controller/AdminReportController.java @@ -0,0 +1,52 @@ +package devkor.com.teamcback.domain.report.controller; + +import devkor.com.teamcback.domain.report.dto.request.UpdateReportStatusReq; +import devkor.com.teamcback.domain.report.dto.response.GetReportListRes; +import devkor.com.teamcback.domain.report.dto.response.UpdateReportStatusRes; +import devkor.com.teamcback.domain.report.entity.ReportStatus; +import devkor.com.teamcback.domain.report.service.ReportService; +import devkor.com.teamcback.global.response.CommonResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/admin/reports") +public class AdminReportController { + private final ReportService reportService; + + @Operation(summary = "신고 관리를 위한 목록 조회", + description = "신고 상태에 따라 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "401", description = "권한이 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @GetMapping + public CommonResponse getReportList( + @Parameter(name = "reportStatus", description = "신고 상태 종류") @RequestParam(required = false) ReportStatus reportStatus + ) { + return CommonResponse.success(reportService.getReportList(reportStatus)); + } + + @Operation(summary = "신고 상태 변경", + description = "신고 상태 변경") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "401", description = "권한이 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @PutMapping("/{reportId}") + public CommonResponse updateReportStatus( + @Parameter(description = "변경할 신고 id") @PathVariable Long reportId, + @Parameter(description = "신고의 변경할 생태") @RequestBody UpdateReportStatusReq req + ) { + return CommonResponse.success(reportService.updateReportStatus(reportId, req)); + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/controller/ReportController.java b/src/main/java/devkor/com/teamcback/domain/report/controller/ReportController.java new file mode 100644 index 0000000..dd2b793 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/controller/ReportController.java @@ -0,0 +1,56 @@ +package devkor.com.teamcback.domain.report.controller; + +import devkor.com.teamcback.domain.report.dto.request.CreateReviewReportReq; +import devkor.com.teamcback.domain.report.dto.response.CreateReviewReportRes; +import devkor.com.teamcback.domain.report.dto.response.GetUserReviewReportStatusRes; +import devkor.com.teamcback.domain.report.service.ReportService; +import devkor.com.teamcback.global.response.CommonResponse; +import devkor.com.teamcback.global.security.UserDetailsImpl; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/reports") +public class ReportController { + + private final ReportService reportService; + + @Operation(summary = "신고 작성", + description = "리뷰에 대한 신고를 작성") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "401", description = "권한이 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @PostMapping(value = "/reviews/{reviewId}") + public CommonResponse createReviewReport( + @Parameter(description = "사용자정보") @AuthenticationPrincipal UserDetailsImpl userDetail, + @Parameter(name = "reviewId", description = "리뷰 ID") @PathVariable Long reviewId, + @Parameter(description = "리뷰 작성 내용", required = true) @Valid @RequestBody CreateReviewReportReq req) { + + return CommonResponse.success(reportService.createReviewReport(userDetail == null ? null : userDetail.getUser().getUserId(), reviewId, req)); + } + + @Operation(summary = "사용자 리뷰 신고 여부 조회", + description = "사용자 신고된 상태인지 확인하고 알림(비회원은 조회 x)") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "401", description = "권한이 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @GetMapping(value = "/status") + public CommonResponse getUserReviewReportStatus( + @Parameter(description = "사용자정보", required = true) @AuthenticationPrincipal UserDetailsImpl userDetail) + { + return CommonResponse.success(reportService.getUserReviewReportStatus(userDetail.getUser().getUserId())); + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/dto/request/CreateReviewReportReq.java b/src/main/java/devkor/com/teamcback/domain/report/dto/request/CreateReviewReportReq.java new file mode 100644 index 0000000..1a7f3d9 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/dto/request/CreateReviewReportReq.java @@ -0,0 +1,17 @@ +package devkor.com.teamcback.domain.report.dto.request; + +import devkor.com.teamcback.domain.report.entity.ReasonCategory; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Schema(description = "저장할 리뷰 신고 내용") +@Getter +@Setter +public class CreateReviewReportReq { + @NotNull + private ReasonCategory reasonCategory; // 신고 이유 + private String content; // 신고 내용 + +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/dto/request/UpdateReportStatusReq.java b/src/main/java/devkor/com/teamcback/domain/report/dto/request/UpdateReportStatusReq.java new file mode 100644 index 0000000..47e0d9a --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/dto/request/UpdateReportStatusReq.java @@ -0,0 +1,15 @@ +package devkor.com.teamcback.domain.report.dto.request; + +import devkor.com.teamcback.domain.report.entity.ReportStatus; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Schema(description = "수정할 신고 내용") +@Getter +@Setter +public class UpdateReportStatusReq { + @NotNull + private ReportStatus status; +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/dto/response/CreateReviewReportRes.java b/src/main/java/devkor/com/teamcback/domain/report/dto/response/CreateReviewReportRes.java new file mode 100644 index 0000000..7813a62 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/dto/response/CreateReviewReportRes.java @@ -0,0 +1,9 @@ +package devkor.com.teamcback.domain.report.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "리뷰 신고 저장 완료") +@JsonIgnoreProperties +public class CreateReviewReportRes { +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/dto/response/GetReportListRes.java b/src/main/java/devkor/com/teamcback/domain/report/dto/response/GetReportListRes.java new file mode 100644 index 0000000..ee07da8 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/dto/response/GetReportListRes.java @@ -0,0 +1,14 @@ +package devkor.com.teamcback.domain.report.dto.response; + +import lombok.Getter; + +import java.util.List; + +@Getter +public class GetReportListRes { + List reportList; + + public GetReportListRes(List reportList) { + this.reportList = reportList; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/dto/response/GetReportRes.java b/src/main/java/devkor/com/teamcback/domain/report/dto/response/GetReportRes.java new file mode 100644 index 0000000..ab3c71d --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/dto/response/GetReportRes.java @@ -0,0 +1,31 @@ +package devkor.com.teamcback.domain.report.dto.response; + +import devkor.com.teamcback.domain.report.entity.Report; +import lombok.Getter; + +@Getter +public class GetReportRes { + private Long reportId; + private Long targetId; + private String targetType; + private String reasonCategory; + private String content; + private String status; + private String effectiveAt; + private String createdAt; + private Long reporterId; + private Long reporterUserId; + + public GetReportRes(Report report) { + this.reportId = report.getId(); + this.targetId = report.getTargetId(); + this.targetType = report.getTargetType().toString(); + this.reasonCategory = report.getReasonCategory().toString(); + this.content = report.getContent(); + this.status = report.getStatus().toString(); + this.effectiveAt = report.getEffectiveAt() != null ? report.getEffectiveAt().toString() : null; + this.createdAt = report.getCreatedAt() != null ? report.getCreatedAt().toString() : null; + this.reporterId = report.getReporter().getUserId(); + this.reporterUserId = report.getReportedUser().getUserId(); + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/dto/response/GetReportedReviewRes.java b/src/main/java/devkor/com/teamcback/domain/report/dto/response/GetReportedReviewRes.java new file mode 100644 index 0000000..de5524e --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/dto/response/GetReportedReviewRes.java @@ -0,0 +1,29 @@ +package devkor.com.teamcback.domain.report.dto.response; + +import devkor.com.teamcback.domain.review.entity.Review; +import lombok.Getter; + +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +@Getter +public class GetReportedReviewRes { + private Long reviewId; + private String comment; + private Long placeId; + private String placeName; + private String reviewCreatedAt; + private String reasonCategory; + + public GetReportedReviewRes(Review review, String reasonCategory) { + this.reviewId = review.getId(); + this.placeId = review.getPlace().getId(); + this.placeName = review.getPlace().getName(); + this.comment = review.getComment(); + this.reasonCategory = reasonCategory; + + // 작성일 변환 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yy.MM.dd(E)", Locale.KOREAN); + this.reviewCreatedAt = review.getCreatedAt() != null ? review.getCreatedAt().format(formatter): ""; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/dto/response/GetUserReviewReportStatusRes.java b/src/main/java/devkor/com/teamcback/domain/report/dto/response/GetUserReviewReportStatusRes.java new file mode 100644 index 0000000..467deb4 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/dto/response/GetUserReviewReportStatusRes.java @@ -0,0 +1,21 @@ +package devkor.com.teamcback.domain.report.dto.response; + +import devkor.com.teamcback.domain.user.entity.User; +import lombok.Getter; + +import java.util.List; + +@Getter +public class GetUserReviewReportStatusRes { + private Long userId; + private String userName; + private boolean isReported; + private List reviewList; + + public GetUserReviewReportStatusRes(User user, List reviewList) { + this.userId = user.getUserId(); + this.userName = user.getUsername(); + this.isReported = !reviewList.isEmpty(); + this.reviewList = reviewList; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/dto/response/UpdateReportStatusRes.java b/src/main/java/devkor/com/teamcback/domain/report/dto/response/UpdateReportStatusRes.java new file mode 100644 index 0000000..1fe7bcf --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/dto/response/UpdateReportStatusRes.java @@ -0,0 +1,9 @@ +package devkor.com.teamcback.domain.report.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; + +@Getter +@JsonIgnoreProperties +public class UpdateReportStatusRes { +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/entity/ReasonCategory.java b/src/main/java/devkor/com/teamcback/domain/report/entity/ReasonCategory.java new file mode 100644 index 0000000..33f86bc --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/entity/ReasonCategory.java @@ -0,0 +1,13 @@ +package devkor.com.teamcback.domain.report.entity; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum ReasonCategory { + ABUSE_OR_DISCRIMINATION("욕설/비하"), + DEFAMATION("명예훼손"), + SPAM_OR_ADVERTISING("홍보/도배"), + INAPPROPRIATE_CONTENT("음란/선정"); + + private final String label; +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/entity/Report.java b/src/main/java/devkor/com/teamcback/domain/report/entity/Report.java new file mode 100644 index 0000000..6d0f0e0 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/entity/Report.java @@ -0,0 +1,61 @@ +package devkor.com.teamcback.domain.report.entity; + +import devkor.com.teamcback.domain.common.entity.BaseEntity; +import devkor.com.teamcback.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; + +@Entity +@Getter +@Table(name = "tb_report") +@NoArgsConstructor +public class Report extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column + @Enumerated(EnumType.STRING) + private TargetType targetType; + + @Column + private Long targetId; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private ReasonCategory reasonCategory; + + @Column(length = 300) + private String content; + + @Setter + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private ReportStatus status; + + @Setter + @Column + private LocalDate effectiveAt; // 신고 정지 시작일 + + @ManyToOne + @JoinColumn(name = "reporter_id") + private User reporter; + + @ManyToOne + @JoinColumn(name = "reported_user_id") + private User reportedUser; + + public Report(TargetType targetType, Long targetId, ReasonCategory reasonCategory, String content, ReportStatus status, User reporter, User reportedUser) { + this.targetType = targetType; + this.targetId = targetId; + this.reasonCategory = reasonCategory; + this.content = content; + this.status = status; + this.reporter = reporter; + this.reportedUser = reportedUser; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/entity/ReportStatus.java b/src/main/java/devkor/com/teamcback/domain/report/entity/ReportStatus.java new file mode 100644 index 0000000..9b4f2f4 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/entity/ReportStatus.java @@ -0,0 +1,8 @@ +package devkor.com.teamcback.domain.report.entity; + +public enum ReportStatus { + PENDING, + RESOLVED, + REJECTED, + EXPIRED +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/entity/TargetType.java b/src/main/java/devkor/com/teamcback/domain/report/entity/TargetType.java new file mode 100644 index 0000000..c32dfad --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/entity/TargetType.java @@ -0,0 +1,5 @@ +package devkor.com.teamcback.domain.report.entity; + +public enum TargetType { + REVIEW +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/repository/CustomReportRepository.java b/src/main/java/devkor/com/teamcback/domain/report/repository/CustomReportRepository.java new file mode 100644 index 0000000..de42fc0 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/repository/CustomReportRepository.java @@ -0,0 +1,12 @@ +package devkor.com.teamcback.domain.report.repository; + +import devkor.com.teamcback.domain.report.entity.Report; +import devkor.com.teamcback.domain.report.entity.ReportStatus; +import devkor.com.teamcback.domain.report.entity.TargetType; +import devkor.com.teamcback.domain.user.entity.User; + +import java.util.List; + +public interface CustomReportRepository { + List findUniqueReportsForUserReviewReportStatus(User user, TargetType type, ReportStatus status); +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/repository/CustomReportRepositoryImpl.java b/src/main/java/devkor/com/teamcback/domain/report/repository/CustomReportRepositoryImpl.java new file mode 100644 index 0000000..3bd8af4 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/repository/CustomReportRepositoryImpl.java @@ -0,0 +1,47 @@ +package devkor.com.teamcback.domain.report.repository; + +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import devkor.com.teamcback.domain.report.entity.Report; +import devkor.com.teamcback.domain.report.entity.ReportStatus; +import devkor.com.teamcback.domain.report.entity.TargetType; +import devkor.com.teamcback.domain.user.entity.User; +import devkor.com.teamcback.domain.vote.dto.response.QGetVoteOptionRes; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static devkor.com.teamcback.domain.report.entity.QReport.report; +import static devkor.com.teamcback.domain.vote.entity.QVote.vote; +import static devkor.com.teamcback.domain.vote.entity.QVoteOption.voteOption; +import static devkor.com.teamcback.domain.vote.entity.QVoteRecord.voteRecord; + +@Repository +@RequiredArgsConstructor +public class CustomReportRepositoryImpl implements CustomReportRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List findUniqueReportsForUserReviewReportStatus(User user, TargetType type, ReportStatus status) { + // 서브쿼리로 각 그룹의 최신 ID 추출 + JPQLQuery subQuery = JPAExpressions + .select(report.id.max()) + .from(report) + .where( + report.reportedUser.eq(user) + .and(report.targetType.eq(type)) + .and(report.status.eq(status)) + ) + .groupBy(report.targetId, report.reasonCategory) + .where(); + + return jpaQueryFactory + .selectFrom(report) + .where(report.id.in(subQuery)) + .orderBy(report.effectiveAt.asc()) + .fetch(); + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/repository/ReportRepository.java b/src/main/java/devkor/com/teamcback/domain/report/repository/ReportRepository.java new file mode 100644 index 0000000..6dc63f4 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/repository/ReportRepository.java @@ -0,0 +1,16 @@ +package devkor.com.teamcback.domain.report.repository; + +import devkor.com.teamcback.domain.report.entity.Report; +import devkor.com.teamcback.domain.report.entity.ReportStatus; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ReportRepository extends JpaRepository, CustomReportRepository { + + List findByStatusOrderByCreatedAtDesc(ReportStatus status); + + List findAllByOrderByCreatedAtDesc(); + + List findAllByStatus(ReportStatus reportStatus); +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/scheduler/ReportScheduler.java b/src/main/java/devkor/com/teamcback/domain/report/scheduler/ReportScheduler.java new file mode 100644 index 0000000..89867c5 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/scheduler/ReportScheduler.java @@ -0,0 +1,41 @@ +package devkor.com.teamcback.domain.report.scheduler; + +import devkor.com.teamcback.domain.report.service.ReportService; +import devkor.com.teamcback.global.redis.RedisLockUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Slf4j(topic = "Report status Scheduler") +@Component +@RequiredArgsConstructor +public class ReportScheduler { + + @Value("${metrics.environment}") + private String env; + private final RedisLockUtil redisLockUtil; + private final ReportService reportService; + + @Scheduled(cron = "0 1 0 * * *") // 매일 자정마다 + @EventListener(ApplicationReadyEvent.class) + public void updateReportStatus() { + + // 배포 서버에서만 실행 + if(!env.equals("prod")) return; + + try{ + log.info("신고 상태 업데이트"); + + redisLockUtil.executeWithLock("report-lock", 1, 300, () -> { + reportService.updateExpiredReportStatus(); + return null; + }); + } catch (Exception e) { + log.info("updateReportStatus() 작업 실패: {}", e.getMessage(), e); + } + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/report/service/ReportService.java b/src/main/java/devkor/com/teamcback/domain/report/service/ReportService.java new file mode 100644 index 0000000..46950ff --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/service/ReportService.java @@ -0,0 +1,160 @@ +package devkor.com.teamcback.domain.report.service; + +import devkor.com.teamcback.domain.report.dto.request.CreateReviewReportReq; +import devkor.com.teamcback.domain.report.dto.request.UpdateReportStatusReq; +import devkor.com.teamcback.domain.report.dto.response.*; +import devkor.com.teamcback.domain.report.entity.Report; +import devkor.com.teamcback.domain.report.entity.ReportStatus; +import devkor.com.teamcback.domain.report.entity.TargetType; +import devkor.com.teamcback.domain.report.repository.ReportRepository; +import devkor.com.teamcback.domain.review.entity.Review; +import devkor.com.teamcback.domain.review.repository.ReviewRepository; +import devkor.com.teamcback.domain.user.entity.User; +import devkor.com.teamcback.domain.user.repository.UserRepository; +import devkor.com.teamcback.global.exception.exception.GlobalException; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import static devkor.com.teamcback.domain.report.entity.ReportStatus.*; +import static devkor.com.teamcback.global.response.ResultCode.*; + +@Service +@RequiredArgsConstructor +public class ReportService { + + private final ReportRepository reportRepository; + private final ReviewRepository reviewRepository; + private final UserRepository userRepository; + + /** + * 리뷰에 대한 신고 작성 + */ + @Transactional + public CreateReviewReportRes createReviewReport(Long userId, Long reviewId, @Valid CreateReviewReportReq req) { + // 신고한 사용자(비회원 가능) + User user = userId == null ? null : findUser(userId); + + // 신고된 게시물 + Review review = findReview(reviewId); + + // TODO: 정책 설정 후 다시 - 신고되자마자 처리하는 경우 + // review.setReported(true); + + // 신고 저장 + Report report = new Report(TargetType.REVIEW, reviewId, req.getReasonCategory(), req.getContent(), PENDING, user, review.getUser()); + reportRepository.save(report); + + return new CreateReviewReportRes(); + } + + /** + * 사용자가 신고되었는지 확인 + */ + @Transactional(readOnly = true) + public GetUserReviewReportStatusRes getUserReviewReportStatus(Long userId) { + // 사용자 검색 + User user = findUser(userId); + + // TODO: 하나만 조회하는 경우 수정 + // 신고 검색 + List reportList = reportRepository.findUniqueReportsForUserReviewReportStatus(user, TargetType.REVIEW, RESOLVED); + + // 신고된 리뷰 목록 조회 + List reviewList = new ArrayList<>(); + for(Report report : reportList) { + reviewList.add(new GetReportedReviewRes(findReview(report.getTargetId()), report.getReasonCategory().toString())); + } + + return new GetUserReviewReportStatusRes(user, reviewList); + } + + /** + * 신고 목록 조회 + */ + @Transactional(readOnly = true) + public GetReportListRes getReportList(ReportStatus status) { + List reportList; + + // 신고 상태에 따라 조회 + if (status == null) { + reportList = reportRepository.findAllByOrderByCreatedAtDesc(); + } + else { + reportList = reportRepository.findByStatusOrderByCreatedAtDesc(status); + } + + return new GetReportListRes(reportList.stream().map(GetReportRes::new).toList()); + } + + /** + * 신고 상태 변경 + */ + @Transactional + public UpdateReportStatusRes updateReportStatus(Long reportId, UpdateReportStatusReq req) { + // 신고 + Report report = findReport(reportId); + + // 신고 상태 수정 + report.setStatus(req.getStatus()); + + // 신고가 받아들여진 경우 + if(req.getStatus().equals(RESOLVED)) { + // 신고 유효일 설정 + report.setEffectiveAt(LocalDate.now()); + + // 신고 대상이 리뷰인 경우 + if(report.getTargetType().equals(TargetType.REVIEW)) { + Review review = findReview(report.getTargetId()); + // 신고된 리뷰 설정(신고가 만료되거나 해도 이 필드는 그대로 유지됨) + review.setReported(true); + } + } + + return new UpdateReportStatusRes(); + } + + /** + * 신고 유효일 체크하고 상태 수정 + */ + @Transactional + public void updateExpiredReportStatus() { + // 유효한 신고 내역 조회 + List reportList = reportRepository.findAllByStatus(RESOLVED); + + for(Report report : reportList) { + LocalDate thirtyDaysAfter = report.getEffectiveAt().plusDays(30); + + if (LocalDate.now().isAfter(thirtyDaysAfter)) { + // 30일이 지났으면 상태 변경 + report.setStatus(EXPIRED); + } + } + } + + /** + * 사용자 찾기 + */ + private User findUser(Long userId) { + return userRepository.findById(userId).orElseThrow(() -> new GlobalException(NOT_FOUND_USER)); + } + + /** + * 리뷰 찾기 + */ + private Review findReview(Long reviewId) { + return reviewRepository.findById(reviewId).orElseThrow(() -> new GlobalException(NOT_FOUND_REVIEW)); + } + + /** + * 신고 찾기 + */ + private Report findReport(Long reportId) { + return reportRepository.findById(reportId).orElseThrow(() -> new GlobalException(NOT_FOUND_REPORT)); + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java b/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java index b626c2f..b0df7e3 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java +++ b/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java @@ -36,6 +36,10 @@ public class Review extends BaseEntity { @Column(nullable = false, length = 500) private String comment = ""; // 500자까지 작성 가능 + @Setter + @Column(nullable = false) + private boolean isReported = false; + @ManyToOne @JoinColumn(name = "user_id") private User user; diff --git a/src/main/java/devkor/com/teamcback/global/response/ResultCode.java b/src/main/java/devkor/com/teamcback/global/response/ResultCode.java index 9fb13a4..1c253ec 100644 --- a/src/main/java/devkor/com/teamcback/global/response/ResultCode.java +++ b/src/main/java/devkor/com/teamcback/global/response/ResultCode.java @@ -97,8 +97,10 @@ public enum ResultCode { // 리뷰 15000번대 NOT_FOUND_REVIEW_TAG(HttpStatus.NOT_FOUND, 15000, "리뷰 태그를 찾을 수 없습니다."), - NOT_FOUND_REVIEW(HttpStatus.NOT_FOUND, 15001, "리뷰를 찾을 수 없습니다.") - ; + NOT_FOUND_REVIEW(HttpStatus.NOT_FOUND, 15001, "리뷰를 찾을 수 없습니다."), + + // 신고 16000번대 + NOT_FOUND_REPORT(HttpStatus.NOT_FOUND, 16000, "신고를 찾을 수 없습니다."); private final HttpStatus status; private final int code; diff --git a/src/main/java/devkor/com/teamcback/global/security/SecurityConfig.java b/src/main/java/devkor/com/teamcback/global/security/SecurityConfig.java index ab99038..7769517 100644 --- a/src/main/java/devkor/com/teamcback/global/security/SecurityConfig.java +++ b/src/main/java/devkor/com/teamcback/global/security/SecurityConfig.java @@ -91,6 +91,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/api/categories/**").authenticated() .requestMatchers("/api/bookmarks/**").authenticated() .requestMatchers(HttpMethod.POST, "/api/reviews/**").authenticated() // 리뷰는 로그인 필요 + .requestMatchers("/api/reports/status").authenticated() // 신고 상태 확인은 로그인 필요 .anyRequest().permitAll() ).exceptionHandling(ex -> ex .accessDeniedHandler(customAccessDeniedHandler()) // 인가 실패 시