diff --git a/hanharam/src/main/java/com/leets/blog/authorization/domain/ResourceType.java b/hanharam/src/main/java/com/leets/blog/authorization/domain/ResourceType.java new file mode 100644 index 00000000..00ba4fec --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/authorization/domain/ResourceType.java @@ -0,0 +1,43 @@ +package com.leets.blog.authorization.domain; + +import lombok.Getter; + +import java.util.Set; + +/** + * 권한 체크 대상이 되는 리소스 타입 + */ +@Getter +public enum ResourceType { + + POST("post", "게시글", Set.of( + PermissionType.READ, PermissionType.WRITE, PermissionType.EDIT, PermissionType.DELETE + )), + COMMENT("comment", "댓글", Set.of( + PermissionType.READ, PermissionType.WRITE, PermissionType.EDIT, PermissionType.DELETE + )), + MEMBER("member", "회원", + Set.of(PermissionType.READ, PermissionType.EDIT, PermissionType.DELETE)); + + private final String code; + private final String description; + private final Set supportedPermissions; + + ResourceType(String code, String description, Set supportedPermissions) { + this.code = code; + this.description = description; + this.supportedPermissions = Set.copyOf(supportedPermissions); + } + + public boolean supports(PermissionType permission) { + return supportedPermissions.contains(permission); + } + + public void validatePermission(PermissionType permission) { + if (!supports(permission)) { + throw new IllegalArgumentException( + String.format("리소스 '%s'은(는) '%s' 권한을 지원하지 않습니다.", this.name(), permission) + ); + } + } +} diff --git a/hanharam/src/main/java/com/leets/blog/comment/adapter/out/persistence/CommentPersistenceAdapter.java b/hanharam/src/main/java/com/leets/blog/comment/adapter/out/persistence/CommentPersistenceAdapter.java new file mode 100644 index 00000000..7c35b66b --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/comment/adapter/out/persistence/CommentPersistenceAdapter.java @@ -0,0 +1,17 @@ +package com.leets.blog.comment.adapter.out.persistence; + +import com.leets.blog.comment.application.port.out.LoadCommentPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class CommentPersistenceAdapter implements LoadCommentPort { + + private final CommentRepository commentRepository; + + @Override + public boolean existsById(Long commentId) { + return commentRepository.existsById(commentId); + } +} diff --git a/hanharam/src/main/java/com/leets/blog/comment/adapter/out/persistence/CommentRepository.java b/hanharam/src/main/java/com/leets/blog/comment/adapter/out/persistence/CommentRepository.java new file mode 100644 index 00000000..2013f3a3 --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/comment/adapter/out/persistence/CommentRepository.java @@ -0,0 +1,7 @@ +package com.leets.blog.comment.adapter.out.persistence; + +import com.leets.blog.comment.domain.CommentJpaEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { +} diff --git a/hanharam/src/main/java/com/leets/blog/comment/application/port/out/LoadCommentPort.java b/hanharam/src/main/java/com/leets/blog/comment/application/port/out/LoadCommentPort.java new file mode 100644 index 00000000..f7a68634 --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/comment/application/port/out/LoadCommentPort.java @@ -0,0 +1,6 @@ +package com.leets.blog.comment.application.port.out; + +public interface LoadCommentPort { + // TODO: Comment 도메인 생성 후 exists 기반 검증 대신 도메인 조회 책임으로 변경 + boolean existsById(Long commentId); +} diff --git a/hanharam/src/main/java/com/leets/blog/comment/domain/CommentJpaEntity.java b/hanharam/src/main/java/com/leets/blog/comment/domain/CommentJpaEntity.java index 777dfea4..94fbf2a8 100644 --- a/hanharam/src/main/java/com/leets/blog/comment/domain/CommentJpaEntity.java +++ b/hanharam/src/main/java/com/leets/blog/comment/domain/CommentJpaEntity.java @@ -6,6 +6,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; @Entity @Getter @@ -36,4 +37,9 @@ private CommentJpaEntity(Long postId, Long memberId, String content, Long parent this.content = content; this.parentId = parentId; } + + // todo: toDomain 임시 설정 -> Comment 도메인 생성 후 메서드 작성하기 + public Comment toDomain() { + return null; + } } diff --git a/hanharam/src/main/java/com/leets/blog/global/exception/constant/Domain.java b/hanharam/src/main/java/com/leets/blog/global/exception/constant/Domain.java index 67c803a6..55351e2b 100644 --- a/hanharam/src/main/java/com/leets/blog/global/exception/constant/Domain.java +++ b/hanharam/src/main/java/com/leets/blog/global/exception/constant/Domain.java @@ -9,5 +9,6 @@ public enum Domain { AUTHENTICATION, COMMENT, POST, - MEMBER + MEMBER, + REPORT } diff --git a/hanharam/src/main/java/com/leets/blog/report/adapter/in/web/ReportController.java b/hanharam/src/main/java/com/leets/blog/report/adapter/in/web/ReportController.java new file mode 100644 index 00000000..8f9daf8f --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/adapter/in/web/ReportController.java @@ -0,0 +1,49 @@ +package com.leets.blog.report.adapter.in.web; + +import com.leets.blog.report.adapter.in.web.dto.request.CreateReportRequest; +import com.leets.blog.report.application.port.in.command.ReportCommentUseCase; +import com.leets.blog.report.application.port.in.command.ReportPostUseCase; +import com.leets.blog.report.application.port.in.command.ReviewReportUseCase; +import com.leets.blog.report.application.port.in.command.dto.ReviewReportCommand; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/reports") +@RequiredArgsConstructor +@Tag(name = "Report | 신고 command", description = "신고 관련 API") +public class ReportController { + + private final ReportCommentUseCase reportCommentUseCase; + private final ReportPostUseCase reportPostUseCase; + private final ReviewReportUseCase reviewReportUseCase; + + @PostMapping("/posts/{postId}") + @Operation(summary = "게시글 신고", description = "특정 게시글을 신고합니다.") + public void reportPost( + @PathVariable Long postId, + @Valid @RequestBody CreateReportRequest request, + Long reporterId + ) { + reportPostUseCase.report(request.toPostCommand(postId, reporterId)); + } + + @PostMapping("/comments/{commentId}") + @Operation(summary = "댓글 신고", description = "특정 댓글을 신고합니다.") + public void reportComment( + @PathVariable Long commentId, + @Valid @RequestBody CreateReportRequest request, + Long reporterId + ) { + reportCommentUseCase.report(request.toCommentCommand(commentId, reporterId)); + } + + @PatchMapping("/{reportId}/reviewing") + @Operation(summary = "신고 검토중 처리", description = "특정 신고를 검토중 상태로 변경합니다.") + public void reviewReport(@PathVariable Long reportId) { + reviewReportUseCase.review(new ReviewReportCommand(reportId)); + } +} diff --git a/hanharam/src/main/java/com/leets/blog/report/adapter/in/web/dto/request/CreateReportRequest.java b/hanharam/src/main/java/com/leets/blog/report/adapter/in/web/dto/request/CreateReportRequest.java new file mode 100644 index 00000000..aca639bd --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/adapter/in/web/dto/request/CreateReportRequest.java @@ -0,0 +1,21 @@ +package com.leets.blog.report.adapter.in.web.dto.request; + +import com.leets.blog.report.application.port.in.command.dto.ReportCommentCommand; +import com.leets.blog.report.application.port.in.command.dto.ReportPostCommand; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "신고 사유 작성 요청") +public record CreateReportRequest( + @Schema(description = "신고 사유", example = "부적절한 언어 포함") + @NotBlank(message = "신고 사유는 필수입니다.") + String reason +) { + public ReportCommentCommand toCommentCommand(Long commentId, Long reporterId) { + return new ReportCommentCommand(commentId, reporterId, reason); + } + + public ReportPostCommand toPostCommand(Long postId, Long reporterId) { + return new ReportPostCommand(postId, reporterId, reason); + } +} diff --git a/hanharam/src/main/java/com/leets/blog/report/adapter/out/persistence/ReportPersistenceAdapter.java b/hanharam/src/main/java/com/leets/blog/report/adapter/out/persistence/ReportPersistenceAdapter.java new file mode 100644 index 00000000..0cd35308 --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/adapter/out/persistence/ReportPersistenceAdapter.java @@ -0,0 +1,48 @@ +package com.leets.blog.report.adapter.out.persistence; + +import com.leets.blog.report.adapter.out.persistence.entity.ReportJpaEntity; +import com.leets.blog.report.application.port.out.LoadReportPort; +import com.leets.blog.report.application.port.out.SaveReportPort; +import com.leets.blog.report.domain.Report; +import com.leets.blog.report.domain.enums.ReportTargetType; +import com.leets.blog.report.domain.exception.ReportDomainException; +import com.leets.blog.report.domain.exception.ReportErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ReportPersistenceAdapter implements LoadReportPort, SaveReportPort { + + private final ReportRepository reportRepository; + + @Override + public boolean existsByReporterIdAndTargetTypeAndTargetId(Long reporterId, ReportTargetType targetType, Long targetId) { + + return reportRepository.existsByReporterIdAndTargetTypeAndTargetId(reporterId, targetType, targetId); + } + + @Override + public Report findReport(Report.ReportId reportId) { + return reportRepository.findById(reportId.id()) + .map(ReportJpaEntity::toDomain) + .orElseThrow(() -> new ReportDomainException(ReportErrorCode.REPORT_NOT_FOUND)); + } + + @Override + public Report save(Report report) { + ReportJpaEntity entity; + + if (report.getReportId() == null) { + entity = ReportJpaEntity.from(report); + } else { + entity = reportRepository.findById(report.getReportId().id()) + .orElseThrow(() -> new ReportDomainException(ReportErrorCode.REPORT_NOT_FOUND)); + entity.update(report); + } + + ReportJpaEntity saved = reportRepository.save(entity); + + return saved.toDomain(); + } +} diff --git a/hanharam/src/main/java/com/leets/blog/report/adapter/out/persistence/ReportRepository.java b/hanharam/src/main/java/com/leets/blog/report/adapter/out/persistence/ReportRepository.java new file mode 100644 index 00000000..5e661b1f --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/adapter/out/persistence/ReportRepository.java @@ -0,0 +1,9 @@ +package com.leets.blog.report.adapter.out.persistence; + +import com.leets.blog.report.adapter.out.persistence.entity.ReportJpaEntity; +import com.leets.blog.report.domain.enums.ReportTargetType; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReportRepository extends JpaRepository { + boolean existsByReporterIdAndTargetTypeAndTargetId(Long reporterId, ReportTargetType targetType, Long targetId); +} diff --git a/hanharam/src/main/java/com/leets/blog/report/adapter/out/persistence/entity/ReportJpaEntity.java b/hanharam/src/main/java/com/leets/blog/report/adapter/out/persistence/entity/ReportJpaEntity.java new file mode 100644 index 00000000..c271d75b --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/adapter/out/persistence/entity/ReportJpaEntity.java @@ -0,0 +1,76 @@ +package com.leets.blog.report.adapter.out.persistence.entity; + +import com.leets.blog.common.BaseEntity; +import com.leets.blog.report.domain.Report; +import com.leets.blog.report.domain.enums.ReportStatus; +import com.leets.blog.report.domain.enums.ReportTargetType; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "report") +public class ReportJpaEntity extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "reporter_id", nullable = false) + private Long reporterId; + + @Enumerated(EnumType.STRING) + @Column(name = "target_type", nullable = false) + private ReportTargetType targetType; + + @Column(name = "target_id", nullable = false) + private Long targetId; + + @Enumerated(EnumType.STRING) + @Column(name = "report_status", nullable = false) + private ReportStatus reportStatus; + + @Column(name = "reason", nullable = false, length = 250) + private String reason; + + @Builder + private ReportJpaEntity(Long reporterId, ReportTargetType targetType, Long targetId, ReportStatus reportStatus, String reason) { + this.reporterId = reporterId; + this.targetType = targetType; + this.targetId = targetId; + this.reportStatus = reportStatus; + this.reason = reason; + } + + // Domain -> JPA Entity + public static ReportJpaEntity from(Report report) { + return ReportJpaEntity.builder() + .reporterId(report.getReporterId()) + .targetType(report.getTargetType()) + .targetId(report.getTargetId()) + .reportStatus(report.getReportStatus()) + .reason(report.getReason()) + .build(); + } + + public void update(Report report) { + this.reportStatus = report.getReportStatus(); + } + + // JPA Entity -> Domain + public Report toDomain() { + return Report.reconstruct( + new Report.ReportId(this.id), + this.reporterId, + this.targetType, + this.targetId, + this.reportStatus, + this.reason + ); + } + +} diff --git a/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/ReportCommentUseCase.java b/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/ReportCommentUseCase.java new file mode 100644 index 00000000..6a2d3bd5 --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/ReportCommentUseCase.java @@ -0,0 +1,7 @@ +package com.leets.blog.report.application.port.in.command; + +import com.leets.blog.report.application.port.in.command.dto.ReportCommentCommand; + +public interface ReportCommentUseCase { + void report(ReportCommentCommand command); +} diff --git a/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/ReportPostUseCase.java b/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/ReportPostUseCase.java new file mode 100644 index 00000000..4af7accc --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/ReportPostUseCase.java @@ -0,0 +1,7 @@ +package com.leets.blog.report.application.port.in.command; + +import com.leets.blog.report.application.port.in.command.dto.ReportPostCommand; + +public interface ReportPostUseCase { + void report(ReportPostCommand command); +} diff --git a/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/ReviewReportUseCase.java b/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/ReviewReportUseCase.java new file mode 100644 index 00000000..45720c53 --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/ReviewReportUseCase.java @@ -0,0 +1,7 @@ +package com.leets.blog.report.application.port.in.command; + +import com.leets.blog.report.application.port.in.command.dto.ReviewReportCommand; + +public interface ReviewReportUseCase { + void review(ReviewReportCommand command); +} diff --git a/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/dto/ReportCommentCommand.java b/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/dto/ReportCommentCommand.java new file mode 100644 index 00000000..485cadcd --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/dto/ReportCommentCommand.java @@ -0,0 +1,15 @@ +package com.leets.blog.report.application.port.in.command.dto; + +import java.util.Objects; + +public record ReportCommentCommand( + Long commentId, + Long reporterId, + String reason +) { + public ReportCommentCommand{ + Objects.requireNonNull(commentId, "댓글 ID는 필수입니다."); + Objects.requireNonNull(reporterId, "신고자 ID는 필수입니다."); + Objects.requireNonNull(reason, "신고 사유는 필수입니다."); + } +} diff --git a/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/dto/ReportPostCommand.java b/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/dto/ReportPostCommand.java new file mode 100644 index 00000000..cbac1ad8 --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/dto/ReportPostCommand.java @@ -0,0 +1,15 @@ +package com.leets.blog.report.application.port.in.command.dto; + +import java.util.Objects; + +public record ReportPostCommand( + Long postId, + Long reporterId, + String reason +) { + public ReportPostCommand{ + Objects.requireNonNull(postId, "게시글 ID는 필수입니다."); + Objects.requireNonNull(reporterId, "신고자 ID는 필수입니다."); + Objects.requireNonNull(reason, "신고 사유는 필수입니다."); + } +} diff --git a/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/dto/ReviewReportCommand.java b/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/dto/ReviewReportCommand.java new file mode 100644 index 00000000..a5402be0 --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/application/port/in/command/dto/ReviewReportCommand.java @@ -0,0 +1,11 @@ +package com.leets.blog.report.application.port.in.command.dto; + +import java.util.Objects; + +public record ReviewReportCommand( + Long reportId +) { + public ReviewReportCommand { + Objects.requireNonNull(reportId, "신고 ID는 필수입니다."); + } +} diff --git a/hanharam/src/main/java/com/leets/blog/report/application/port/out/LoadReportPort.java b/hanharam/src/main/java/com/leets/blog/report/application/port/out/LoadReportPort.java new file mode 100644 index 00000000..a4c51f49 --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/application/port/out/LoadReportPort.java @@ -0,0 +1,10 @@ +package com.leets.blog.report.application.port.out; + +import com.leets.blog.report.domain.Report; +import com.leets.blog.report.domain.enums.ReportTargetType; + +public interface LoadReportPort { + boolean existsByReporterIdAndTargetTypeAndTargetId(Long reporterId, ReportTargetType targetType, Long targetId); + + Report findReport(Report.ReportId reportId); +} diff --git a/hanharam/src/main/java/com/leets/blog/report/application/port/out/SaveReportPort.java b/hanharam/src/main/java/com/leets/blog/report/application/port/out/SaveReportPort.java new file mode 100644 index 00000000..8b43ac57 --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/application/port/out/SaveReportPort.java @@ -0,0 +1,7 @@ +package com.leets.blog.report.application.port.out; + +import com.leets.blog.report.domain.Report; + +public interface SaveReportPort { + Report save(Report report); +} diff --git a/hanharam/src/main/java/com/leets/blog/report/application/service/ReportCommandService.java b/hanharam/src/main/java/com/leets/blog/report/application/service/ReportCommandService.java new file mode 100644 index 00000000..c7959d4d --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/application/service/ReportCommandService.java @@ -0,0 +1,78 @@ +package com.leets.blog.report.application.service; + +import com.leets.blog.comment.application.port.out.LoadCommentPort; +import com.leets.blog.post.application.port.out.LoadPostPort; +import com.leets.blog.report.application.port.in.command.ReportCommentUseCase; +import com.leets.blog.report.application.port.in.command.ReportPostUseCase; +import com.leets.blog.report.application.port.in.command.ReviewReportUseCase; +import com.leets.blog.report.application.port.in.command.dto.ReportCommentCommand; +import com.leets.blog.report.application.port.in.command.dto.ReportPostCommand; +import com.leets.blog.report.application.port.in.command.dto.ReviewReportCommand; +import com.leets.blog.report.application.port.out.LoadReportPort; +import com.leets.blog.report.application.port.out.SaveReportPort; +import com.leets.blog.report.domain.Report; +import com.leets.blog.report.domain.enums.ReportTargetType; +import com.leets.blog.report.domain.exception.ReportDomainException; +import com.leets.blog.report.domain.exception.ReportErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class ReportCommandService implements ReportPostUseCase, ReportCommentUseCase, ReviewReportUseCase { + + private final SaveReportPort saveReportPort; + private final LoadReportPort loadReportPort; + private final LoadPostPort loadPostPort; + private final LoadCommentPort loadCommentPort; + + @Override + public void report(ReportCommentCommand command) { + // 댓글 존재 확인 + if (!loadCommentPort.existsById(command.commentId())) { + throw new ReportDomainException(ReportErrorCode.COMMENT_NOT_FOUND); + } + + // 중복 신고 확인 및 저장 + checkDuplicateAndSaveReport(command.reporterId(), ReportTargetType.COMMENT, command.commentId(), command.reason()); + } + + @Override + public void report(ReportPostCommand command) { + // 게시글 존재 확인 + loadPostPort.findById(command.postId()) + .orElseThrow(() -> new ReportDomainException(ReportErrorCode.POST_NOT_FOUND)); + + // 중복 신고 확인 및 저장 + checkDuplicateAndSaveReport(command.reporterId(), ReportTargetType.POST, command.postId(), command.reason()); + } + + @Override + public void review(ReviewReportCommand command) { + Report report = loadReportPort.findReport(new Report.ReportId(command.reportId())); + report.markAsReviewing(); + saveReportPort.save(report); + } + + /** + * 중복 신고를 확인하고 신고를 생성합니다. + * + * @param reporterId 신고자 ID + * @param targetType 신고 타겟 유형 (Comment, Post) + * @param targetId 신고 대상 ID + * @throws ReportDomainException 이미 신고한 경우 + */ + + private void checkDuplicateAndSaveReport(Long reporterId, ReportTargetType targetType, Long targetId, String reason) { + // 중복 신고 확인 + if (loadReportPort.existsByReporterIdAndTargetTypeAndTargetId(reporterId, targetType, targetId)) { + throw new ReportDomainException(ReportErrorCode.REPORT_ALREADY_EXISTS); + } + + // 신고 생성 및 저장 + Report report = Report.create(reporterId, targetType, targetId, reason); + saveReportPort.save(report); + } +} diff --git a/hanharam/src/main/java/com/leets/blog/report/domain/Report.java b/hanharam/src/main/java/com/leets/blog/report/domain/Report.java new file mode 100644 index 00000000..f98942b6 --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/domain/Report.java @@ -0,0 +1,124 @@ +package com.leets.blog.report.domain; + +import com.leets.blog.report.domain.enums.ReportStatus; +import com.leets.blog.report.domain.enums.ReportTargetType; +import com.leets.blog.report.domain.exception.ReportDomainException; +import com.leets.blog.report.domain.exception.ReportErrorCode; +import lombok.*; + +import java.time.LocalDateTime; + + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder(access = AccessLevel.PRIVATE) +public class Report{ + @Getter + private final ReportId reportId; + + @Getter + private final Long reporterId; // 신고자 ID + + @Getter + private final ReportTargetType targetType; // COMMENT, POST + + @Getter + private final Long targetId; + + @Getter + private ReportStatus reportStatus; // PENDING, APPROVED, REJECTED + + @Getter + private final String reason; // 신고 사유 + + @Getter + private final LocalDateTime createdAt; + + // 신고 생성 + public static Report create(Long reporterId, ReportTargetType targetType, Long targetId, String reason) { + validateReporterId(reporterId); + validateTargetType(targetType); + validateTargetId(targetId); + validateReason(reason); + + // 생성 시점의 상태는 대기로 초기화 + ReportStatus initialStatus = ReportStatus.PENDING; + LocalDateTime now = LocalDateTime.now(); + + return Report.builder() + .reporterId(reporterId) + .targetType(targetType) + .targetId(targetId) + .reportStatus(initialStatus) + .reason(reason) + .createdAt(now) + .build(); + } + + // JPA Entity -> Domain + public static Report reconstruct( + ReportId reportId, + Long reporterId, + ReportTargetType targetType, + Long targetId, + ReportStatus reportStatus, + String reason) { + + validateReporterId(reporterId); + validateTargetType(targetType); + validateTargetId(targetId); + validateReason(reason); + + return Report.builder() + .reportId(reportId) + .reporterId(reporterId) + .targetType(targetType) + .targetId(targetId) + .reportStatus(reportStatus) + .reason(reason) + .build(); + } + + public void markAsReviewing() { + if (this.reportStatus != ReportStatus.PENDING) { + throw new ReportDomainException(ReportErrorCode.INVALID_STATUS_TRANSITION); + } + + this.reportStatus = ReportStatus.REVIEWING; + } + + // 신고자 아이디 검증 + private static void validateReporterId(Long reporterId) { + if (reporterId == null || reporterId <= 0) { + throw new ReportDomainException(ReportErrorCode.INVALID_REPORTER_ID); + } + } + // 타겟 유형 검증 + private static void validateTargetType(ReportTargetType targetType) { + if (targetType == null) { + throw new ReportDomainException(ReportErrorCode.INVALID_TARGET_TYPE); + } + } + // 타겟 아이디 검증 + private static void validateTargetId(Long targetId) { + if (targetId == null || targetId <= 0) { + throw new ReportDomainException(ReportErrorCode.INVALID_TARGET_ID); + } + } + // 신고 사유 검증 + private static void validateReason(String reason) { + if (reason == null || reason.isBlank()) { + throw new ReportDomainException(ReportErrorCode.INVALID_REPORT_REASON); + } + // 신고 사유 250자로 제한 + if (reason.length() > 250) { + throw new ReportDomainException(ReportErrorCode.INVALID_REASON_SIZE); + } + } + public record ReportId(Long id) { + public ReportId { + if (id <= 0) { + throw new ReportDomainException(ReportErrorCode.INVALID_ID); + } + } + } +} diff --git a/hanharam/src/main/java/com/leets/blog/report/domain/enums/ReportStatus.java b/hanharam/src/main/java/com/leets/blog/report/domain/enums/ReportStatus.java new file mode 100644 index 00000000..04a866f0 --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/domain/enums/ReportStatus.java @@ -0,0 +1,8 @@ +package com.leets.blog.report.domain.enums; + +public enum ReportStatus { + PENDING, + REVIEWING, + APPROVED, + REJECTED +} diff --git a/hanharam/src/main/java/com/leets/blog/report/domain/enums/ReportTargetType.java b/hanharam/src/main/java/com/leets/blog/report/domain/enums/ReportTargetType.java new file mode 100644 index 00000000..90f6b0b5 --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/domain/enums/ReportTargetType.java @@ -0,0 +1,6 @@ +package com.leets.blog.report.domain.enums; + +public enum ReportTargetType { + POST, + COMMENT +} diff --git a/hanharam/src/main/java/com/leets/blog/report/domain/exception/ReportDomainException.java b/hanharam/src/main/java/com/leets/blog/report/domain/exception/ReportDomainException.java new file mode 100644 index 00000000..bff4a422 --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/domain/exception/ReportDomainException.java @@ -0,0 +1,12 @@ +package com.leets.blog.report.domain.exception; + +import com.leets.blog.global.exception.BusinessException; +import com.leets.blog.global.exception.constant.Domain; + +public class ReportDomainException extends BusinessException { + public ReportDomainException(ReportErrorCode errorCode) { + super(Domain.REPORT, errorCode); + } + + public ReportDomainException(ReportErrorCode errorCode, String message) { super(Domain.REPORT, errorCode, message); } +} diff --git a/hanharam/src/main/java/com/leets/blog/report/domain/exception/ReportErrorCode.java b/hanharam/src/main/java/com/leets/blog/report/domain/exception/ReportErrorCode.java new file mode 100644 index 00000000..5fe732f5 --- /dev/null +++ b/hanharam/src/main/java/com/leets/blog/report/domain/exception/ReportErrorCode.java @@ -0,0 +1,30 @@ +package com.leets.blog.report.domain.exception; + +import com.leets.blog.global.response.code.BaseCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ReportErrorCode implements BaseCode { + + INVALID_ID(HttpStatus.BAD_REQUEST, "REPORT-001", "ID는 양수입니다."), + INVALID_REPORTER_ID(HttpStatus.BAD_REQUEST, "REPORT-002", "신고자ID는 필수입니다."), + INVALID_TARGET_TYPE(HttpStatus.BAD_REQUEST, "REPORT-003", "타겟 타입은 필수입니다."), + INVALID_TARGET_ID(HttpStatus.BAD_REQUEST, "REPORT-004", "타겟ID는 필수입니다."), + INVALID_REPORT_REASON(HttpStatus.BAD_REQUEST, "REPORT-005", "신고 사유는 필수입니다."), + INVALID_REASON_SIZE(HttpStatus.BAD_REQUEST, "REPORT-006", "신고 사유가 250자 초과입니다."), + INVALID_REPORT_STATUS(HttpStatus.BAD_REQUEST, "REPORT-007", "신고 상태는 필수입니다."), + + ALREADY_PROCESSED(HttpStatus.BAD_REQUEST, "REPORT-008", "이미 처리된 신고입니다."), + POST_NOT_FOUND(HttpStatus.BAD_REQUEST, "REPORT-009", "신고한 게시글을 찾을 수 없습니다."), + REPORT_ALREADY_EXISTS(HttpStatus.CONFLICT, "REPORT-010", "이미 신고한 게시글/댓글입니다."), + COMMENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "REPORT-011", "신고한 댓글을 찾을 수 없습니다."), + REPORT_NOT_FOUND(HttpStatus.BAD_REQUEST, "REPORT-012", "신고를 찾을 수 없습니다."), + INVALID_STATUS_TRANSITION(HttpStatus.BAD_REQUEST, "REPORT-013", "해당 상태로 변경할 수 없습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +}