From c81edb8d88ac9771501116a93ed9942d68cc0e19 Mon Sep 17 00:00:00 2001 From: tkdtn4657 Date: Sun, 15 Dec 2024 17:47:28 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat=20:=20=EC=8B=A0=EA=B3=A0=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=83=9D=EC=84=B1,=20=EC=82=AD=EC=A0=9C,=20?= =?UTF-8?q?=EB=8B=A8=EA=B1=B4=20=EC=A1=B0=ED=9A=8C,=20=EB=8B=A4=EC=A4=91?= =?UTF-8?q?=EA=B1=B4=20=EC=A1=B0=ED=9A=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../picnee/travel/api/ReportController.java | 57 +++++++++++++ .../com/picnee/travel/api/in/ReportApi.java | 8 ++ .../report/dto/req/CreateReportReq.java | 44 ++++++++++ .../domain/report/dto/res/FindReportRes.java | 51 ++++++++++++ .../domain/report/entity/ReportType.java | 4 +- .../report/repository/ReportRepository.java | 11 +++ .../repository/ReportRepositoryCustom.java | 11 +++ .../repository/ReportRepositoryImpl.java | 40 +++++++++ .../domain/report/service/ReportService.java | 83 +++++++++++++++++++ .../user/dto/req/AuthenticatedUserReq.java | 4 + 10 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/picnee/travel/api/ReportController.java create mode 100644 src/main/java/com/picnee/travel/api/in/ReportApi.java create mode 100644 src/main/java/com/picnee/travel/domain/report/dto/req/CreateReportReq.java create mode 100644 src/main/java/com/picnee/travel/domain/report/dto/res/FindReportRes.java create mode 100644 src/main/java/com/picnee/travel/domain/report/repository/ReportRepository.java create mode 100644 src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryCustom.java create mode 100644 src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java create mode 100644 src/main/java/com/picnee/travel/domain/report/service/ReportService.java diff --git a/src/main/java/com/picnee/travel/api/ReportController.java b/src/main/java/com/picnee/travel/api/ReportController.java new file mode 100644 index 0000000..98185d2 --- /dev/null +++ b/src/main/java/com/picnee/travel/api/ReportController.java @@ -0,0 +1,57 @@ +package com.picnee.travel.api; + +import com.picnee.travel.domain.report.dto.req.CreateReportReq; +import com.picnee.travel.domain.report.dto.res.FindReportRes; +import com.picnee.travel.domain.report.service.ReportService; +import com.picnee.travel.domain.user.dto.req.AuthenticatedUserReq; +import com.picnee.travel.global.security.annotation.AuthenticatedUser; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.OK; + +@Slf4j +@RestController +@RequestMapping("/reports") +@RequiredArgsConstructor +public class ReportController { + + private final ReportService reportService; + + @PostMapping + public ResponseEntity createReport(@RequestBody CreateReportReq dto, + @AuthenticatedUser AuthenticatedUserReq auth) { + + return ResponseEntity.status(CREATED).body(reportService.create(dto, auth).getId().toString()); + } + + @DeleteMapping("{reportId}") + public ResponseEntity deleteReport(@PathVariable("reportId") UUID reportId, + @AuthenticatedUser AuthenticatedUserReq auth) { + reportService.delete(reportId, auth); + return ResponseEntity.status(OK).build(); + } + + @GetMapping("{reportId}") + public ResponseEntity findReport(@PathVariable("reportId") UUID reportId, + @AuthenticatedUser AuthenticatedUserReq auth) { + FindReportRes findReportRes = reportService.find(reportId, auth); + return ResponseEntity.status(OK).body(findReportRes); + } + + @GetMapping + public ResponseEntity> findReports(@AuthenticatedUser AuthenticatedUserReq auth, + @RequestParam(name = "page", defaultValue = "0") int page) { + Page reports = reportService.findReports(auth, page); + return ResponseEntity.ok().body(reports); + } + + + +} diff --git a/src/main/java/com/picnee/travel/api/in/ReportApi.java b/src/main/java/com/picnee/travel/api/in/ReportApi.java new file mode 100644 index 0000000..505322e --- /dev/null +++ b/src/main/java/com/picnee/travel/api/in/ReportApi.java @@ -0,0 +1,8 @@ +package com.picnee.travel.api.in; + +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "report", description = "report API") +public interface ReportApi { + +} diff --git a/src/main/java/com/picnee/travel/domain/report/dto/req/CreateReportReq.java b/src/main/java/com/picnee/travel/domain/report/dto/req/CreateReportReq.java new file mode 100644 index 0000000..e568a5c --- /dev/null +++ b/src/main/java/com/picnee/travel/domain/report/dto/req/CreateReportReq.java @@ -0,0 +1,44 @@ +package com.picnee.travel.domain.report.dto.req; + +import com.picnee.travel.domain.report.entity.Report; +import com.picnee.travel.domain.report.entity.ReportTargetType; +import com.picnee.travel.domain.report.entity.ReportType; +import com.picnee.travel.domain.user.entity.User; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +import static lombok.AccessLevel.PROTECTED; + +@Getter +@Builder +@NoArgsConstructor(access = PROTECTED) +@AllArgsConstructor +public class CreateReportReq { + + @NotNull(message = "targetId는 필수입니다.") + private UUID targetId; + + @NotNull + private ReportTargetType reportTargetType; + + @NotNull + private ReportType reportType; + + @NotNull + private Boolean isVisible; + + public Report toEntity(CreateReportReq dto, User user) { + return Report.builder() + .targetId(dto.getTargetId()) + .reportTargetType(dto.getReportTargetType()) + .reportType(dto.getReportType()) + .isVisible(getIsVisible()) + .user(user) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/picnee/travel/domain/report/dto/res/FindReportRes.java b/src/main/java/com/picnee/travel/domain/report/dto/res/FindReportRes.java new file mode 100644 index 0000000..d848b82 --- /dev/null +++ b/src/main/java/com/picnee/travel/domain/report/dto/res/FindReportRes.java @@ -0,0 +1,51 @@ +package com.picnee.travel.domain.report.dto.res; + +import com.picnee.travel.domain.report.entity.Report; +import com.picnee.travel.domain.report.entity.ReportTargetType; +import com.picnee.travel.domain.report.entity.ReportType; +import com.picnee.travel.domain.user.entity.User; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +import java.util.List; +import java.util.UUID; + +import static lombok.AccessLevel.PROTECTED; + +@Getter +@Builder +@NoArgsConstructor(access = PROTECTED) +@AllArgsConstructor +public class FindReportRes { + + private UUID reportId; + private UUID targetId; + private ReportTargetType reportTargetType; + private ReportType reportType; + private Boolean isVisible; + private User user; + + public static FindReportRes from(Report report){ + return FindReportRes.builder() + .reportId(report.getId()) + .targetId(report.getTargetId()) + .reportTargetType(report.getReportTargetType()) + .reportType(report.getReportType()) + .isVisible(report.getIsVisible()) + .user(report.getUser()) + .build(); + } + + public static Page paging(Page reports) { + List reportResList = reports.stream() + .map(FindReportRes::from) + .toList(); + + return new PageImpl<>(reportResList, reports.getPageable(), reports.getTotalElements()); + } +} diff --git a/src/main/java/com/picnee/travel/domain/report/entity/ReportType.java b/src/main/java/com/picnee/travel/domain/report/entity/ReportType.java index bf53a40..b95d7b8 100644 --- a/src/main/java/com/picnee/travel/domain/report/entity/ReportType.java +++ b/src/main/java/com/picnee/travel/domain/report/entity/ReportType.java @@ -1,6 +1,6 @@ package com.picnee.travel.domain.report.entity; public enum ReportType { - ADVERTISEMENT, - SWEAR + ADVERTISEMENT, // 스팸 + SWEAR // 욕설 } diff --git a/src/main/java/com/picnee/travel/domain/report/repository/ReportRepository.java b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepository.java new file mode 100644 index 0000000..d99acdc --- /dev/null +++ b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepository.java @@ -0,0 +1,11 @@ +package com.picnee.travel.domain.report.repository; + +import com.picnee.travel.domain.report.entity.Report; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +public interface ReportRepository extends JpaRepository, ReportRepositoryCustom { +} diff --git a/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryCustom.java b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryCustom.java new file mode 100644 index 0000000..6e5ec52 --- /dev/null +++ b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryCustom.java @@ -0,0 +1,11 @@ +package com.picnee.travel.domain.report.repository; + +import com.picnee.travel.domain.report.entity.Report; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface ReportRepositoryCustom { + + Page findReports(Pageable pageable); + +} diff --git a/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java new file mode 100644 index 0000000..8e48a4a --- /dev/null +++ b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java @@ -0,0 +1,40 @@ +package com.picnee.travel.domain.report.repository; + +import com.picnee.travel.domain.report.entity.QReport; +import com.picnee.travel.domain.report.entity.Report; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +public class ReportRepositoryImpl implements ReportRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public Page findReports(Pageable pageable) { + QReport report = QReport.report; + + JPAQuery query = jpaQueryFactory + .selectFrom(report); + + List reports = query.offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = Optional.ofNullable(jpaQueryFactory + .select(report.count()) + .from(report).fetchOne()) + .orElse(0L); + + return new PageImpl<>(reports, pageable, total); + } +} diff --git a/src/main/java/com/picnee/travel/domain/report/service/ReportService.java b/src/main/java/com/picnee/travel/domain/report/service/ReportService.java new file mode 100644 index 0000000..0a113f7 --- /dev/null +++ b/src/main/java/com/picnee/travel/domain/report/service/ReportService.java @@ -0,0 +1,83 @@ +package com.picnee.travel.domain.report.service; + +import com.picnee.travel.domain.report.dto.req.CreateReportReq; +import com.picnee.travel.domain.report.dto.res.FindReportRes; +import com.picnee.travel.domain.report.entity.Report; +import com.picnee.travel.domain.report.repository.ReportRepository; +import com.picnee.travel.domain.user.dto.req.AuthenticatedUserReq; +import com.picnee.travel.domain.user.entity.User; +import com.picnee.travel.domain.user.service.UserService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + + +@Slf4j +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ReportService { + + private final ReportRepository reportRepository; + private final UserService userService; + + /** + * 신고 생성 + */ + @Transactional + public Report create(CreateReportReq dto, AuthenticatedUserReq auth) { + User user = userService.findByEmail(auth.getEmail()); + + return reportRepository.save(dto.toEntity(dto, user)); + } + + /** + * 신고 삭제 + * 권한 : 어드민 + */ + @Transactional + public void delete(UUID reportId, AuthenticatedUserReq auth) { + validateAdmin(auth); + + reportRepository.deleteById(reportId); + } + + /** + * 신고 단건 조회 + */ + public FindReportRes find(UUID reportId, AuthenticatedUserReq auth) { + validateAdmin(auth); + Report report = reportRepository.findById(reportId).orElseThrow(() -> new IllegalArgumentException("신고 건이 존재하지않습니다.")); + + return FindReportRes.from(report); + } + + /** + * 신고 전체 조회 + * 권한 : 어드민 + */ + public Page findReports(AuthenticatedUserReq auth, int page) { + validateAdmin(auth); + + Pageable pageable = PageRequest.of(page, 10); + Page reports = reportRepository.findReports(pageable); + + return FindReportRes.paging(reports); + } + + /** + * 어드민 권한 확인 + */ + private void validateAdmin(AuthenticatedUserReq auth) { + if (!auth.isAdmin()) { + throw new IllegalArgumentException("어드민 권한이 아닙니다"); + } + } + +} diff --git a/src/main/java/com/picnee/travel/domain/user/dto/req/AuthenticatedUserReq.java b/src/main/java/com/picnee/travel/domain/user/dto/req/AuthenticatedUserReq.java index 148c30b..ee99583 100644 --- a/src/main/java/com/picnee/travel/domain/user/dto/req/AuthenticatedUserReq.java +++ b/src/main/java/com/picnee/travel/domain/user/dto/req/AuthenticatedUserReq.java @@ -24,4 +24,8 @@ public static AuthenticatedUserReq of(User user) { .role(user.getRole()) .build(); } + + public boolean isAdmin() { + return role == Role.ADMIN; + } } From 60c0c7f3f31eba97003eb38694bae1bf478b7dab Mon Sep 17 00:00:00 2001 From: tkdtn4657 Date: Sun, 22 Dec 2024 18:39:16 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat=20:=20=EC=8B=A0=EA=B3=A0=20=EC=A0=9C?= =?UTF-8?q?=EC=9E=AC=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../picnee/travel/api/ReportController.java | 23 ++++-- .../domain/post/service/PostService.java | 14 +++- .../service/PostCommentService.java | 9 +++ .../report/dto/req/CreateReportReq.java | 9 +-- .../domain/report/dto/res/FindReportRes.java | 8 ++- .../repository/ReportRepositoryCustom.java | 5 +- .../repository/ReportRepositoryImpl.java | 70 ++++++++++++++++++- .../domain/report/service/ReportService.java | 37 +++++++++- 8 files changed, 155 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/picnee/travel/api/ReportController.java b/src/main/java/com/picnee/travel/api/ReportController.java index 98185d2..970b3a0 100644 --- a/src/main/java/com/picnee/travel/api/ReportController.java +++ b/src/main/java/com/picnee/travel/api/ReportController.java @@ -5,6 +5,7 @@ import com.picnee.travel.domain.report.service.ReportService; import com.picnee.travel.domain.user.dto.req.AuthenticatedUserReq; import com.picnee.travel.global.security.annotation.AuthenticatedUser; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -25,7 +26,7 @@ public class ReportController { private final ReportService reportService; @PostMapping - public ResponseEntity createReport(@RequestBody CreateReportReq dto, + public ResponseEntity createReport(@Valid @RequestBody CreateReportReq dto, @AuthenticatedUser AuthenticatedUserReq auth) { return ResponseEntity.status(CREATED).body(reportService.create(dto, auth).getId().toString()); @@ -46,10 +47,22 @@ public ResponseEntity findReport(@PathVariable("reportId") UUID r } @GetMapping - public ResponseEntity> findReports(@AuthenticatedUser AuthenticatedUserReq auth, - @RequestParam(name = "page", defaultValue = "0") int page) { - Page reports = reportService.findReports(auth, page); - return ResponseEntity.ok().body(reports); + public ResponseEntity> findReports(@RequestParam(name = "targetId", required = false) String targetId, + @RequestParam(name = "reportTargetType", required = false) String reportTargetType, + @RequestParam(name = "reportType", required = false) String reportType, + @RequestParam(name = "is_visible", required = false) String isVisible, + @RequestParam(name = "sort", required = false) String sort, + @RequestParam(name = "page", defaultValue = "0") int page, + @AuthenticatedUser AuthenticatedUserReq auth) { + Page reports = reportService.findReports(auth, targetId, reportTargetType, reportType, isVisible, sort, page); + return ResponseEntity.status(OK).body(reports); + } + + @PatchMapping("/{reportId}") + public ResponseEntity processReport(@PathVariable("reportId") UUID reportTargetId, + @AuthenticatedUser AuthenticatedUserReq auth) { + + return ResponseEntity.status(OK).body(reportService.processReport(reportTargetId, auth).getId().toString()); } diff --git a/src/main/java/com/picnee/travel/domain/post/service/PostService.java b/src/main/java/com/picnee/travel/domain/post/service/PostService.java index 380753c..4481825 100644 --- a/src/main/java/com/picnee/travel/domain/post/service/PostService.java +++ b/src/main/java/com/picnee/travel/domain/post/service/PostService.java @@ -120,7 +120,7 @@ public Page findPosts(String boardCategory, String region, String s * 내가 작성한 게시글 조회 */ public Page getMyPosts(AuthenticatedUserReq auth, int page) { - if(!isUserAuthenticated(auth)) { + if (!isUserAuthenticated(auth)) { throw new NotAuthException(NOT_AUTH_EXCEPTION); } @@ -161,4 +161,16 @@ public Post findById(UUID postId) { return postRepository.findById(postId) .orElseThrow(() -> new NotFoundPostException(NOT_FOUND_POST_EXCEPTION)); } + + /** + * 신고된 댓글 제재 + */ + @Transactional + public void sanction(UUID reportTargetId) { + Post post = postRepository.findById(reportTargetId) + .orElseThrow(() -> new NotFoundPostException(NOT_FOUND_POST_EXCEPTION)); + + post.softDelete(); + boardService.delete(post); + } } diff --git a/src/main/java/com/picnee/travel/domain/postComment/service/PostCommentService.java b/src/main/java/com/picnee/travel/domain/postComment/service/PostCommentService.java index 3f701d4..a637f86 100644 --- a/src/main/java/com/picnee/travel/domain/postComment/service/PostCommentService.java +++ b/src/main/java/com/picnee/travel/domain/postComment/service/PostCommentService.java @@ -183,4 +183,13 @@ public PostComment findById(UUID commentId) { return postCommentRepository.findById(commentId) .orElseThrow(() -> new NotFoundCommentException(NOT_FOUND_COMMENT_EXCEPTION)); } + + /** + * 신고된 댓글 제재 + */ + @Transactional + public void sanction(UUID reportTargetId) { + PostComment postComment = findById(reportTargetId); + postComment.softDelete(); + } } diff --git a/src/main/java/com/picnee/travel/domain/report/dto/req/CreateReportReq.java b/src/main/java/com/picnee/travel/domain/report/dto/req/CreateReportReq.java index e568a5c..75c47ff 100644 --- a/src/main/java/com/picnee/travel/domain/report/dto/req/CreateReportReq.java +++ b/src/main/java/com/picnee/travel/domain/report/dto/req/CreateReportReq.java @@ -23,21 +23,18 @@ public class CreateReportReq { @NotNull(message = "targetId는 필수입니다.") private UUID targetId; - @NotNull + @NotNull(message = "신고 대상은 타입은 필수입니다.(ex : REVIEW)") private ReportTargetType reportTargetType; - @NotNull + @NotNull(message = "신고 유형은 필수입니다.(ex : ADVERTISEMENT)") private ReportType reportType; - @NotNull - private Boolean isVisible; - public Report toEntity(CreateReportReq dto, User user) { return Report.builder() .targetId(dto.getTargetId()) .reportTargetType(dto.getReportTargetType()) .reportType(dto.getReportType()) - .isVisible(getIsVisible()) + .isVisible(false) .user(user) .build(); } diff --git a/src/main/java/com/picnee/travel/domain/report/dto/res/FindReportRes.java b/src/main/java/com/picnee/travel/domain/report/dto/res/FindReportRes.java index d848b82..2cb0585 100644 --- a/src/main/java/com/picnee/travel/domain/report/dto/res/FindReportRes.java +++ b/src/main/java/com/picnee/travel/domain/report/dto/res/FindReportRes.java @@ -4,7 +4,6 @@ import com.picnee.travel.domain.report.entity.ReportTargetType; import com.picnee.travel.domain.report.entity.ReportType; import com.picnee.travel.domain.user.entity.User; -import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -12,6 +11,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; +import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -28,7 +28,8 @@ public class FindReportRes { private ReportTargetType reportTargetType; private ReportType reportType; private Boolean isVisible; - private User user; + private LocalDateTime createAt; + private UUID userId; public static FindReportRes from(Report report){ return FindReportRes.builder() @@ -37,7 +38,8 @@ public static FindReportRes from(Report report){ .reportTargetType(report.getReportTargetType()) .reportType(report.getReportType()) .isVisible(report.getIsVisible()) - .user(report.getUser()) + .createAt(report.getCreatedAt()) + .userId(report.getUser().getId()) .build(); } diff --git a/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryCustom.java b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryCustom.java index 6e5ec52..6eadbed 100644 --- a/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryCustom.java +++ b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryCustom.java @@ -4,8 +4,11 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import java.util.UUID; + public interface ReportRepositoryCustom { - Page findReports(Pageable pageable); + Page findReports(String targetId, String reportTargetType, String reportType, String isVisible, String sort, Pageable pageable); + Report processReport(UUID reportTargetId); } diff --git a/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java index 8e48a4a..d98b897 100644 --- a/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java +++ b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java @@ -2,16 +2,26 @@ import com.picnee.travel.domain.report.entity.QReport; import com.picnee.travel.domain.report.entity.Report; +import com.picnee.travel.domain.report.entity.ReportTargetType; +import com.picnee.travel.domain.report.entity.ReportType; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.PathBuilder; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import java.util.Optional; + +import org.hibernate.tool.schema.TargetType; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import java.util.List; +import java.util.UUID; @Slf4j @RequiredArgsConstructor @@ -20,11 +30,15 @@ public class ReportRepositoryImpl implements ReportRepositoryCustom { private final JPAQueryFactory jpaQueryFactory; @Override - public Page findReports(Pageable pageable) { + public Page findReports(String targetId, String reportTargetType, String reportType, String isVisible, String sort, Pageable pageable) { QReport report = QReport.report; + // 조건 빌딩 메서드 호출 + BooleanBuilder builder = buildCondition(report, targetId, reportTargetType, reportType, isVisible); + JPAQuery query = jpaQueryFactory - .selectFrom(report); + .selectFrom(report) + .where(builder); List reports = query.offset(pageable.getOffset()) .limit(pageable.getPageSize()) @@ -37,4 +51,56 @@ public Page findReports(Pageable pageable) { return new PageImpl<>(reports, pageable, total); } + + @Override + public Report processReport(UUID reportTargetId) { + QReport report = QReport.report; + + // 신고하기 위한 targetId가 같은 reportId 목록을 선 조회 + List reportIds = jpaQueryFactory + .select(report.id) + .from(report) + .where(report.targetId.eq(reportTargetId)) + .fetch(); + + // isVisible 업데이트 처리 + if (!reportIds.isEmpty()) { + jpaQueryFactory + .update(report) + .set(report.isVisible, true) + .where(report.id.in(reportIds)) + .execute(); + } + + return null; + } + + // 조건 빌딩 + private BooleanBuilder buildCondition(QReport report, String targetId, String reportTargetType, String reportType, String isVisible) { + BooleanBuilder builder = new BooleanBuilder(); + + // 기본 조건 추가 + if ("true".equalsIgnoreCase(isVisible)) { + builder.and(report.isVisible.isTrue()); + } else { + builder.and(report.isVisible.isFalse()); + } + + // 조건별 동적 빌딩 + if (targetId != null) { + builder.and(report.targetId.eq(UUID.fromString(targetId))); + } + if (reportTargetType != null) { + try { + builder.and(report.reportTargetType.eq(ReportTargetType.valueOf(reportTargetType))); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid reportTargetType: " + reportTargetType); + } + } + if (reportType != null) { + builder.and(report.reportType.eq(ReportType.valueOf(reportType))); + } + + return builder; + } } diff --git a/src/main/java/com/picnee/travel/domain/report/service/ReportService.java b/src/main/java/com/picnee/travel/domain/report/service/ReportService.java index 0a113f7..6278303 100644 --- a/src/main/java/com/picnee/travel/domain/report/service/ReportService.java +++ b/src/main/java/com/picnee/travel/domain/report/service/ReportService.java @@ -1,9 +1,13 @@ package com.picnee.travel.domain.report.service; +import com.picnee.travel.domain.post.service.PostService; +import com.picnee.travel.domain.postComment.service.PostCommentService; import com.picnee.travel.domain.report.dto.req.CreateReportReq; import com.picnee.travel.domain.report.dto.res.FindReportRes; import com.picnee.travel.domain.report.entity.Report; +import com.picnee.travel.domain.report.entity.ReportTargetType; import com.picnee.travel.domain.report.repository.ReportRepository; +import com.picnee.travel.domain.review.service.ReviewService; import com.picnee.travel.domain.user.dto.req.AuthenticatedUserReq; import com.picnee.travel.domain.user.entity.User; import com.picnee.travel.domain.user.service.UserService; @@ -26,6 +30,9 @@ public class ReportService { private final ReportRepository reportRepository; private final UserService userService; + private final PostService postService; + private final PostCommentService postCommentService; + private final ReviewService reviewService; /** * 신고 생성 @@ -62,15 +69,41 @@ public FindReportRes find(UUID reportId, AuthenticatedUserReq auth) { * 신고 전체 조회 * 권한 : 어드민 */ - public Page findReports(AuthenticatedUserReq auth, int page) { + public Page findReports(AuthenticatedUserReq auth, String targetId, String reportTargetType, String reportType, String isVisible, String sort, int page) { validateAdmin(auth); Pageable pageable = PageRequest.of(page, 10); - Page reports = reportRepository.findReports(pageable); + Page reports = reportRepository.findReports(targetId, reportTargetType, reportType, isVisible, sort, pageable); return FindReportRes.paging(reports); } + /** + * 신고 처리 + * 권한 : 어드민 + */ + @Transactional + public Report processReport(UUID reportTargetId, AuthenticatedUserReq auth) { + validateAdmin(auth); + + Report report = reportRepository.processReport(reportTargetId); + + switch (report.getReportTargetType()) { + case REVIEW : + //TODO : 리뷰 제재는 개발완료 후 작성 +// reviewService.sanction(reportTargetId); + break; + case POST : + postService.sanction(reportTargetId); + break; + case COMMENT: + postCommentService.sanction(reportTargetId); + break; + } + + return report; + } + /** * 어드민 권한 확인 */ From 9832026da828e44d769a6031d2a8bbf22e510b3a Mon Sep 17 00:00:00 2001 From: tkdtn4657 Date: Tue, 24 Dec 2024 00:59:33 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix=20:=20=EC=8B=A0=EA=B3=A0=20targetId?= =?UTF-8?q?=EA=B0=80=20=EB=9E=9C=EB=8D=A4=EC=9C=BC=EB=A1=9C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EB=90=98=EB=8A=94=20=EC=97=90=EB=9F=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=EC=A0=95=EB=A0=AC=EC=A1=B0=EA=B1=B4=20sort=20asc,?= =?UTF-8?q?=20desc=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../picnee/travel/api/ReportController.java | 8 +++---- .../travel/domain/report/entity/Report.java | 1 - .../repository/ReportRepositoryImpl.java | 22 ++++++++++++++++++- .../domain/report/service/ReportService.java | 6 ++--- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/picnee/travel/api/ReportController.java b/src/main/java/com/picnee/travel/api/ReportController.java index 970b3a0..f46c910 100644 --- a/src/main/java/com/picnee/travel/api/ReportController.java +++ b/src/main/java/com/picnee/travel/api/ReportController.java @@ -32,14 +32,14 @@ public ResponseEntity createReport(@Valid @RequestBody CreateReportReq d return ResponseEntity.status(CREATED).body(reportService.create(dto, auth).getId().toString()); } - @DeleteMapping("{reportId}") + @DeleteMapping("/{reportId}") public ResponseEntity deleteReport(@PathVariable("reportId") UUID reportId, @AuthenticatedUser AuthenticatedUserReq auth) { reportService.delete(reportId, auth); return ResponseEntity.status(OK).build(); } - @GetMapping("{reportId}") + @GetMapping("/{reportId}") public ResponseEntity findReport(@PathVariable("reportId") UUID reportId, @AuthenticatedUser AuthenticatedUserReq auth) { FindReportRes findReportRes = reportService.find(reportId, auth); @@ -61,8 +61,8 @@ public ResponseEntity> findReports(@RequestParam(name = "tar @PatchMapping("/{reportId}") public ResponseEntity processReport(@PathVariable("reportId") UUID reportTargetId, @AuthenticatedUser AuthenticatedUserReq auth) { - - return ResponseEntity.status(OK).body(reportService.processReport(reportTargetId, auth).getId().toString()); + reportService.processReport(reportTargetId, auth); + return ResponseEntity.status(OK).build(); } diff --git a/src/main/java/com/picnee/travel/domain/report/entity/Report.java b/src/main/java/com/picnee/travel/domain/report/entity/Report.java index 9438b26..3818a15 100644 --- a/src/main/java/com/picnee/travel/domain/report/entity/Report.java +++ b/src/main/java/com/picnee/travel/domain/report/entity/Report.java @@ -33,7 +33,6 @@ public class Report extends BaseEntity { @JdbcTypeCode(SqlTypes.VARCHAR) @Column(name = "report_id", columnDefinition = "VARCHAR(36)") private UUID id; - @UuidGenerator(style = RANDOM) @JdbcTypeCode(SqlTypes.VARCHAR) @Column(name = "target_id", columnDefinition = "VARCHAR(36)") private UUID targetId; diff --git a/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java index d98b897..32c2fb4 100644 --- a/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java +++ b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java @@ -5,6 +5,7 @@ import com.picnee.travel.domain.report.entity.ReportTargetType; import com.picnee.travel.domain.report.entity.ReportType; import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Path; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.PathBuilder; @@ -12,6 +13,8 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; import java.util.Optional; import org.hibernate.tool.schema.TargetType; @@ -40,6 +43,12 @@ public Page findReports(String targetId, String reportTargetType, String .selectFrom(report) .where(builder); + // 정렬 조건 추가 + if (sort != null) { + OrderSpecifier orderSpecifier = getOrderSpecifier(report, sort); + query.orderBy(orderSpecifier); + } + List reports = query.offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); @@ -72,7 +81,7 @@ public Report processReport(UUID reportTargetId) { .execute(); } - return null; + return jpaQueryFactory.selectFrom(report).fetchFirst(); } // 조건 빌딩 @@ -103,4 +112,15 @@ private BooleanBuilder buildCondition(QReport report, String targetId, String re return builder; } + + private OrderSpecifier getOrderSpecifier(QReport report, String sort) { + switch (sort.toLowerCase()) { + case "asc": + return report.createdAt.asc(); + case "desc": + return report.createdAt.desc(); + default: + throw new IllegalArgumentException("Invalid sort order: " + sort); + } + } } diff --git a/src/main/java/com/picnee/travel/domain/report/service/ReportService.java b/src/main/java/com/picnee/travel/domain/report/service/ReportService.java index 6278303..3b3f40e 100644 --- a/src/main/java/com/picnee/travel/domain/report/service/ReportService.java +++ b/src/main/java/com/picnee/travel/domain/report/service/ReportService.java @@ -5,7 +5,6 @@ import com.picnee.travel.domain.report.dto.req.CreateReportReq; import com.picnee.travel.domain.report.dto.res.FindReportRes; import com.picnee.travel.domain.report.entity.Report; -import com.picnee.travel.domain.report.entity.ReportTargetType; import com.picnee.travel.domain.report.repository.ReportRepository; import com.picnee.travel.domain.review.service.ReviewService; import com.picnee.travel.domain.user.dto.req.AuthenticatedUserReq; @@ -21,7 +20,6 @@ import java.util.UUID; - @Slf4j @Service @Transactional(readOnly = true) @@ -83,7 +81,7 @@ public Page findReports(AuthenticatedUserReq auth, String targetI * 권한 : 어드민 */ @Transactional - public Report processReport(UUID reportTargetId, AuthenticatedUserReq auth) { + public Boolean processReport(UUID reportTargetId, AuthenticatedUserReq auth) { validateAdmin(auth); Report report = reportRepository.processReport(reportTargetId); @@ -101,7 +99,7 @@ public Report processReport(UUID reportTargetId, AuthenticatedUserReq auth) { break; } - return report; + return true; } /** From 49694d7a318c93244bca5c126d54bd7b8e6489d1 Mon Sep 17 00:00:00 2001 From: tkdtn4657 Date: Tue, 24 Dec 2024 01:10:19 +0900 Subject: [PATCH 4/5] =?UTF-8?q?docs=20:=20swagger=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../picnee/travel/api/ReportController.java | 3 ++- .../com/picnee/travel/api/in/ReportApi.java | 25 ++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/picnee/travel/api/ReportController.java b/src/main/java/com/picnee/travel/api/ReportController.java index f46c910..ca8a5f7 100644 --- a/src/main/java/com/picnee/travel/api/ReportController.java +++ b/src/main/java/com/picnee/travel/api/ReportController.java @@ -1,5 +1,6 @@ package com.picnee.travel.api; +import com.picnee.travel.api.in.ReportApi; import com.picnee.travel.domain.report.dto.req.CreateReportReq; import com.picnee.travel.domain.report.dto.res.FindReportRes; import com.picnee.travel.domain.report.service.ReportService; @@ -21,7 +22,7 @@ @RestController @RequestMapping("/reports") @RequiredArgsConstructor -public class ReportController { +public class ReportController implements ReportApi { private final ReportService reportService; diff --git a/src/main/java/com/picnee/travel/api/in/ReportApi.java b/src/main/java/com/picnee/travel/api/in/ReportApi.java index 505322e..b377d00 100644 --- a/src/main/java/com/picnee/travel/api/in/ReportApi.java +++ b/src/main/java/com/picnee/travel/api/in/ReportApi.java @@ -1,8 +1,31 @@ package com.picnee.travel.api.in; +import com.picnee.travel.domain.report.dto.req.CreateReportReq; +import com.picnee.travel.domain.report.dto.res.FindReportRes; +import com.picnee.travel.domain.user.dto.req.AuthenticatedUserReq; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; -@Tag(name = "report", description = "report API") +import java.util.UUID; + +@Tag(name = "reports", description = "report API") public interface ReportApi { + @Operation(summary = "신고", description = "신고를 진행한다") + ResponseEntity createReport(CreateReportReq dto, AuthenticatedUserReq auth); + + @Operation(summary = "신고 삭제(어드민)", description = "신고된 것을 삭제한다") + public ResponseEntity deleteReport(UUID reportId, AuthenticatedUserReq auth); + + @Operation(summary = "신고 단건 조회(어드민)", description = "신고한 내역 단건 조회") + public ResponseEntity findReport(UUID reportId, AuthenticatedUserReq auth); + + @Operation(summary = "신고 다중 건 조회(어드민)", description = "신고한 내역 리스트 조회") + public ResponseEntity> findReports(String targetId, String reportTargetType, String reportType, String isVisible, String sort, int page, AuthenticatedUserReq auth); + + @Operation(summary = "신고 제재(어드민)", description = "신고 제재처리") + public ResponseEntity processReport(UUID reportTargetId, AuthenticatedUserReq auth); + } From b5e202efd280858f27c9e97a1eab5a8b3dd2baaa Mon Sep 17 00:00:00 2001 From: tkdtn4657 Date: Wed, 29 Jan 2025 18:01:16 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat=20:=20=EC=8B=A0=EA=B3=A0=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80,=20=EC=8B=A0?= =?UTF-8?q?=EA=B3=A0=20=EB=88=84=EC=A0=81=20=EC=8B=9C=20blind=EC=B2=98?= =?UTF-8?q?=EB=A6=AC,=20=EC=9C=A0=EC=A0=80=20=EC=8B=A0=EA=B3=A0=EB=88=84?= =?UTF-8?q?=EC=A0=81=20=EC=8B=9C=20=EC=A0=95=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../picnee/travel/api/ReportController.java | 4 +- .../domain/post/dto/req/CreatePostReq.java | 1 + .../travel/domain/post/entity/Post.java | 16 +++++++ .../domain/post/service/PostService.java | 11 +++-- .../dto/req/CreatePostCommentReq.java | 2 + .../postComment/entity/PostComment.java | 23 ++++++++++ .../service/PostCommentService.java | 10 ++++- .../travel/domain/report/entity/Report.java | 4 ++ .../exception/NotFoundReportException.java | 10 +++++ .../repository/ReportRepositoryCustom.java | 2 - .../repository/ReportRepositoryImpl.java | 23 ---------- .../domain/report/service/ReportService.java | 43 +++++++++++-------- .../domain/review/dto/req/BaseReviewReq.java | 1 + .../travel/domain/review/entity/Review.java | 16 +++++++ .../domain/review/service/ReviewService.java | 15 +++++++ .../travel/domain/user/entity/User.java | 14 ++++++ .../user/exception/NotAdminException.java | 10 +++++ .../domain/user/service/UserService.java | 5 +++ .../travel/global/exception/ErrorCode.java | 25 ++++++----- .../travel/global/oauth/OAuthAttributes.java | 1 + src/main/resources/data.sql | 10 +++-- 21 files changed, 183 insertions(+), 63 deletions(-) create mode 100644 src/main/java/com/picnee/travel/domain/report/exception/NotFoundReportException.java create mode 100644 src/main/java/com/picnee/travel/domain/user/exception/NotAdminException.java diff --git a/src/main/java/com/picnee/travel/api/ReportController.java b/src/main/java/com/picnee/travel/api/ReportController.java index ca8a5f7..dd93b2a 100644 --- a/src/main/java/com/picnee/travel/api/ReportController.java +++ b/src/main/java/com/picnee/travel/api/ReportController.java @@ -60,9 +60,9 @@ public ResponseEntity> findReports(@RequestParam(name = "tar } @PatchMapping("/{reportId}") - public ResponseEntity processReport(@PathVariable("reportId") UUID reportTargetId, + public ResponseEntity processReport(@PathVariable("reportId") UUID reportId, @AuthenticatedUser AuthenticatedUserReq auth) { - reportService.processReport(reportTargetId, auth); + reportService.processReport(reportId, auth); return ResponseEntity.status(OK).build(); } diff --git a/src/main/java/com/picnee/travel/domain/post/dto/req/CreatePostReq.java b/src/main/java/com/picnee/travel/domain/post/dto/req/CreatePostReq.java index 2bd417b..76def2a 100644 --- a/src/main/java/com/picnee/travel/domain/post/dto/req/CreatePostReq.java +++ b/src/main/java/com/picnee/travel/domain/post/dto/req/CreatePostReq.java @@ -33,6 +33,7 @@ public static Post toEntity(CreatePostReq dto, Board board, User user) { .title(dto.getTitle()) .content(dto.getContent()) .viewed(0L) + .reportSanctionCount(0) .user(user) .board(board) .build(); diff --git a/src/main/java/com/picnee/travel/domain/post/entity/Post.java b/src/main/java/com/picnee/travel/domain/post/entity/Post.java index e1b8531..cb3d4fd 100644 --- a/src/main/java/com/picnee/travel/domain/post/entity/Post.java +++ b/src/main/java/com/picnee/travel/domain/post/entity/Post.java @@ -43,6 +43,8 @@ public class Post extends SoftDeleteBaseEntity { private String content; @Column(name = "viewed") private Long viewed; + @Column(name = "report_sanction_count") + private Integer reportSanctionCount; @ManyToOne(fetch = LAZY) @JoinColumn(name = "user_id") private User user; @@ -75,4 +77,18 @@ public void softDelete() { public void incrementViewCount() { this.viewed++; } + + /** + * 신고 횟수 누적 + */ + public void reportSanctionCountPlus(){ + this.reportSanctionCount++; + } + + /** + * 게시글 제재 가능여부 확인 + */ + public boolean isRequiringSanctions() { + return this.reportSanctionCount >= 5; + } } diff --git a/src/main/java/com/picnee/travel/domain/post/service/PostService.java b/src/main/java/com/picnee/travel/domain/post/service/PostService.java index 4481825..4f6134d 100644 --- a/src/main/java/com/picnee/travel/domain/post/service/PostService.java +++ b/src/main/java/com/picnee/travel/domain/post/service/PostService.java @@ -166,11 +166,16 @@ public Post findById(UUID postId) { * 신고된 댓글 제재 */ @Transactional - public void sanction(UUID reportTargetId) { + public User sanction(UUID reportTargetId) { Post post = postRepository.findById(reportTargetId) .orElseThrow(() -> new NotFoundPostException(NOT_FOUND_POST_EXCEPTION)); - post.softDelete(); - boardService.delete(post); + post.reportSanctionCountPlus(); + if(post.isRequiringSanctions()){ + post.softDelete(); + boardService.delete(post); + } + + return post.getUser(); } } diff --git a/src/main/java/com/picnee/travel/domain/postComment/dto/req/CreatePostCommentReq.java b/src/main/java/com/picnee/travel/domain/postComment/dto/req/CreatePostCommentReq.java index b2068dd..005e8bb 100644 --- a/src/main/java/com/picnee/travel/domain/postComment/dto/req/CreatePostCommentReq.java +++ b/src/main/java/com/picnee/travel/domain/postComment/dto/req/CreatePostCommentReq.java @@ -27,6 +27,7 @@ public static PostComment toEntity(CreatePostCommentReq dto, User user, Post pos .user(user) .content(dto.content) .likes(0L) + .reportSanctionCount(0) .post(post) .build(); } @@ -36,6 +37,7 @@ public static PostComment toEntityCoComment(Post post, PostComment postComment, .user(user) .content(dto.content) .likes(0L) + .reportSanctionCount(0) .commentParent(postComment) .post(post) .build(); diff --git a/src/main/java/com/picnee/travel/domain/postComment/entity/PostComment.java b/src/main/java/com/picnee/travel/domain/postComment/entity/PostComment.java index 4966a66..24e9aba 100644 --- a/src/main/java/com/picnee/travel/domain/postComment/entity/PostComment.java +++ b/src/main/java/com/picnee/travel/domain/postComment/entity/PostComment.java @@ -42,6 +42,8 @@ public class PostComment extends SoftDeleteBaseEntity { private String content; @Column(name = "likes") private Long likes; + @Column(name = "report_sanction_count") + private Integer reportSanctionCount; @ManyToOne(fetch = LAZY) @JoinColumn(name = "post_comment_parent_id") private PostComment commentParent; @@ -75,4 +77,25 @@ public void addLike() { public void deleteLike() { this.likes--; } + + /** + * 댓글 삭제 + */ + public void softDelete() { + super.delete(); + } + + /** + * 신고 횟수 누적 + */ + public void reportSanctionCountPlus(){ + this.reportSanctionCount++; + } + + /** + * 댓글 제재 가능여부 확인 + */ + public boolean isRequiringSanctions() { + return this.reportSanctionCount >= 5; + } } diff --git a/src/main/java/com/picnee/travel/domain/postComment/service/PostCommentService.java b/src/main/java/com/picnee/travel/domain/postComment/service/PostCommentService.java index 302163e..e573d27 100644 --- a/src/main/java/com/picnee/travel/domain/postComment/service/PostCommentService.java +++ b/src/main/java/com/picnee/travel/domain/postComment/service/PostCommentService.java @@ -189,8 +189,14 @@ public PostComment findById(UUID commentId) { * 신고된 댓글 제재 */ @Transactional - public void sanction(UUID reportTargetId) { + public User sanction(UUID reportTargetId) { PostComment postComment = findById(reportTargetId); - postComment.softDelete(); + + postComment.reportSanctionCountPlus(); + if(postComment.isRequiringSanctions()){ + postComment.softDelete(); + } + + return postComment.getUser(); } } diff --git a/src/main/java/com/picnee/travel/domain/report/entity/Report.java b/src/main/java/com/picnee/travel/domain/report/entity/Report.java index 3818a15..5cfbfb1 100644 --- a/src/main/java/com/picnee/travel/domain/report/entity/Report.java +++ b/src/main/java/com/picnee/travel/domain/report/entity/Report.java @@ -47,4 +47,8 @@ public class Report extends BaseEntity { @ManyToOne(fetch = LAZY) @JoinColumn(name = "user_id") private User user; + + public void softDelete() { + this.isVisible = true; + } } diff --git a/src/main/java/com/picnee/travel/domain/report/exception/NotFoundReportException.java b/src/main/java/com/picnee/travel/domain/report/exception/NotFoundReportException.java new file mode 100644 index 0000000..b2981c4 --- /dev/null +++ b/src/main/java/com/picnee/travel/domain/report/exception/NotFoundReportException.java @@ -0,0 +1,10 @@ +package com.picnee.travel.domain.report.exception; + +import com.picnee.travel.global.exception.BusinessException; +import com.picnee.travel.global.exception.ErrorCode; + +public class NotFoundReportException extends BusinessException { + public NotFoundReportException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryCustom.java b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryCustom.java index 6eadbed..38ee9dd 100644 --- a/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryCustom.java +++ b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryCustom.java @@ -9,6 +9,4 @@ public interface ReportRepositoryCustom { Page findReports(String targetId, String reportTargetType, String reportType, String isVisible, String sort, Pageable pageable); - - Report processReport(UUID reportTargetId); } diff --git a/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java index 32c2fb4..787fadf 100644 --- a/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java +++ b/src/main/java/com/picnee/travel/domain/report/repository/ReportRepositoryImpl.java @@ -61,29 +61,6 @@ public Page findReports(String targetId, String reportTargetType, String return new PageImpl<>(reports, pageable, total); } - @Override - public Report processReport(UUID reportTargetId) { - QReport report = QReport.report; - - // 신고하기 위한 targetId가 같은 reportId 목록을 선 조회 - List reportIds = jpaQueryFactory - .select(report.id) - .from(report) - .where(report.targetId.eq(reportTargetId)) - .fetch(); - - // isVisible 업데이트 처리 - if (!reportIds.isEmpty()) { - jpaQueryFactory - .update(report) - .set(report.isVisible, true) - .where(report.id.in(reportIds)) - .execute(); - } - - return jpaQueryFactory.selectFrom(report).fetchFirst(); - } - // 조건 빌딩 private BooleanBuilder buildCondition(QReport report, String targetId, String reportTargetType, String reportType, String isVisible) { BooleanBuilder builder = new BooleanBuilder(); diff --git a/src/main/java/com/picnee/travel/domain/report/service/ReportService.java b/src/main/java/com/picnee/travel/domain/report/service/ReportService.java index 3b3f40e..66c2027 100644 --- a/src/main/java/com/picnee/travel/domain/report/service/ReportService.java +++ b/src/main/java/com/picnee/travel/domain/report/service/ReportService.java @@ -5,11 +5,14 @@ import com.picnee.travel.domain.report.dto.req.CreateReportReq; import com.picnee.travel.domain.report.dto.res.FindReportRes; import com.picnee.travel.domain.report.entity.Report; +import com.picnee.travel.domain.report.exception.NotFoundReportException; import com.picnee.travel.domain.report.repository.ReportRepository; import com.picnee.travel.domain.review.service.ReviewService; import com.picnee.travel.domain.user.dto.req.AuthenticatedUserReq; import com.picnee.travel.domain.user.entity.User; +import com.picnee.travel.domain.user.exception.NotAdminException; import com.picnee.travel.domain.user.service.UserService; +import com.picnee.travel.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -20,6 +23,8 @@ import java.util.UUID; +import static com.picnee.travel.global.exception.ErrorCode.*; + @Slf4j @Service @Transactional(readOnly = true) @@ -81,25 +86,24 @@ public Page findReports(AuthenticatedUserReq auth, String targetI * 권한 : 어드민 */ @Transactional - public Boolean processReport(UUID reportTargetId, AuthenticatedUserReq auth) { + public void processReport(UUID reportId, AuthenticatedUserReq auth) { validateAdmin(auth); - - Report report = reportRepository.processReport(reportTargetId); - - switch (report.getReportTargetType()) { - case REVIEW : - //TODO : 리뷰 제재는 개발완료 후 작성 -// reviewService.sanction(reportTargetId); - break; - case POST : - postService.sanction(reportTargetId); - break; - case COMMENT: - postCommentService.sanction(reportTargetId); - break; + Report report = findById(reportId); + UUID reportTargetId = report.getTargetId(); + User reportedUser = switch (report.getReportTargetType()) { + case REVIEW -> reviewService.sanction(reportTargetId); + case POST -> postService.sanction(reportTargetId); + case COMMENT -> postCommentService.sanction(reportTargetId); + }; + + //리포트 누적 5회 시 유저 계정 정지 BLOCKED 처리 + reportedUser.reportSanctionCountPlus(); + if(reportedUser.isRequiringSanctions()){ + reportedUser.updateBlockedStatus(); } - return true; + report.softDelete(); + } /** @@ -107,8 +111,13 @@ public Boolean processReport(UUID reportTargetId, AuthenticatedUserReq auth) { */ private void validateAdmin(AuthenticatedUserReq auth) { if (!auth.isAdmin()) { - throw new IllegalArgumentException("어드민 권한이 아닙니다"); + throw new NotAdminException(NOT_ADMIN_EXCEPTION); } } + private Report findById(UUID reportId) { + return reportRepository.findById(reportId) + .orElseThrow(() -> new NotFoundReportException(NOT_FOUND_REPORT_EXCEPTION)); + } + } diff --git a/src/main/java/com/picnee/travel/domain/review/dto/req/BaseReviewReq.java b/src/main/java/com/picnee/travel/domain/review/dto/req/BaseReviewReq.java index 5046444..f4ef24c 100644 --- a/src/main/java/com/picnee/travel/domain/review/dto/req/BaseReviewReq.java +++ b/src/main/java/com/picnee/travel/domain/review/dto/req/BaseReviewReq.java @@ -26,6 +26,7 @@ public Review toEntity(BaseReviewReq dto, User user, Place place) { .placeTips(dto.getPlaceTips()) .rating(dto.getRating()) .likes(0L) + .reportSanctionCount(0) .user(user) .place(place) .build(); diff --git a/src/main/java/com/picnee/travel/domain/review/entity/Review.java b/src/main/java/com/picnee/travel/domain/review/entity/Review.java index 41e2609..feb12df 100644 --- a/src/main/java/com/picnee/travel/domain/review/entity/Review.java +++ b/src/main/java/com/picnee/travel/domain/review/entity/Review.java @@ -45,6 +45,8 @@ public class Review extends SoftDeleteBaseEntity { private String placeTips; @Column(name = "likes") private Long likes; + @Column(name = "report_sanction_count") + private Integer reportSanctionCount; @Column(name = "rating") private Double rating; @ManyToOne(fetch = LAZY) @@ -95,4 +97,18 @@ public void addLike() { public void deleteLike() { this.likes--; } + + /** + * 신고 횟수 누적 + */ + public void reportSanctionCountPlus(){ + this.reportSanctionCount++; + } + + /** + * 리뷰 제재 가능여부 확인 + */ + public boolean isRequiringSanctions() { + return this.reportSanctionCount >= 5; + } } diff --git a/src/main/java/com/picnee/travel/domain/review/service/ReviewService.java b/src/main/java/com/picnee/travel/domain/review/service/ReviewService.java index e6a7460..0354d38 100644 --- a/src/main/java/com/picnee/travel/domain/review/service/ReviewService.java +++ b/src/main/java/com/picnee/travel/domain/review/service/ReviewService.java @@ -323,4 +323,19 @@ public Review findById(UUID reviewId) { private boolean isUserAuthenticated(AuthenticatedUserReq auth) { return auth == null; } + + /** + * 신고된 리뷰 제재 + */ + @Transactional + public User sanction(UUID reportTargetId) { + Review review = findByIdNotDeletedReview(reportTargetId); + + review.reportSanctionCountPlus(); + if(review.isRequiringSanctions()){ + review.softDelete(); + } + + return review.getUser(); + } } diff --git a/src/main/java/com/picnee/travel/domain/user/entity/User.java b/src/main/java/com/picnee/travel/domain/user/entity/User.java index c525108..9548629 100644 --- a/src/main/java/com/picnee/travel/domain/user/entity/User.java +++ b/src/main/java/com/picnee/travel/domain/user/entity/User.java @@ -50,6 +50,8 @@ public class User extends SoftDeleteBaseEntity { private Gender gender; @Column(name = "social_root") private String socialRoot; + @Column(name = "report_sanction_count") + private Integer reportSanctionCount; @JsonProperty("password_count") @Column(name = "password_count") private Integer passwordCount; @@ -83,10 +85,22 @@ public void resetPasswordCount() { this.passwordCount = 0; } + public void reportSanctionCountPlus(){ + this.reportSanctionCount++; + } + + public boolean isRequiringSanctions() { + return this.reportSanctionCount >= 5; + } + public void updateLockedStatus() { this.state = State.LOCKED; } + public void updateBlockedStatus() { + this.state = State.BLOCKED; + } + public void changeNullState() { this.state = null; } diff --git a/src/main/java/com/picnee/travel/domain/user/exception/NotAdminException.java b/src/main/java/com/picnee/travel/domain/user/exception/NotAdminException.java new file mode 100644 index 0000000..42183f4 --- /dev/null +++ b/src/main/java/com/picnee/travel/domain/user/exception/NotAdminException.java @@ -0,0 +1,10 @@ +package com.picnee.travel.domain.user.exception; + +import com.picnee.travel.global.exception.BusinessException; +import com.picnee.travel.global.exception.ErrorCode; + +public class NotAdminException extends BusinessException { + public NotAdminException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/picnee/travel/domain/user/service/UserService.java b/src/main/java/com/picnee/travel/domain/user/service/UserService.java index 2057db4..66120c5 100644 --- a/src/main/java/com/picnee/travel/domain/user/service/UserService.java +++ b/src/main/java/com/picnee/travel/domain/user/service/UserService.java @@ -47,6 +47,7 @@ public User create(CreateUserReq dto) { .nickname(dto.getNickname()) .password(passwordEncoder.encode(dto.getPassword())) .passwordCount(0) + .reportSanctionCount(0) .accountLock(false) .lastPasswordExpired(LocalDateTime.now()) .profileImage(null) @@ -166,5 +167,9 @@ public void validateUser(User user) { if (user.getState() == LOCKED) { throw new LoginLockedException(LOGIN_LOCKED_EXCEPTION); } + + if(user.getState() == BLOCKED) { + throw new LoginLockedException(LOGIN_BLOCKED_EXCEPTION); + } } } diff --git a/src/main/java/com/picnee/travel/global/exception/ErrorCode.java b/src/main/java/com/picnee/travel/global/exception/ErrorCode.java index ed6febf..16f61be 100644 --- a/src/main/java/com/picnee/travel/global/exception/ErrorCode.java +++ b/src/main/java/com/picnee/travel/global/exception/ErrorCode.java @@ -21,16 +21,18 @@ public enum ErrorCode { */ LOGIN_FAILED_EXCEPTION(UNAUTHORIZED, "G001", "회 로그인에 실패했습니다. 5회 실패 시 계정이 차단됩니다."), LOGIN_LOCKED_EXCEPTION(UNAUTHORIZED, "G002", "비밀번호를 오입력으로 계정이 잠겼습니다. 비밀번호를 변경해주세요."), - NOT_POST_AUTHOR_EXCEPTION(UNAUTHORIZED, "G003", "게시글의 작성자가 아닙니다. 본인이 작성한 게시글만 수정/삭제가 가능합니다."), - NOT_LOGIN_EXCEPTION(UNAUTHORIZED, "G004", "로그인이 필요한 서비스입니다."), - NOT_VALID_REFRESH_TOKEN_EXCEPTION(UNAUTHORIZED, "005", "유효하지 않은 토큰입니다."), - NOT_VALID_OWNER_EXCEPTION(UNAUTHORIZED, "G006", "작성자만 삭제/수정이 가능합니다."), - NOT_NOTIFICATION_RECIPIENT_EXCEPTION(UNAUTHORIZED, "G007", "알림 수신자가 아닙니다. 본인의 알림만 읽을 수 있습니다."), - NOT_PROVIDE_OAUTH_EXCEPTION(UNAUTHORIZED, "G008", "올바르지 않은 소셜 로그인입니다."), - NOT_PROVIDE_COMMENT_LIKE_EXCEPTION(UNAUTHORIZED, "G009", "댓글 좋아요는 로그인시 가능합니다."), - NOT_AUTH_EXCEPTION(UNAUTHORIZED, "G010", "로그인한 유저만 접근가능합니다."), - NOT_REVIEW_AUTHOR_EXCEPTION(UNAUTHORIZED, "G011", "리뷰의 작성자가 아닙니다. 본인이 작성한 리뷰만 수정/삭제가 가능합니다."), - EXISTS_ALREADY_REVIEW_EXCEPTION(UNAUTHORIZED, "G012", "이미 해당 리뷰에 평가를 하였습니다."), + LOGIN_BLOCKED_EXCEPTION(UNAUTHORIZED, "G003", "누적된 신고로 계정이 잠겼습니다."), + NOT_POST_AUTHOR_EXCEPTION(UNAUTHORIZED, "G004", "게시글의 작성자가 아닙니다. 본인이 작성한 게시글만 수정/삭제가 가능합니다."), + NOT_LOGIN_EXCEPTION(UNAUTHORIZED, "G005", "로그인이 필요한 서비스입니다."), + NOT_VALID_REFRESH_TOKEN_EXCEPTION(UNAUTHORIZED, "006", "유효하지 않은 토큰입니다."), + NOT_VALID_OWNER_EXCEPTION(UNAUTHORIZED, "G007", "작성자만 삭제/수정이 가능합니다."), + NOT_NOTIFICATION_RECIPIENT_EXCEPTION(UNAUTHORIZED, "G008", "알림 수신자가 아닙니다. 본인의 알림만 읽을 수 있습니다."), + NOT_PROVIDE_OAUTH_EXCEPTION(UNAUTHORIZED, "G009", "올바르지 않은 소셜 로그인입니다."), + NOT_PROVIDE_COMMENT_LIKE_EXCEPTION(UNAUTHORIZED, "G010", "댓글 좋아요는 로그인시 가능합니다."), + NOT_AUTH_EXCEPTION(UNAUTHORIZED, "G011", "로그인한 유저만 접근가능합니다."), + NOT_REVIEW_AUTHOR_EXCEPTION(UNAUTHORIZED, "G012", "리뷰의 작성자가 아닙니다. 본인이 작성한 리뷰만 수정/삭제가 가능합니다."), + EXISTS_ALREADY_REVIEW_EXCEPTION(UNAUTHORIZED, "G013", "이미 해당 리뷰에 평가를 하였습니다."), + NOT_ADMIN_EXCEPTION(UNAUTHORIZED, "G014", "어드민 권한이 아닙니다"), /** * 403 @@ -52,7 +54,8 @@ public enum ErrorCode { NOT_FOUND_NOTIFICATION_EXCEPTION(NOT_FOUND, "G008", "존재하지 않는 알림입니다."), NOT_FOUND_REVIEW_EXCEPTION(NOT_FOUND, "G009", "존재하지 않는 리뷰입니다."), NOT_FOUND_REVIEW_CATEGORY_EXCEPTION(NOT_FOUND, "G010", "존재하지 않는 리뷰 카테고리입니다."), - NOT_FOUND_PLACE_EXCEPTION(NOT_FOUND, "G010", "존재하지 않는 장소 입니다."), + NOT_FOUND_PLACE_EXCEPTION(NOT_FOUND, "G011", "존재하지 않는 장소 입니다."), + NOT_FOUND_REPORT_EXCEPTION(NOT_FOUND, "G012", "존재하지 않는 리뷰 입니다."), /** * 500 diff --git a/src/main/java/com/picnee/travel/global/oauth/OAuthAttributes.java b/src/main/java/com/picnee/travel/global/oauth/OAuthAttributes.java index 16ef473..9fc5f9e 100644 --- a/src/main/java/com/picnee/travel/global/oauth/OAuthAttributes.java +++ b/src/main/java/com/picnee/travel/global/oauth/OAuthAttributes.java @@ -49,6 +49,7 @@ public User toEntity() { .isDefaultNickname(isDefaultNickname) .socialRoot(socialRoot) .passwordCount(0) + .reportSanctionCount(0) .accountLock(false) .lastPasswordExpired(LocalDateTime.now()) .profileImage(null) diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index d3bc46b..6df8660 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -25,6 +25,7 @@ CREATE TABLE `users` ( `nickname` VARCHAR(255) NOT NULL UNIQUE, `gender` VARCHAR(10), `social_root` VARCHAR(20), + `report_sanction_count` INT NOT NULL DEFAULT 0, `password_count` INT NOT NULL DEFAULT 0, `account_lock` BOOLEAN NOT NULL, `last_password_expired` TIMESTAMP NOT NULL, @@ -84,6 +85,7 @@ CREATE TABLE `post` ( `title` VARCHAR(255) NOT NULL, `content` VARCHAR(255) NOT NULL, `viewed` BIGINT DEFAULT 0, + `report_sanction_count` INT NOT NULL DEFAULT 0, `created_at` TIMESTAMP NOT NULL, `modified_at` TIMESTAMP NOT NULL, `deleted_at` TIMESTAMP NULL, @@ -97,6 +99,7 @@ CREATE TABLE `post_comment` ( `post_comment_id` VARCHAR(36) NOT NULL, `content` LONGTEXT NOT NULL, `likes` BIGINT DEFAULT 0, + `report_sanction_count` INT NOT NULL DEFAULT 0, `created_at` TIMESTAMP NOT NULL, `modified_at` TIMESTAMP NOT NULL, `deleted_at` TIMESTAMP NULL, @@ -179,6 +182,7 @@ CREATE TABLE `review` ( `place_tips` LONGTEXT NULL, `rating` DOUBLE NOT NULL, `likes` BIGINT DEFAULT 0, + `report_sanction_count` INT NOT NULL DEFAULT 0, `created_at` TIMESTAMP NOT NULL, `modified_at` TIMESTAMP NOT NULL, `deleted_at` TIMESTAMP NULL, @@ -230,7 +234,7 @@ CREATE TABLE `review_vote_accommodation` ( `has_good_soundproofing` BOOLEAN NOT NULL, `has_delicious_breakfast` BOOLEAN NOT NULL, `has_friendly_service` BOOLEAN NOT NULL, - `is_easy_public_transport` BOOLEAN NOT NULL + `is_easy_public_transport` BOOLEAN NOT NULL, PRIMARY KEY (`review_id`), FOREIGN KEY (`review_id`) REFERENCES `review`(`review_id`) ); @@ -238,7 +242,7 @@ CREATE TABLE `review_vote_accommodation` ( CREATE TABLE `review_vote_touristspot` ( `review_id` VARCHAR(36) NOT NULL, `is_paid_entry` BOOLEAN NOT NULL, - `is_reservation_required` BOOLEAN NOT NULL, + `is_reservation_required` BOOLEAN `` NOT NULL, `is_korean_guide_available` BOOLEAN NOT NULL, `is_bike_parking_available` BOOLEAN NOT NULL, `is_car_parking_available` BOOLEAN NOT NULL, @@ -251,7 +255,7 @@ CREATE TABLE `review_vote_touristspot` ( `has_experience_programs` BOOLEAN NOT NULL, `has_clean_restrooms` BOOLEAN NOT NULL, `is_easy_public_transport` BOOLEAN NOT NULL, - `is_quiet_and_peaceful` BOOLEAN NOT NULL + `is_quiet_and_peaceful` BOOLEAN NOT NULL, PRIMARY KEY (`review_id`), FOREIGN KEY (`review_id`) REFERENCES `review`(`review_id`) );