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 @@ -44,6 +44,7 @@ public enum ErrorCode {
LANGUAGE_TEST_SCORE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 어학성적입니다."),
NEWS_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 소식지입니다."),
MENTOR_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 멘토입니다."),
REPORT_TARGET_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 신고 대상입니다."),

// auth
USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."),
Expand Down Expand Up @@ -78,7 +79,7 @@ public enum ErrorCode {
// community
INVALID_POST_CATEGORY(HttpStatus.BAD_REQUEST.value(), "잘못된 카테고리명입니다."),
INVALID_BOARD_CODE(HttpStatus.BAD_REQUEST.value(), "잘못된 게시판 코드입니다."),
INVALID_POST_ID(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 게시글입니다."),
INVALID_POST_ID(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 게시글입니다."), // todo: NOT_FOUND로 통일 필요
INVALID_POST_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 게시글만 제어할 수 있습니다."),
CAN_NOT_DELETE_OR_UPDATE_QUESTION(HttpStatus.BAD_REQUEST.value(), "질문글은 수정이나 삭제할 수 없습니다."),
CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES(HttpStatus.BAD_REQUEST.value(), "5개 이상의 파일을 업로드할 수 없습니다."),
Expand Down Expand Up @@ -111,6 +112,9 @@ public enum ErrorCode {
UNAUTHORIZED_MENTORING(HttpStatus.FORBIDDEN.value(), "멘토링 권한이 없습니다."),
MENTORING_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "이미 승인 또는 거절된 멘토링입니다."),

// report
ALREADY_REPORTED_BY_CURRENT_USER(HttpStatus.BAD_REQUEST.value(), "이미 신고한 상태입니다."),

// database
DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT.value(), "데이터베이스 무결성 제약조건 위반이 발생했습니다."),

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.example.solidconnection.report.controller;

import com.example.solidconnection.common.resolver.AuthorizedUser;
import com.example.solidconnection.report.dto.ReportRequest;
import com.example.solidconnection.report.service.ReportService;
import com.example.solidconnection.siteuser.domain.SiteUser;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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;

@RestController
@RequiredArgsConstructor
@RequestMapping("/reports")
public class ReportController {

private final ReportService reportService;

@PostMapping
public ResponseEntity<Void> createReport(
@AuthorizedUser SiteUser siteUser,
@Valid @RequestBody ReportRequest reportRequest
) {
reportService.createReport(siteUser.getId(), reportRequest);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.example.solidconnection.report.domain;

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.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(uniqueConstraints = {
@UniqueConstraint(
name = "uk_report_reporter_id_target_type_target_id",
columnNames = {"reporter_id", "target_type", "target_id"}
)
})
public class Report {

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

@Column(name = "reporter_id")
private long reporterId;

@Column(name = "report_type")
@Enumerated(value = EnumType.STRING)
private ReportType reportType;

@Column(name = "target_type")
@Enumerated(value = EnumType.STRING)
private TargetType targetType;

@Column(name = "target_id")
private long targetId;

public Report(long reporterId, ReportType reportType, TargetType targetType, long targetId) {
this.reportType = reportType;
this.reporterId = reporterId;
this.targetType = targetType;
this.targetId = targetId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.solidconnection.report.domain;

public enum ReportType {

ADVERTISEMENT, // 광고
SPAM, // 낚시/도배
PERSONAL_INFO_EXPOSURE, // 개인정보 노출
PORNOGRAPHY, // 선정성
COPYRIGHT_INFRINGEMENT, // 저작권 침해
ILLEGAL_ACTIVITY, // 불법 행위
IMPERSONATION, // 사칭/도용
INSULT, // 욕설/비하
;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.solidconnection.report.domain;

public enum TargetType {

POST,
;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.solidconnection.report.dto;

import com.example.solidconnection.report.domain.ReportType;
import com.example.solidconnection.report.domain.TargetType;
import jakarta.validation.constraints.NotNull;

public record ReportRequest(
@NotNull(message = "신고 유형을 선택해주세요.")
ReportType reportType,

@NotNull(message = "신고 대상을 포함해주세요.")
TargetType targetType,

long targetId
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

targetId 필드에 검증 로직 추가를 고려해보세요.

현재 targetId에는 검증 어노테이션이 없어 음수나 0값이 들어올 수 있습니다.

+        @Positive(message = "유효한 대상 ID를 입력해주세요.")
         long targetId

필요한 import도 추가해주세요:

+import jakarta.validation.constraints.Positive;
🤖 Prompt for AI Agents
In src/main/java/com/example/solidconnection/report/dto/ReportRequest.java at
line 14, the targetId field lacks validation annotations, allowing invalid
values like negative numbers or zero. Add a validation annotation such as
@Positive to ensure targetId is a positive number, and include the necessary
import for this annotation from javax.validation.constraints or
jakarta.validation.constraints depending on the project setup.

) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.solidconnection.report.repository;

import com.example.solidconnection.report.domain.Report;
import com.example.solidconnection.report.domain.TargetType;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ReportRepository extends JpaRepository<Report, Long> {

boolean existsByReporterIdAndTargetTypeAndTargetId(long reporterId, TargetType targetType, long targetId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.example.solidconnection.report.service;

import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.common.exception.ErrorCode;
import com.example.solidconnection.community.post.repository.PostRepository;
import com.example.solidconnection.report.domain.Report;
import com.example.solidconnection.report.domain.TargetType;
import com.example.solidconnection.report.dto.ReportRequest;
import com.example.solidconnection.report.repository.ReportRepository;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ReportService {

private final ReportRepository reportRepository;
private final SiteUserRepository siteUserRepository;
private final PostRepository postRepository;

@Transactional
public void createReport(long reporterId, ReportRequest request) {
validateReporterExists(reporterId);
validateTargetExists(request.targetType(), request.targetId());
validateFirstReportByUser(reporterId, request.targetType(), request.targetId());

Report report = new Report(reporterId, request.reportType(), request.targetType(), request.targetId());
reportRepository.save(report);
}

private void validateReporterExists(long reporterId) {
if (!siteUserRepository.existsById(reporterId)) {
throw new CustomException(ErrorCode.USER_NOT_FOUND);
}
}

private void validateTargetExists(TargetType targetType, long targetId) {
if (targetType == TargetType.POST && !postRepository.existsById(targetId)) {
throw new CustomException(ErrorCode.REPORT_TARGET_NOT_FOUND);
}
}

private void validateFirstReportByUser(long reporterId, TargetType targetType, long targetId) {
if (reportRepository.existsByReporterIdAndTargetTypeAndTargetId(reporterId, targetType, targetId)) {
throw new CustomException(ErrorCode.ALREADY_REPORTED_BY_CURRENT_USER);
}
}
}
11 changes: 11 additions & 0 deletions src/main/resources/db/migration/V24__create_report_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE report
(
id BIGINT NOT NULL AUTO_INCREMENT,
reporter_id BIGINT NOT NULL,
target_type ENUM ('POST') NOT NULL,
target_id BIGINT NOT NULL,
report_type ENUM ('ADVERTISEMENT', 'SPAM', 'PERSONAL_INFO_EXPOSURE', 'PORNOGRAPHY', 'COPYRIGHT_INFRINGEMENT', 'ILLEGAL_ACTIVITY', 'IMPERSONATION', 'INSULT') NOT NULL,
primary key (id),
constraint fk_report_reporter_id foreign key (reporter_id) references site_user (id),
unique uk_report_reporter_id_target_type_target_id (reporter_id, target_type, target_id)
);
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ public class PostFixture {

private final PostFixtureBuilder postFixtureBuilder;

public Post 게시글(
Board board,
SiteUser siteUser
) {
return postFixtureBuilder
.title("제목")
.content("내용")
.isQuestion(false)
.likeCount(0L)
.postCategory(PostCategory.자유)
.board(board)
.siteUser(siteUser)
.create();
}

public Post 게시글(
String title,
String content,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.solidconnection.report.fixture;

import com.example.solidconnection.report.domain.Report;
import com.example.solidconnection.report.domain.TargetType;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.test.context.TestComponent;

@TestComponent
@RequiredArgsConstructor
public class ReportFixture {

private final ReportFixtureBuilder reportFixtureBuilder;

public Report 신고(long reporterId, TargetType targetType, long targetId) {
return reportFixtureBuilder.report()
.reporterId(reporterId)
.targetType(targetType)
.targetId(targetId)
.create();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.example.solidconnection.report.fixture;

import com.example.solidconnection.report.domain.Report;
import com.example.solidconnection.report.domain.ReportType;
import com.example.solidconnection.report.domain.TargetType;
import com.example.solidconnection.report.repository.ReportRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.test.context.TestComponent;

@TestComponent
@RequiredArgsConstructor
public class ReportFixtureBuilder {

private final ReportRepository reportRepository;

private long reporterId;
private TargetType targetType;
private long targetId;
private ReportType reportType = ReportType.ADVERTISEMENT;

public ReportFixtureBuilder report() {
return new ReportFixtureBuilder(reportRepository);
}

public ReportFixtureBuilder reporterId(long reporterId) {
this.reporterId = reporterId;
return this;
}

public ReportFixtureBuilder targetType(TargetType targetType) {
this.targetType = targetType;
return this;
}

public ReportFixtureBuilder targetId(long targetId) {
this.targetId = targetId;
return this;
}

public ReportFixtureBuilder reasonType(ReportType reportType) {
this.reportType = reportType;
return this;
}

public Report create() {
Report report = new Report(
reporterId,
reportType,
targetType,
targetId
);
return reportRepository.save(report);
}
}
Loading
Loading