diff --git a/src/main/java/org/ezcode/codetest/application/report/dto/ReportChangeResponse.java b/src/main/java/org/ezcode/codetest/application/report/dto/ReportChangeResponse.java new file mode 100644 index 00000000..bf2a0096 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/report/dto/ReportChangeResponse.java @@ -0,0 +1,10 @@ +package org.ezcode.codetest.application.report.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ReportChangeResponse { + private String message; +} diff --git a/src/main/java/org/ezcode/codetest/application/report/dto/ReportDetailResponse.java b/src/main/java/org/ezcode/codetest/application/report/dto/ReportDetailResponse.java new file mode 100644 index 00000000..03028356 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/report/dto/ReportDetailResponse.java @@ -0,0 +1,14 @@ +package org.ezcode.codetest.application.report.dto; + +public record ReportDetailResponse( + Long reportId, + String reporterEmail, + Long targetId, + String targetType, + String reportType, + String reportStatus, + String message, + String imageUrl, + String createdAt, + String modifiedAt +) {} diff --git a/src/main/java/org/ezcode/codetest/application/report/dto/ReportListResponse.java b/src/main/java/org/ezcode/codetest/application/report/dto/ReportListResponse.java new file mode 100644 index 00000000..c03330e4 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/report/dto/ReportListResponse.java @@ -0,0 +1,12 @@ +package org.ezcode.codetest.application.report.dto; + +public record ReportListResponse( + Long reportId, + String reporterEmail, + Long targetId, + String targetType, + String reportType, + String reportStatus, + String message, + String createdAt +) {} diff --git a/src/main/java/org/ezcode/codetest/application/report/dto/ReportRequest.java b/src/main/java/org/ezcode/codetest/application/report/dto/ReportRequest.java new file mode 100644 index 00000000..06bf5f36 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/report/dto/ReportRequest.java @@ -0,0 +1,20 @@ +package org.ezcode.codetest.application.report.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record ReportRequest( + @NotNull + Long targetId, + + @NotBlank + String targetType, + + @NotBlank + String reportType, + + @NotBlank + String message, + + String imageUrl +) {} diff --git a/src/main/java/org/ezcode/codetest/application/report/dto/ReportResponse.java b/src/main/java/org/ezcode/codetest/application/report/dto/ReportResponse.java new file mode 100644 index 00000000..b3c168d4 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/report/dto/ReportResponse.java @@ -0,0 +1,12 @@ +package org.ezcode.codetest.application.report.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "ReportResponse", description = "신고 DTO") +public record ReportResponse( + Long reportId, + String status, + String type, + String message, + String imageUrl +) {} diff --git a/src/main/java/org/ezcode/codetest/application/report/dto/ReportStatusUpdateRequest.java b/src/main/java/org/ezcode/codetest/application/report/dto/ReportStatusUpdateRequest.java new file mode 100644 index 00000000..4086cc0f --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/report/dto/ReportStatusUpdateRequest.java @@ -0,0 +1,13 @@ +package org.ezcode.codetest.application.report.dto; + +import jakarta.validation.constraints.NotBlank; +import org.ezcode.codetest.domain.report.model.ReportStatus; + +public record ReportStatusUpdateRequest( + @NotBlank + String newStatus // 예시 RESOLVED(해결됨), REJECTED(거절) +) { + public ReportStatus toEnum() { + return ReportStatus.from(newStatus); + } +} diff --git a/src/main/java/org/ezcode/codetest/application/report/port/ReportRepository.java b/src/main/java/org/ezcode/codetest/application/report/port/ReportRepository.java index f32c1c5b..0609e5c7 100644 --- a/src/main/java/org/ezcode/codetest/application/report/port/ReportRepository.java +++ b/src/main/java/org/ezcode/codetest/application/report/port/ReportRepository.java @@ -1,4 +1,5 @@ package org.ezcode.codetest.application.report.port; + public interface ReportRepository { } diff --git a/src/main/java/org/ezcode/codetest/application/report/service/ReportService.java b/src/main/java/org/ezcode/codetest/application/report/service/ReportService.java new file mode 100644 index 00000000..ec69e644 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/report/service/ReportService.java @@ -0,0 +1,97 @@ +package org.ezcode.codetest.application.report.service; + +import org.ezcode.codetest.application.report.dto.*; +import org.ezcode.codetest.domain.report.model.*; +import org.ezcode.codetest.domain.report.repository.ReportRepository; +import org.ezcode.codetest.domain.user.model.entity.User; +import org.ezcode.codetest.domain.user.repository.UserRepository; +import org.springframework.data.domain.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ReportService { + + private final UserRepository userRepository; + private final ReportRepository reportRepository; + + //신규 신고 생성 + @Transactional + public ReportResponse submitReport(Long reporterId, ReportRequest request) { + User reporter = userRepository.findUserById(reporterId) + .orElseThrow(() -> new IllegalArgumentException("신고자 없음")); + + ReportTargetType targetType = ReportTargetType.valueOf(request.targetType().toUpperCase()); + ReportType reportType = ReportType.from(request.reportType()); + + if (targetType == ReportTargetType.USER && reporterId.equals(request.targetId())) { + throw new IllegalArgumentException("자기 자신은 신고할 수 없습니다."); + } + + Report report = new Report(reporter, + request.targetId(), + targetType, + request.message(), + request.imageUrl(), + reportType); + + reportRepository.save(report); + + return new ReportResponse( + report.getId(), + report.getReportStatus().name(), + report.getReportType().name(), + report.getMessage(), + report.getImageUrl() + ); + } + + //신고 목록 페이징 + @Transactional(readOnly = true) + public Page getReportList(Pageable pageable) { + return reportRepository.findAll(pageable) + .map(r -> new ReportListResponse( + r.getId(), + r.getReporter().getEmail(), + r.getTargetId(), + r.getTargetType().name(), + r.getReportType().name(), + r.getReportStatus().name(), + r.getMessage(), + r.getCreatedAt().toString() + )); + } + + //신고 상세 + @Transactional(readOnly = true) + public ReportDetailResponse getReportDetail(Long reportId) { + Report r = reportRepository.findById(reportId) + .orElseThrow(() -> new IllegalArgumentException("신고가 존재하지 않습니다.")); + + return new ReportDetailResponse( + r.getId(), + r.getReporter().getEmail(), + r.getTargetId(), + r.getTargetType().name(), + r.getReportType().name(), + r.getReportStatus().name(), + r.getMessage(), + r.getImageUrl(), + r.getCreatedAt().toString(), + r.getModifiedAt().toString() + ); + } + + //신고 상태 변경 + @Transactional + public void updateReportStatus(Long reportId, ReportStatusUpdateRequest req) { + Report report = reportRepository.findById(reportId) + .orElseThrow(() -> new IllegalArgumentException("신고가 존재하지 않습니다.")); + + ReportStatus newStatus = req.toEnum(); + report.updateStatus(newStatus); + } +} diff --git a/src/main/java/org/ezcode/codetest/domain/report/model/Report.java b/src/main/java/org/ezcode/codetest/domain/report/model/Report.java index 4a00517f..6144e598 100644 --- a/src/main/java/org/ezcode/codetest/domain/report/model/Report.java +++ b/src/main/java/org/ezcode/codetest/domain/report/model/Report.java @@ -1,59 +1,58 @@ package org.ezcode.codetest.domain.report.model; +import jakarta.persistence.*; import org.ezcode.codetest.common.base.entity.BaseEntity; import org.ezcode.codetest.domain.user.model.entity.User; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import jakarta.persistence.ManyToOne; -@Getter @Entity @Table(name = "report") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter public class Report extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "reporter_id", nullable = false) private User reporter; - @ManyToOne - @JoinColumn(name = "target_id", nullable = false) - private User target; + @Column(nullable = false) + private Long targetId; + @Column(nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private ReportTargetType targetType; @Column(nullable = false) private String message; - @Column(nullable = false) private String imageUrl; @Enumerated(EnumType.STRING) - @Column(nullable = false) + @Column(nullable = false, length = 50) private ReportType reportType; + @Column(nullable = false, length = 50) @Enumerated(EnumType.STRING) private ReportStatus reportStatus; - public Report(User reporter, User target, String message, String imageUrl, - ReportType reportType) { + public Report(User reporter, Long targetId, ReportTargetType targetType, + String message, String imageUrl, ReportType reportType) { this.reporter = reporter; - this.target = target; + this.targetId = targetId; + this.targetType = targetType; this.message = message; this.imageUrl = imageUrl; this.reportType = reportType; this.reportStatus = ReportStatus.PENDING; } + + public void updateStatus(ReportStatus newStatus) { + this.reportStatus = newStatus; + } + } diff --git a/src/main/java/org/ezcode/codetest/domain/report/model/ReportStatus.java b/src/main/java/org/ezcode/codetest/domain/report/model/ReportStatus.java index 47072481..f85899ab 100644 --- a/src/main/java/org/ezcode/codetest/domain/report/model/ReportStatus.java +++ b/src/main/java/org/ezcode/codetest/domain/report/model/ReportStatus.java @@ -2,15 +2,16 @@ import java.util.Arrays; +import com.fasterxml.jackson.annotation.JsonCreator; import lombok.Getter; @Getter public enum ReportStatus { - PENDING("대기 중"), // 대기 중 - IN_PROGRESS("검토 중"), // 검토 중 - RESOLVED("조치 완료"), // 조치 완료 - REJECTED("기각됨"), // 기각됨 - CANCELED("사용자 철회"); // 사용자 철회 (옵션); + PENDING("대기 중"), + IN_PROGRESS("검토 중"), + RESOLVED("조치 완료"), + REJECTED("기각됨"), + CANCELED("사용자 철회"); private final String description; @@ -18,10 +19,11 @@ public enum ReportStatus { this.description = description; } + @JsonCreator public static ReportStatus from(String reportStatus) { return Arrays.stream(ReportStatus.values()) .filter(r -> r.name().equalsIgnoreCase(reportStatus)) .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Invalid reportStatus input : " + reportStatus)); + .orElseThrow(() -> new IllegalArgumentException("목록에 없는 신고 상태 입니다. : " + reportStatus)); } } diff --git a/src/main/java/org/ezcode/codetest/domain/report/model/ReportTargetType.java b/src/main/java/org/ezcode/codetest/domain/report/model/ReportTargetType.java new file mode 100644 index 00000000..b678d3f1 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/domain/report/model/ReportTargetType.java @@ -0,0 +1,9 @@ +package org.ezcode.codetest.domain.report.model; + +public enum ReportTargetType { + USER, + PROBLEM, + POST, + COMMENT, + OTHER +} diff --git a/src/main/java/org/ezcode/codetest/domain/report/model/ReportType.java b/src/main/java/org/ezcode/codetest/domain/report/model/ReportType.java index 40b773eb..0ff5723d 100644 --- a/src/main/java/org/ezcode/codetest/domain/report/model/ReportType.java +++ b/src/main/java/org/ezcode/codetest/domain/report/model/ReportType.java @@ -6,6 +6,7 @@ @Getter public enum ReportType { + PROBLEM_ERROR("문제 오류"), PROFANITY("욕설/비속어"), SPAM("스팸/도배"), SEXUAL_CONTENT("음란성 표현"), @@ -23,10 +24,10 @@ public enum ReportType { this.description = description; } - public static ReportType from(String reportType) { - return Arrays.stream(ReportType.values()) - .filter(r -> r.name().equalsIgnoreCase(reportType)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Invalid reportType input : " + reportType)); + public static ReportType from(String value) { + return Arrays.stream(values()) + .filter(r -> r.name().equalsIgnoreCase(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("목록에 없는 신고 타입입니다. : " + value)); } -} +} \ No newline at end of file diff --git a/src/main/java/org/ezcode/codetest/domain/report/repository/ReportRepository.java b/src/main/java/org/ezcode/codetest/domain/report/repository/ReportRepository.java new file mode 100644 index 00000000..7b47ada8 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/domain/report/repository/ReportRepository.java @@ -0,0 +1,7 @@ +package org.ezcode.codetest.domain.report.repository; + +import org.ezcode.codetest.domain.report.model.Report; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReportRepository extends JpaRepository { +} diff --git a/src/main/java/org/ezcode/codetest/domain/report/service/ReportDomainService.java b/src/main/java/org/ezcode/codetest/domain/report/service/ReportDomainService.java index 20244499..99111a4a 100644 --- a/src/main/java/org/ezcode/codetest/domain/report/service/ReportDomainService.java +++ b/src/main/java/org/ezcode/codetest/domain/report/service/ReportDomainService.java @@ -1,4 +1,17 @@ package org.ezcode.codetest.domain.report.service; -public class ReportDomainService { +import lombok.RequiredArgsConstructor; +import org.ezcode.codetest.domain.report.model.ReportTargetType; +import org.ezcode.codetest.domain.user.model.entity.User; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ReportDomainService{ + + public void validate(User reporter, Long targetId, ReportTargetType targetType) { + if (targetType == ReportTargetType.USER && reporter.getId().equals(targetId)) { + throw new IllegalArgumentException("자기 자신은 신고할 수 없습니다."); + } + } } diff --git a/src/main/java/org/ezcode/codetest/presentation/ReportController.java b/src/main/java/org/ezcode/codetest/presentation/ReportController.java deleted file mode 100644 index 01c972fd..00000000 --- a/src/main/java/org/ezcode/codetest/presentation/ReportController.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.ezcode.codetest.presentation; - -public class ReportController { -} diff --git a/src/main/java/org/ezcode/codetest/presentation/report/ReportAdminController.java b/src/main/java/org/ezcode/codetest/presentation/report/ReportAdminController.java new file mode 100644 index 00000000..f67823ee --- /dev/null +++ b/src/main/java/org/ezcode/codetest/presentation/report/ReportAdminController.java @@ -0,0 +1,49 @@ +package org.ezcode.codetest.presentation.report; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.ezcode.codetest.application.report.dto.*; +import org.ezcode.codetest.application.report.service.ReportService; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.data.domain.*; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/reports/admin") +public class ReportAdminController { + + private final ReportService reportService; + + // 신고 목록 조회 + @GetMapping + public ResponseEntity> list( + @ParameterObject + @PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC) + Pageable pageable) { + + return ResponseEntity.ok(reportService.getReportList(pageable)); + } + + // 신고 조회 + @GetMapping("/{reportId}") + public ResponseEntity detail(@PathVariable Long reportId) { + return ResponseEntity.ok(reportService.getReportDetail(reportId)); + } + + // 신고 상태 변경 + @PatchMapping("/{reportId}/status") + public ResponseEntity changeStatus( + @PathVariable Long reportId, + @RequestBody @Valid ReportStatusUpdateRequest request) { + + reportService.updateReportStatus(reportId, request); + String message = request.newStatus() + "로 변경 완료되었습니다."; + return ResponseEntity.ok(new ReportChangeResponse(message)); + } + + +} diff --git a/src/main/java/org/ezcode/codetest/presentation/report/ReportController.java b/src/main/java/org/ezcode/codetest/presentation/report/ReportController.java new file mode 100644 index 00000000..c7da9840 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/presentation/report/ReportController.java @@ -0,0 +1,33 @@ +package org.ezcode.codetest.presentation.report; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ezcode.codetest.application.report.dto.ReportRequest; +import org.ezcode.codetest.application.report.dto.ReportResponse; +import org.ezcode.codetest.application.report.service.ReportService; +import org.ezcode.codetest.domain.user.model.entity.AuthUser; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/reports") +public class ReportController { + + private final ReportService reportService; + + @PostMapping + public ResponseEntity report( + @AuthenticationPrincipal AuthUser authUser, + @RequestBody @Valid ReportRequest request + ) { + return ResponseEntity.ok(reportService.submitReport(authUser.getId(), request)); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index eb5aebe7..a449d2d1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,7 @@ # ======================== # Spring Config # ======================== -spring.application.name=min +spring.application.name=ezcode spring.profiles.active=${SPRING_PROFILES_ACTIVE:dev} # ========================