Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.dodo.backend.pet.exception.PetException;
import com.dodo.backend.petweight.exception.PetWeightException;
import com.dodo.backend.reaction.exception.ReactionException;
import com.dodo.backend.report.exception.ReportException;
import com.dodo.backend.user.exception.UserErrorCode;
import com.dodo.backend.user.exception.UserException;
import com.dodo.backend.userpet.exception.UserPetException;
Expand Down Expand Up @@ -129,6 +130,15 @@ protected ResponseEntity<ErrorResponse> handleReactionException(ReactionExceptio
return toResponseEntity(e.getErrorCode());
}

/**
* 신고(Report) 도메인 비즈니스 로직에서 발생하는 {@link ReportException}을 처리합니다.
*/
@ExceptionHandler(ReportException.class)
protected ResponseEntity<ErrorResponse> handleReportException(ReportException e) {
log.error("ReportException occurred: {}", e.getErrorCode());
return toResponseEntity(e.getErrorCode());
}

/**
* 이미지 파일(ImageFile) 도메인 비즈니스 로직에서 발생하는 {@link ImageFileException}을 처리합니다.
*/
Expand Down
150 changes: 150 additions & 0 deletions src/main/java/com/dodo/backend/report/controller/ReportController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package com.dodo.backend.report.controller;

import com.dodo.backend.common.exception.ErrorResponse;
import com.dodo.backend.report.dto.request.ReportRequest.ReportCreateRequest;
import com.dodo.backend.report.dto.response.ReportResponse.ReportSimpleResponse;
import com.dodo.backend.report.service.ReportService;
import io.swagger.v3.oas.annotations.Operation;
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 io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

