Skip to content
Open
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 @@ -65,4 +65,13 @@ public ApiResponse<Void> deletePost(
return ApiResponse.onSuccess("POST200_4", "게시글 삭제에 성공했습니다.", null);
}

// 6. 게시글 숨기기
@PatchMapping("/{postId}/hide")
public ApiResponse<Void> hidePost(
@PathVariable Long postId,
@RequestParam Long userId // 실제로는 인증된 유저 정보를 사용해야 함
) {
postService.hidePost(postId, userId);
return ApiResponse.onSuccess("POST200_5", "게시글이 숨김 처리되었습니다.", null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public class Post extends BaseEntity {
@Column(name = "title", nullable = false)
private String title;

@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private PostStatus status = PostStatus.ACTIVE;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id") // FK 컬럼명 명시
private User user;
Expand All @@ -34,6 +38,20 @@ public class Post extends BaseEntity {
private Post(String title, User user) {
this.title = title;
this.user = user;
this.status = PostStatus.ACTIVE;
}


public void hideByUser() {
this.status = PostStatus.HIDDEN_BY_USER;
}

public void hideByAdmin() {
this.status = PostStatus.HIDDEN_BY_ADMIN;
}

public void unhide() {
this.status = PostStatus.ACTIVE;
}

public void softDelete() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.leets.assignment.domain.post.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum PostStatus {
ACTIVE("Active"),
HIDDEN_BY_USER("Hidden by user"),
HIDDEN_BY_ADMIN("Hidden by admin");

private final String description;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.leets.assignment.domain.post.dto.res.PostResponseDTO;
import com.leets.assignment.domain.post.entity.Post;
import com.leets.assignment.domain.post.entity.PostBlock;
import com.leets.assignment.domain.post.entity.PostStatus;
import com.leets.assignment.domain.post.exception.code.PostErrorCode;
import com.leets.assignment.domain.post.exception.PostException;
import com.leets.assignment.domain.post.repository.PostRepository;
Expand Down Expand Up @@ -62,6 +63,7 @@ public PostResponseDTO.PostDetailResDTO getPost(Long postId) {
// 2. 데이터가 없으면 PostNotFoundException 예외 발생! (-> 404 응답)
Post post = postRepository.findById(postId)
.filter(p -> p.getDeletedAt() == null) // 삭제 안 된 것만 필터링
.filter(p -> p.getStatus() == PostStatus.ACTIVE) // 활성 상태 필터링
.orElseThrow(() -> new PostException(PostErrorCode.POST_NOT_FOUND));

// 3. 찾은 엔티티를 DTO로 변환하여 반환
Expand All @@ -73,6 +75,7 @@ public List<PostResponseDTO.PostListResDTO> getPostList() {
// DB의 모든 글을 가져와서 ListResDTO로 변환
return postRepository.findAll().stream()
.filter(post -> post.getDeletedAt() == null) // 삭제된 글 제외
.filter(post -> post.getStatus() == PostStatus.ACTIVE) // 활성 상태 필터링
.map(PostResponseDTO.PostListResDTO::from)
.collect(Collectors.toList());
}
Expand Down Expand Up @@ -102,15 +105,20 @@ public PostResponseDTO.PostDetailResDTO updatePost(Long postId, PostRequestDTO.U
.filter(p -> p.getDeletedAt() == null)
.orElseThrow(() -> new PostException(PostErrorCode.POST_NOT_FOUND));

// 2. 수정 권한 확인
// 2. 관리자가 숨긴 글은 수정 불가
if (post.getStatus() == PostStatus.HIDDEN_BY_ADMIN) {
throw new PostException(PostErrorCode.POST_FORBIDDEN); // 혹은 전용 에러 코드 사용
}

// 3. 수정 권한 확인
if (!post.getUser().getUserId().equals(request.getUserId())) {
throw new PostException(PostErrorCode.POST_FORBIDDEN);
}

// 3. 제목 수정 (Dirty Checking)
// 4. 제목 수정 (Dirty Checking)
post.update(request.getTitle());

// 4. 블록 수정 (기존 블록 비우고 새로 추가)
// 5. 블록 수정 (기존 블록 비우고 새로 추가)
post.getBlocks().clear();
request.getBlocks().forEach(blockDto -> {
PostBlock block = PostBlock.builder()
Expand All @@ -124,4 +132,25 @@ public PostResponseDTO.PostDetailResDTO updatePost(Long postId, PostRequestDTO.U

return PostResponseDTO.PostDetailResDTO.from(post);
}

// 게시글 숨김
@Transactional
public void hidePost(Long postId, Long userId) {

// 1. 게시글 존재 및 삭제 여부 확인
Post post = postRepository.findById(postId)
.filter(p -> p.getDeletedAt() == null)
.orElseThrow(() -> new PostException(PostErrorCode.POST_NOT_FOUND));

// 2. 권한 확인 (관리자 기능이 없다면 우선 작성자 혹은 특정 조건 확인)
// post.getUser()가 null인지 먼저 확인하거나, equals를 활용
if (post.getUser() == null || !post.getUser().getUserId().equals(userId)) {
throw new PostException(PostErrorCode.POST_FORBIDDEN);
}

// 3. 상태 변경 (ACTIVE -> HIDDEN)
post.hideByUser();
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.leets.assignment.domain.report.controller;

import com.leets.assignment.domain.report.dto.req.PostReportRequestDTO;
import com.leets.assignment.domain.report.dto.res.PostReportResponseDTO;
import com.leets.assignment.domain.report.service.PostReportService;
import com.leets.assignment.global.common.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/posts")
@RequiredArgsConstructor
public class PostReportController {

private final PostReportService postReportService;

@PostMapping("/{postId}/reports")
public ApiResponse<PostReportResponseDTO> createReport(
@PathVariable Long postId,
@Valid @RequestBody PostReportRequestDTO request) {

PostReportResponseDTO response = postReportService.reportPost(postId, request.reporterId(), request.reason());
return ApiResponse.onSuccess("REPORT201", "신고가 정상적으로 접수되었습니다.", response);
}

@PatchMapping("/reports/{reportId}/resolve")
public ApiResponse<PostReportResponseDTO> resolveReport(
@PathVariable Long reportId
) {
PostReportResponseDTO response = postReportService.resolveReport(reportId);
return ApiResponse.onSuccess("REPORT200_1", "신고 처리가 완료되어 해당 게시글이 숨겨졌습니다.", response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.leets.assignment.domain.report.dto.req;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public record PostReportRequestDTO(
@NotNull(message = "신고자 ID는 필수입니다.")
Long reporterId,

@NotBlank(message = "신고 사유를 입력해주세요.")
String reason
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.leets.assignment.domain.report.dto.res;

import com.leets.assignment.domain.report.entity.ReportStatus;
import lombok.Builder;
import java.time.LocalDateTime;

@Builder
public record PostReportResponseDTO(
Long reportId,
Long postId,
Long reporterId,
String reason,
ReportStatus status,
LocalDateTime createdAt
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.leets.assignment.domain.report.entity;

import com.leets.assignment.domain.user.entity.User;
import com.leets.assignment.global.baseEntity.BaseEntity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@MappedSuperclass
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class BaseReport extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long reportId; // 신고 아이디

@Column(nullable = false)
private String reason; // 신고 사유

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ReportStatus status = ReportStatus.PENDING; // 신고 상태 (기본값: 대기중)

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

protected BaseReport(User reporter, String reason) {
this.reporter = reporter;
this.reason = reason;
this.status = ReportStatus.PENDING;
}

public void resolve() {
this.status = ReportStatus.RESOLVED;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.leets.assignment.domain.report.entity;

import com.leets.assignment.domain.post.entity.Post;
import com.leets.assignment.domain.user.entity.User;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "post_reports")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PostReport extends BaseReport {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

현재 설계에서는 report 타입 별로 데이터베이스 테이블을 따로 가지기 때문에 관리가 힘들고 새로운 도메인에 대한 신고 기능을 구현할 때 기존 코드를 재사용하기 힘들어 확장성이 떨어지는 단점이 있을 것 같습니다. 똑같은 기능을 구현할 때 다양한 설계와 장단점들을 고려해보는 것도 좋을 것 같습니다.


@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post; // 신고 대상 게시글

@Builder
public PostReport(User reporter, Post post, String reason) {
super(reporter, reason);
this.post = post;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.leets.assignment.domain.report.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ReportStatus {
PENDING("Pending"), // 대기 중
RESOLVED("Resolved"); // 처리 완료

private final String description;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.leets.assignment.domain.report.exception;

import com.leets.assignment.domain.report.exception.code.ReportErrorCode;
import lombok.Getter;

@Getter
public class ReportException extends RuntimeException {
private final ReportErrorCode errorCode;

public ReportException(ReportErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.leets.assignment.domain.report.exception.code;

import com.leets.assignment.global.exception.code.BaseErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum ReportErrorCode implements BaseErrorCode {
REPORT_DUPLICATED("REPORT409_1", "이미 신고한 게시글입니다.", HttpStatus.CONFLICT),
REPORT_SELF_FORBIDDEN("REPORT403_1", "자신의 게시글은 신고할 수 없습니다.", HttpStatus.FORBIDDEN),
REPORT_NOT_FOUND("REPORT404_1", "해당 신고 내역을 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
REPORT_ALREADY_RESOLVED("REPORT400_1", "이미 처리가 완료된 신고입니다.", HttpStatus.BAD_REQUEST);

private final String code;
private final String message;
private final HttpStatus httpStatus;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.leets.assignment.domain.report.repository;

import com.leets.assignment.domain.post.entity.Post;
import com.leets.assignment.domain.report.entity.PostReport;
import com.leets.assignment.domain.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PostReportRepository extends JpaRepository<PostReport, Long> {
// 특정 게시글의 전체 신고 횟수를 카운트하는 메서드
// Post 엔티티 필드명(post) + Post 엔티티 내부의 PK 필드명(PostId)
long countByPost_PostId(Long postId);
// 중복 신고 확인: 동일 신고자가 동일 게시글을 이미 신고했는지 체크
boolean existsByReporter_UserIdAndPost_PostId(Long userId, Long postId);
}
Loading