From e6c61d990eb7c53ba9b570561a867e47ef4576ca Mon Sep 17 00:00:00 2001 From: yejin Date: Mon, 9 Feb 2026 16:44:55 +0900 Subject: [PATCH 1/6] =?UTF-8?q?Feat:=20=EC=8B=A0=EA=B3=A0=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/report/entity/ReasonCategory.java | 13 ++++++ .../domain/report/entity/Report.java | 44 +++++++++++++++++++ .../domain/report/entity/ReportStatus.java | 8 ++++ .../domain/report/entity/TargetType.java | 5 +++ .../domain/review/entity/Review.java | 3 ++ 5 files changed, 73 insertions(+) create mode 100644 src/main/java/devkor/com/teamcback/domain/report/entity/ReasonCategory.java create mode 100644 src/main/java/devkor/com/teamcback/domain/report/entity/Report.java create mode 100644 src/main/java/devkor/com/teamcback/domain/report/entity/ReportStatus.java create mode 100644 src/main/java/devkor/com/teamcback/domain/report/entity/TargetType.java 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..a4b81c5 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/entity/Report.java @@ -0,0 +1,44 @@ +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 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 + private String content; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private ReportStatus status; + + @Column + private LocalDate effective_at; // 신고 정지 시작일 + + @ManyToOne + @JoinColumn(name = "user_id") + private User reporter; +} 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/review/entity/Review.java b/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java index b626c2f..c415a49 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,9 @@ public class Review extends BaseEntity { @Column(nullable = false, length = 500) private String comment = ""; // 500자까지 작성 가능 + @Column(nullable = false) + private boolean isReported = false; + @ManyToOne @JoinColumn(name = "user_id") private User user; From 65ff60c44e45d99a1eeb155ce7c0deb18054c9ba Mon Sep 17 00:00:00 2001 From: yejin Date: Tue, 10 Feb 2026 14:59:05 +0900 Subject: [PATCH 2/6] =?UTF-8?q?Feat:=20=EC=8B=A0=EA=B3=A0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/controller/ReportController.java | 41 +++++++++++++ .../dto/request/CreateReviewReportReq.java | 17 ++++++ .../dto/response/CreateReviewReportRes.java | 9 +++ .../domain/report/entity/Report.java | 13 +++- .../report/repository/ReportRepository.java | 7 +++ .../domain/report/service/ReportService.java | 61 +++++++++++++++++++ .../domain/review/entity/Review.java | 1 + 7 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 src/main/java/devkor/com/teamcback/domain/report/controller/ReportController.java create mode 100644 src/main/java/devkor/com/teamcback/domain/report/dto/request/CreateReviewReportReq.java create mode 100644 src/main/java/devkor/com/teamcback/domain/report/dto/response/CreateReviewReportRes.java create mode 100644 src/main/java/devkor/com/teamcback/domain/report/repository/ReportRepository.java create mode 100644 src/main/java/devkor/com/teamcback/domain/report/service/ReportService.java 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..33b4ff9 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/controller/ReportController.java @@ -0,0 +1,41 @@ +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.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)); + } +} 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/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/entity/Report.java b/src/main/java/devkor/com/teamcback/domain/report/entity/Report.java index a4b81c5..0adfbee 100644 --- a/src/main/java/devkor/com/teamcback/domain/report/entity/Report.java +++ b/src/main/java/devkor/com/teamcback/domain/report/entity/Report.java @@ -28,7 +28,7 @@ public class Report extends BaseEntity { @Enumerated(EnumType.STRING) private ReasonCategory reasonCategory; - @Column + @Column(length = 300) private String content; @Column(nullable = false) @@ -36,9 +36,18 @@ public class Report extends BaseEntity { private ReportStatus status; @Column - private LocalDate effective_at; // 신고 정지 시작일 + private LocalDate effectiveAt; // 신고 정지 시작일 @ManyToOne @JoinColumn(name = "user_id") private User reporter; + + public Report(TargetType targetType, Long targetId, ReasonCategory reasonCategory, String content, ReportStatus status, User reporter) { + this.targetType = targetType; + this.targetId = targetId; + this.reasonCategory = reasonCategory; + this.content = content; + this.status = status; + this.reporter = reporter; + } } 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..2d304ba --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/repository/ReportRepository.java @@ -0,0 +1,7 @@ +package devkor.com.teamcback.domain.report.repository; + +import devkor.com.teamcback.domain.report.entity.Report; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReportRepository extends JpaRepository { +} 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..77af7ac --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/service/ReportService.java @@ -0,0 +1,61 @@ +package devkor.com.teamcback.domain.report.service; + +import devkor.com.teamcback.domain.report.dto.request.CreateReviewReportReq; +import devkor.com.teamcback.domain.report.dto.response.CreateReviewReportRes; +import devkor.com.teamcback.domain.report.entity.Report; +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.transaction.Transactional; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import static devkor.com.teamcback.domain.report.entity.ReportStatus.PENDING; +import static devkor.com.teamcback.global.response.ResultCode.NOT_FOUND_REVIEW; +import static devkor.com.teamcback.global.response.ResultCode.NOT_FOUND_USER; + +@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); + reportRepository.save(report); + + return new CreateReviewReportRes(); + } + + /** + * 사용자 찾기 + */ + 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)); + } +} 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 c415a49..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,7 @@ public class Review extends BaseEntity { @Column(nullable = false, length = 500) private String comment = ""; // 500자까지 작성 가능 + @Setter @Column(nullable = false) private boolean isReported = false; From 212f33453539a98daed24e12e86acd47d8e187a3 Mon Sep 17 00:00:00 2001 From: yejin Date: Tue, 10 Feb 2026 16:01:11 +0900 Subject: [PATCH 3/6] =?UTF-8?q?Feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=8B=A0=EA=B3=A0=EB=90=9C=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80,=20=EB=82=B4=EC=97=AD=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/controller/ReportController.java | 16 +++++++ .../dto/response/GetReportedReviewRes.java | 29 ++++++++++++ .../GetUserReviewReportStatusRes.java | 21 +++++++++ .../domain/report/entity/Report.java | 9 +++- .../repository/CustomReportRepository.java | 12 +++++ .../CustomReportRepositoryImpl.java | 47 +++++++++++++++++++ .../report/repository/ReportRepository.java | 3 +- .../domain/report/service/ReportService.java | 34 +++++++++++++- .../global/security/SecurityConfig.java | 1 + 9 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 src/main/java/devkor/com/teamcback/domain/report/dto/response/GetReportedReviewRes.java create mode 100644 src/main/java/devkor/com/teamcback/domain/report/dto/response/GetUserReviewReportStatusRes.java create mode 100644 src/main/java/devkor/com/teamcback/domain/report/repository/CustomReportRepository.java create mode 100644 src/main/java/devkor/com/teamcback/domain/report/repository/CustomReportRepositoryImpl.java 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 index 33b4ff9..0e69c2c 100644 --- a/src/main/java/devkor/com/teamcback/domain/report/controller/ReportController.java +++ b/src/main/java/devkor/com/teamcback/domain/report/controller/ReportController.java @@ -2,6 +2,8 @@ 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.entity.TargetType; import devkor.com.teamcback.domain.report.service.ReportService; import devkor.com.teamcback.global.response.CommonResponse; import devkor.com.teamcback.global.security.UserDetailsImpl; @@ -38,4 +40,18 @@ public CommonResponse createReviewReport( 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))), + }) + @PostMapping(value = "/status") + public CommonResponse searchUserReviewReportStatus( + @Parameter(description = "사용자정보", required = true) @AuthenticationPrincipal UserDetailsImpl userDetail) + { + return CommonResponse.success(reportService.searchUserReviewReportStatus(userDetail.getUser().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/entity/Report.java b/src/main/java/devkor/com/teamcback/domain/report/entity/Report.java index 0adfbee..f3d6e7b 100644 --- a/src/main/java/devkor/com/teamcback/domain/report/entity/Report.java +++ b/src/main/java/devkor/com/teamcback/domain/report/entity/Report.java @@ -39,15 +39,20 @@ public class Report extends BaseEntity { private LocalDate effectiveAt; // 신고 정지 시작일 @ManyToOne - @JoinColumn(name = "user_id") + @JoinColumn(name = "reporter_id") private User reporter; - public Report(TargetType targetType, Long targetId, ReasonCategory reasonCategory, String content, ReportStatus status, 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/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 index 2d304ba..5482956 100644 --- a/src/main/java/devkor/com/teamcback/domain/report/repository/ReportRepository.java +++ b/src/main/java/devkor/com/teamcback/domain/report/repository/ReportRepository.java @@ -3,5 +3,6 @@ import devkor.com.teamcback.domain.report.entity.Report; import org.springframework.data.jpa.repository.JpaRepository; -public interface ReportRepository extends JpaRepository { +public interface ReportRepository extends JpaRepository, CustomReportRepository { + } 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 index 77af7ac..18532b0 100644 --- a/src/main/java/devkor/com/teamcback/domain/report/service/ReportService.java +++ b/src/main/java/devkor/com/teamcback/domain/report/service/ReportService.java @@ -2,6 +2,8 @@ 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.GetReportedReviewRes; +import devkor.com.teamcback.domain.report.dto.response.GetUserReviewReportStatusRes; import devkor.com.teamcback.domain.report.entity.Report; import devkor.com.teamcback.domain.report.entity.TargetType; import devkor.com.teamcback.domain.report.repository.ReportRepository; @@ -10,12 +12,16 @@ 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.transaction.Transactional; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; import static devkor.com.teamcback.domain.report.entity.ReportStatus.PENDING; +import static devkor.com.teamcback.domain.report.entity.ReportStatus.RESOLVED; import static devkor.com.teamcback.global.response.ResultCode.NOT_FOUND_REVIEW; import static devkor.com.teamcback.global.response.ResultCode.NOT_FOUND_USER; @@ -27,6 +33,9 @@ public class ReportService { private final ReviewRepository reviewRepository; private final UserRepository userRepository; + /** + * 리뷰에 대한 신고 작성 + */ @Transactional public CreateReviewReportRes createReviewReport(Long userId, Long reviewId, @Valid CreateReviewReportReq req) { // 신고한 사용자(비회원 가능) @@ -39,12 +48,33 @@ public CreateReviewReportRes createReviewReport(Long userId, Long reviewId, @Val // review.setReported(true); // 신고 저장 - Report report = new Report(TargetType.REVIEW, reviewId, req.getReasonCategory(), req.getContent(), PENDING, user); + 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 searchUserReviewReportStatus(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); + } + /** * 사용자 찾기 */ 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()) // 인가 실패 시 From 2a1a45adba9c8e5db19c6b35eb8c5a44142dcb90 Mon Sep 17 00:00:00 2001 From: yejin Date: Tue, 10 Feb 2026 16:28:36 +0900 Subject: [PATCH 4/6] =?UTF-8?q?Feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EC=8B=A0=EA=B3=A0=20=EB=82=B4=EC=97=AD=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminReportController.java | 35 +++++++++++++++++++ .../report/controller/ReportController.java | 7 ++-- .../report/dto/response/GetReportListRes.java | 14 ++++++++ .../report/dto/response/GetReportRes.java | 31 ++++++++++++++++ .../report/repository/ReportRepository.java | 6 ++++ .../domain/report/service/ReportService.java | 25 ++++++++++--- 6 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 src/main/java/devkor/com/teamcback/domain/report/controller/AdminReportController.java create mode 100644 src/main/java/devkor/com/teamcback/domain/report/dto/response/GetReportListRes.java create mode 100644 src/main/java/devkor/com/teamcback/domain/report/dto/response/GetReportRes.java 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..dc43a27 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/report/controller/AdminReportController.java @@ -0,0 +1,35 @@ +package devkor.com.teamcback.domain.report.controller; + +import devkor.com.teamcback.domain.report.dto.response.GetReportListRes; +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)); + } +} 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 index 0e69c2c..dd2b793 100644 --- a/src/main/java/devkor/com/teamcback/domain/report/controller/ReportController.java +++ b/src/main/java/devkor/com/teamcback/domain/report/controller/ReportController.java @@ -3,7 +3,6 @@ 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.entity.TargetType; import devkor.com.teamcback.domain.report.service.ReportService; import devkor.com.teamcback.global.response.CommonResponse; import devkor.com.teamcback.global.security.UserDetailsImpl; @@ -48,10 +47,10 @@ public CommonResponse createReviewReport( @ApiResponse(responseCode = "401", description = "권한이 없습니다.", content = @Content(schema = @Schema(implementation = CommonResponse.class))), }) - @PostMapping(value = "/status") - public CommonResponse searchUserReviewReportStatus( + @GetMapping(value = "/status") + public CommonResponse getUserReviewReportStatus( @Parameter(description = "사용자정보", required = true) @AuthenticationPrincipal UserDetailsImpl userDetail) { - return CommonResponse.success(reportService.searchUserReviewReportStatus(userDetail.getUser().getUserId())); + return CommonResponse.success(reportService.getUserReviewReportStatus(userDetail.getUser().getUserId())); } } 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/repository/ReportRepository.java b/src/main/java/devkor/com/teamcback/domain/report/repository/ReportRepository.java index 5482956..427d679 100644 --- a/src/main/java/devkor/com/teamcback/domain/report/repository/ReportRepository.java +++ b/src/main/java/devkor/com/teamcback/domain/report/repository/ReportRepository.java @@ -1,8 +1,14 @@ 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(); } 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 index 18532b0..6b088f2 100644 --- a/src/main/java/devkor/com/teamcback/domain/report/service/ReportService.java +++ b/src/main/java/devkor/com/teamcback/domain/report/service/ReportService.java @@ -1,10 +1,9 @@ package devkor.com.teamcback.domain.report.service; 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.GetReportedReviewRes; -import devkor.com.teamcback.domain.report.dto.response.GetUserReviewReportStatusRes; +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; @@ -58,7 +57,7 @@ public CreateReviewReportRes createReviewReport(Long userId, Long reviewId, @Val * 사용자가 신고되었는지 확인 */ @Transactional(readOnly = true) - public GetUserReviewReportStatusRes searchUserReviewReportStatus(Long userId) { + public GetUserReviewReportStatusRes getUserReviewReportStatus(Long userId) { // 사용자 검색 User user = findUser(userId); @@ -75,6 +74,24 @@ public GetUserReviewReportStatusRes searchUserReviewReportStatus(Long userId) { 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()); + } + /** * 사용자 찾기 */ From aefe1b1e81911dd8bd0dffaf76315d72ecfccb03 Mon Sep 17 00:00:00 2001 From: yejin Date: Tue, 10 Feb 2026 16:40:19 +0900 Subject: [PATCH 5/6] =?UTF-8?q?Feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EC=8B=A0=EA=B3=A0=20=EC=83=81=ED=83=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminReportController.java | 17 ++++++++ .../dto/request/UpdateReportStatusReq.java | 15 +++++++ .../dto/response/UpdateReportStatusRes.java | 9 +++++ .../domain/report/entity/Report.java | 3 ++ .../domain/report/service/ReportService.java | 39 ++++++++++++++++++- .../teamcback/global/response/ResultCode.java | 6 ++- 6 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 src/main/java/devkor/com/teamcback/domain/report/dto/request/UpdateReportStatusReq.java create mode 100644 src/main/java/devkor/com/teamcback/domain/report/dto/response/UpdateReportStatusRes.java 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 index dc43a27..091bf2b 100644 --- a/src/main/java/devkor/com/teamcback/domain/report/controller/AdminReportController.java +++ b/src/main/java/devkor/com/teamcback/domain/report/controller/AdminReportController.java @@ -1,6 +1,8 @@ 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; @@ -32,4 +34,19 @@ public CommonResponse getReportList( ) { 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/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/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/Report.java b/src/main/java/devkor/com/teamcback/domain/report/entity/Report.java index f3d6e7b..6d0f0e0 100644 --- a/src/main/java/devkor/com/teamcback/domain/report/entity/Report.java +++ b/src/main/java/devkor/com/teamcback/domain/report/entity/Report.java @@ -5,6 +5,7 @@ import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import java.time.LocalDate; @@ -31,10 +32,12 @@ public class Report extends BaseEntity { @Column(length = 300) private String content; + @Setter @Column(nullable = false) @Enumerated(EnumType.STRING) private ReportStatus status; + @Setter @Column private LocalDate effectiveAt; // 신고 정지 시작일 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 index 6b088f2..d8b6304 100644 --- a/src/main/java/devkor/com/teamcback/domain/report/service/ReportService.java +++ b/src/main/java/devkor/com/teamcback/domain/report/service/ReportService.java @@ -1,6 +1,7 @@ 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; @@ -16,13 +17,13 @@ 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.PENDING; import static devkor.com.teamcback.domain.report.entity.ReportStatus.RESOLVED; -import static devkor.com.teamcback.global.response.ResultCode.NOT_FOUND_REVIEW; -import static devkor.com.teamcback.global.response.ResultCode.NOT_FOUND_USER; +import static devkor.com.teamcback.global.response.ResultCode.*; @Service @RequiredArgsConstructor @@ -92,6 +93,33 @@ public GetReportListRes getReportList(ReportStatus 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(); + } + /** * 사용자 찾기 */ @@ -105,4 +133,11 @@ private User findUser(Long userId) { 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/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; From f7c09be28f70d196d57b19d1f1e1f308a2f71a1e Mon Sep 17 00:00:00 2001 From: yejin Date: Tue, 10 Feb 2026 16:57:00 +0900 Subject: [PATCH 6/6] =?UTF-8?q?Feat:=20=EC=8B=A0=EA=B3=A0=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=9D=BC=20=EA=B2=80=EC=82=AC=20=EC=8A=A4=EC=BC=80?= =?UTF-8?q?=EC=A4=84=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/repository/ReportRepository.java | 2 + .../report/scheduler/ReportScheduler.java | 41 +++++++++++++++++++ .../domain/report/service/ReportService.java | 21 +++++++++- 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 src/main/java/devkor/com/teamcback/domain/report/scheduler/ReportScheduler.java 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 index 427d679..6dc63f4 100644 --- a/src/main/java/devkor/com/teamcback/domain/report/repository/ReportRepository.java +++ b/src/main/java/devkor/com/teamcback/domain/report/repository/ReportRepository.java @@ -11,4 +11,6 @@ public interface ReportRepository extends JpaRepository, CustomRep 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 index d8b6304..46950ff 100644 --- a/src/main/java/devkor/com/teamcback/domain/report/service/ReportService.java +++ b/src/main/java/devkor/com/teamcback/domain/report/service/ReportService.java @@ -21,8 +21,7 @@ import java.util.ArrayList; import java.util.List; -import static devkor.com.teamcback.domain.report.entity.ReportStatus.PENDING; -import static devkor.com.teamcback.domain.report.entity.ReportStatus.RESOLVED; +import static devkor.com.teamcback.domain.report.entity.ReportStatus.*; import static devkor.com.teamcback.global.response.ResultCode.*; @Service @@ -120,6 +119,24 @@ public UpdateReportStatusRes updateReportStatus(Long reportId, UpdateReportStatu 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); + } + } + } + /** * 사용자 찾기 */