/**
* 신고 API 요청을 처리하는 컨트롤러입니다.
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/reports")
@Tag(name = "Report API", description = "신고 관련 API")
@Slf4j
public class ReportController {

private final ReportService reportService;

/**
* 특정 게시글을 신고합니다.
*
* @param boardId 신고 대상 게시글 ID
* @param request 신고 요청 DTO
* @param userDetails 인증 사용자 정보
* @return 신고 처리 응답
*/
@Operation(summary = "게시글 신고", description = "인증 사용자가 특정 게시글을 신고합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "신고가 성공적으로 접수되었습니다.",
content = @Content(schema = @Schema(implementation = ReportSimpleResponse.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청입니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "401", description = "로그인이 필요한 기능입니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "404", description = "신고할 게시글을 찾을 수 없습니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "409", description = "이미 신고한 대상입니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "500", description = "서버 내부 오류가 발생했습니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@PostMapping("/board/{boardId}")
public ResponseEntity<ReportSimpleResponse> reportBoard(
@PathVariable Long boardId,
@Valid @RequestBody ReportCreateRequest request,
@AuthenticationPrincipal UserDetails userDetails
) {
UUID reporterId = UUID.fromString(userDetails.getUsername());
log.info("게시글 신고 요청 - Reporter: {}, BoardId: {}", reporterId, boardId);

return ResponseEntity
.status(HttpStatus.CREATED)
.body(reportService.reportBoard(reporterId, boardId, request));
}

/**
* 특정 유저를 신고합니다.
*
* @param userId 신고 대상 유저 ID
* @param request 신고 요청 DTO
* @param userDetails 인증 사용자 정보
* @return 신고 처리 응답
*/
@Operation(summary = "유저 신고", description = "인증 사용자가 특정 유저를 신고합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "신고가 성공적으로 접수되었습니다.",
content = @Content(schema = @Schema(implementation = ReportSimpleResponse.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청입니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "401", description = "로그인이 필요한 기능입니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "404", description = "신고할 유저를 찾을 수 없습니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "409", description = "이미 신고한 유저입니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "500", description = "서버 내부 오류가 발생했습니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@PostMapping("/user/{userId}")
public ResponseEntity<ReportSimpleResponse> reportUser(
@PathVariable UUID userId,
@Valid @RequestBody ReportCreateRequest request,
@AuthenticationPrincipal UserDetails userDetails
) {
UUID reporterId = UUID.fromString(userDetails.getUsername());
log.info("유저 신고 요청 - Reporter: {}, ReportedUser: {}", reporterId, userId);

return ResponseEntity
.status(HttpStatus.CREATED)
.body(reportService.reportUser(reporterId, userId, request));
}

/**
* 특정 댓글을 신고합니다.
*
* @param commentId 신고 대상 댓글 ID
* @param request 신고 요청 DTO
* @param userDetails 인증 사용자 정보
* @return 신고 처리 응답
*/
@Operation(summary = "댓글 신고", description = "인증 사용자가 특정 댓글을 신고합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "신고가 성공적으로 접수되었습니다.",
content = @Content(schema = @Schema(implementation = ReportSimpleResponse.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청입니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "401", description = "로그인이 필요한 기능입니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "404", description = "신고할 댓글을 찾을 수 없습니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "409", description = "이미 신고한 댓글입니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "500", description = "서버 내부 오류가 발생했습니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@PostMapping("/comment/{commentId}")
public ResponseEntity<ReportSimpleResponse> reportComment(
@PathVariable Long commentId,
@Valid @RequestBody ReportCreateRequest request,
@AuthenticationPrincipal UserDetails userDetails
) {
UUID reporterId = UUID.fromString(userDetails.getUsername());
log.info("댓글 신고 요청 - Reporter: {}, CommentId: {}", reporterId, commentId);

return ResponseEntity
.status(HttpStatus.CREATED)
.body(reportService.reportComment(reporterId, commentId, request));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.dodo.backend.report.dto.request;

import com.dodo.backend.report.entity.ReportReason;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
* 신고 API에서 사용하는 요청 DTO를 모아 둔 클래스입니다.
*/
@Schema(description = "신고 요청 DTO 그룹")
public class ReportRequest {

/**
* 신고 생성 요청 DTO입니다.
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "신고 생성 요청")
public static class ReportCreateRequest {

@NotNull(message = "신고 사유는 필수입니다.")
@Schema(description = "신고 사유", example = "SPAM")
private ReportReason reportReason;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.dodo.backend.report.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
* 신고 API에서 사용하는 응답 DTO를 모아 둔 클래스입니다.
*/
@Schema(description = "신고 응답 DTO 그룹")
public class ReportResponse {

/**
* 신고 처리 결과 메시지를 반환하는 응답 DTO입니다.
*/
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "신고 단순 처리 응답")
public static class ReportSimpleResponse {

@Schema(description = "응답 메시지", example = "신고가 성공적으로 접수되었습니다.")
private String message;

/**
* 신고 단순 처리 응답 DTO를 생성합니다.
*
* @param message 응답 메시지
* @return 신고 단순 처리 응답 DTO
*/
public static ReportSimpleResponse toDto(String message) {
return ReportSimpleResponse.builder()
.message(message)
.build();
}
}
}
83 changes: 83 additions & 0 deletions src/main/java/com/dodo/backend/report/entity/Report.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.dodo.backend.report.entity;

import com.dodo.backend.board.entity.Board;
import com.dodo.backend.comment.entity.Comment;
import com.dodo.backend.user.entity.User;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

/**
* 신고 정보를 관리하는 엔티티입니다.
* <p>
* 신고자와 신고 대상(게시글, 유저, 댓글), 신고 사유, 처리 상태와 생성 일시를 저장합니다.
*/
@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
@Table(
name = "report",
uniqueConstraints = {
@UniqueConstraint(name = "uk_report_reporter_board", columnNames = {"reporter_id", "board_id"}),
@UniqueConstraint(name = "uk_report_reporter_reported_user", columnNames = {"reporter_id", "reported_user_id"}),
@UniqueConstraint(name = "uk_report_reporter_comment", columnNames = {"reporter_id", "comment_id"})
}
)
public class Report {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "report_id")
private Long reportId;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "reporter_id", nullable = false)
private User reporter;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
private Board board;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "reported_user_id")
private User reportedUser;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "comment_id")
private Comment comment;

@Enumerated(EnumType.STRING)
@Column(name = "report_reason", nullable = false)
private ReportReason reportReason;

@Builder.Default
@Enumerated(EnumType.STRING)
@Column(name = "report_status", nullable = false)
private ReportStatus reportStatus = ReportStatus.PENDING;

@CreatedDate
@Column(name = "report_created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
}
51 changes: 51 additions & 0 deletions src/main/java/com/dodo/backend/report/entity/ReportReason.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.dodo.backend.report.entity;

/**
* 신고 사유를 정의하는 열거형입니다.
*/
public enum ReportReason {
/**
* 스팸 또는 광고성 게시물입니다.
*/
SPAM,

/**
* 음란물 또는 선정적 콘텐츠입니다.
*/
OBSCENITY,

/**
* 욕설, 비방, 혐오 발언입니다.
*/
ABUSE,

/**
* 부적절한 프로필입니다.
*/
INAPPROPRIATE_PROFILE,

/**
* 스팸 또는 광고 목적의 계정/댓글입니다.
*/
SPAM_ADVERTISING,

/**
* 사칭 또는 명의 도용입니다.
*/
IMPERSONATION,

/**
* 혐오 발언입니다.
*/
HATE_SPEECH,

/**
* 개인정보 노출입니다.
*/
PRIVATE_INFO_EXPOSURE,

/**
* 괴롭힘 또는 지속적인 공격입니다.
*/
HARASSMENT
}
16 changes: 16 additions & 0 deletions src/main/java/com/dodo/backend/report/entity/ReportStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.dodo.backend.report.entity;

/**
* 신고 처리 상태를 정의하는 열거형입니다.
*/
public enum ReportStatus {
/**
* 신고 접수 후 처리 대기 상태입니다.
*/
PENDING,

/**
* 신고 처리가 완료된 상태입니다.
*/
COMPLETED
}
Loading
Loading