diff --git a/backend/src/main/java/org/sejongisc/backend/board/controller/BoardController.java b/backend/src/main/java/org/sejongisc/backend/board/controller/BoardController.java index 0379572d..94d0d3ed 100644 --- a/backend/src/main/java/org/sejongisc/backend/board/controller/BoardController.java +++ b/backend/src/main/java/org/sejongisc/backend/board/controller/BoardController.java @@ -3,8 +3,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -import java.util.List; -import java.util.UUID; import lombok.RequiredArgsConstructor; import org.sejongisc.backend.board.dto.BoardResponse; import org.sejongisc.backend.board.dto.CommentRequest; @@ -17,16 +15,9 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.UUID; @RestController @RequiredArgsConstructor @@ -40,10 +31,12 @@ public class BoardController { private final PostService postService; private final PostInteractionService postInteractionService; - // 게시글 작성 + // 게시물 생성 @Operation( summary = "게시물 작성", - description = "게시판 ID, 제목, 내용, 첨부파일을 포함한 게시물을 작성합니다." + description = "게시판 ID, 제목, 내용, 첨부파일을 포함한 게시물 생성" + + "anonymous 값을 true로 보내면 익명 게시물 작성됨" + + "값을 보내지 않으면 기본값(false) 으로 설정됨" ) @PostMapping(value = "/post", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity createPost( @@ -54,12 +47,12 @@ public ResponseEntity createPost( return ResponseEntity.ok().build(); } - // 게시글 수정 + // 게시물 수정 @Operation( summary = "게시물 수정", - description = "제목, 내용, 첨부파일을 포함한 게시물을 수정합니다." - + "첨부파일은 전체 파일 삭제 후 재저장 방식으로 이루어집니다." - + "게시판 종류는 수정할 수 없습니다." + description = "제목, 내용, 첨부파일을 포함한 게시물 수정" + + "anonymous 값을 통해 익명 여부도 변경할 수 있음 " + + "첨부파일은 전체 교체 방식으로 처리되며, 게시판 종류는 변경할 수 없음" ) @PutMapping(value = "/post/{postId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity updatePost( @@ -71,12 +64,11 @@ public ResponseEntity updatePost( return ResponseEntity.ok().build(); } - // 게시글 삭제 + // 게시물 삭제 @Operation( - summary = "게시글 삭제", - description = "게시글 ID를 통해 게시글을 삭제합니다." - + "작성자 본인만 삭제할 수 있습니다." - + "관련 첨부파일 및 댓글 등도 함께 삭제됩니다." + summary = "게시물 삭제", + description = "게시물 ID를 통해 게시물 삭제" + + "작성자 본인만 삭제할 수 있으며, 관련 첨부파일과 댓글 정보도 함께 삭제" ) @DeleteMapping("/post/{postId}") public void deletePost( @@ -86,12 +78,11 @@ public void deletePost( postService.deletePost(postId, userId); } - // 게시글 조회 + // 게시물 조회 @Operation( - summary = "게시글 조회", - description = "게시판 ID를 통해 해당 게시판의 게시글 목록을 조회합니다." - + "페이지 번호와 페이지 크기를 통해 페이징 처리가 가능합니다." - + "기본값은 페이지 번호 0, 페이지 크기 20입니다." + summary = "게시물 조회", + description = "게시판 ID를 통해 해당 게시판의 게시물 목록 조회" + + "pageNumber와 pageSize로 페이징할 수 있으며 기본값은 각각 0, 20" ) @GetMapping("/posts") public ResponseEntity> getPosts( @@ -103,12 +94,11 @@ public ResponseEntity> getPosts( return ResponseEntity.ok(postService.getPosts(boardId, userId, pageNumber, pageSize)); } - // 게시글 검색 + // 게시물 검색 @Operation( - summary = "게시글 검색", - description = "게시판 ID와 키워드를 통해 해당 게시판의 게시글 목록을 검색합니다." - + "페이지 번호와 페이지 크기를 통해 페이징 처리가 가능합니다." - + "기본값은 페이지 번호 0, 페이지 크기 20입니다." + summary = "게시물 검색", + description = "게시판 ID와 검색어를 기준으로 제목 또는 내용에 검색어가 포함된 게시물 조회 " + + "pageNumber와 pageSize로 페이징할 수 있으며 기본값은 각각 0, 20" ) @GetMapping("/posts/search") public ResponseEntity> searchPosts( @@ -124,9 +114,9 @@ public ResponseEntity> searchPosts( // 게시물 상세 조회 @Operation( summary = "게시물 상세 조회", - description = "게시물 ID를 통해 게시물의 상세 정보를 조회합니다." - + "댓글에 대해서도 페이지 번호와 페이지 크기를 통해 페이징 처리가 가능합니다." - + "기본값은 댓글 페이지 번호 0, 댓글 페이지 크기 20입니다." + description = "게시물 ID를 통해 게시물의 상세 정보와 댓글 목록 조회 " + + "익명 게시물과 익명 댓글은 작성자 정보가 닉네임은 \"익명\" 그 외 회원정보는 null로 반환됨" + + "댓글은 commentPageNumber와 commentPageSize로 페이징할 수 있으며 기본값은 각각 0, 20" ) @GetMapping("/post/{postId}") public ResponseEntity getPostDetail( @@ -142,7 +132,7 @@ public ResponseEntity getPostDetail( // 최상위 게시판 목록 조회 @Operation( summary = "부모 게시판 목록 조회", - description = "최상위 부모 게시판들의 목록을 조회합니다." + description = "최상위 부모 게시판 목록 조회" ) @GetMapping("/parents") public ResponseEntity> getParentBoards( @@ -150,24 +140,22 @@ public ResponseEntity> getParentBoards( return ResponseEntity.ok(postService.getParentBoards()); } - // 하위 게시판 목록 조회 @Operation( - summary = "하위 게시판 목록 조회", - description = "하위 게시판 목록을 조회합니다." + summary = "하위 게시판 목록 조회", + description = "하위 게시판 목록 조회" ) @GetMapping("/childs") public ResponseEntity> getChildBoards( - @AuthenticationPrincipal CustomUserDetails customUserDetails) { + @AuthenticationPrincipal CustomUserDetails customUserDetails) { return ResponseEntity.ok(postService.getChildBoards()); } // 좋아요 토글 @Operation( summary = "좋아요 등록 및 취소", - description = "좋아요를 등록하거나 취소합니다. " - + "이미 좋아요를 한 게시물인 경우 좋아요가 취소되고, " - + "좋아요를 하지 않은 게시물인 경우 좋아요가 등록됩니다." + description = "게시물 좋아요를 토글" + + "이미 좋아요를 누른 게시물이면 취소되고, 누르지 않은 게시물이면 등록" ) @PostMapping("/{postId}/like") public ResponseEntity toggleLike( @@ -181,9 +169,8 @@ public ResponseEntity toggleLike( // 북마크 토글 @Operation( summary = "북마크 등록 및 취소", - description = "북마크를 등록하거나 취소합니다. " - + "이미 북마크를 한 게시물인 경우 북마크가 취소되고, " - + "북마크를 하지 않은 게시물인 경우 북마크가 등록됩니다." + description = "게시물 북마크를 토글 " + + "이미 북마크한 게시물이면 취소되고, 북마크하지 않은 게시물이면 등록" ) @PostMapping("/{postId}/bookmark") public ResponseEntity toggleBookmark( @@ -194,12 +181,12 @@ public ResponseEntity toggleBookmark( return ResponseEntity.ok().build(); } - // 댓글 작성 + // 댓글 생성 @Operation( summary = "댓글 작성", - description = "게시물에 댓글을 작성합니다." - + "parentCommentId가 제공되면 해당 댓글에 대한 대댓글로 작성됩니다." - + "null일 경우 일반 댓글로 작성됩니다." + description = "게시물에 댓글 작성" + + "anonymous 값을 true로 보내면 작성자 정보가 익명으로 표시 " + + "parentCommentId가 있으면 대댓글로 작성되고, 없으면 일반 댓글로 작성" ) @PostMapping("/comment") public ResponseEntity createComment( @@ -213,7 +200,8 @@ public ResponseEntity createComment( // 댓글 수정 @Operation( summary = "댓글 수정", - description = "댓글을 수정합니다. 작성자 본인만 수정할 수 있습니다." + description = "댓글 내용을 수정합니다. 작성자 본인만 수정할 수 있으며, " + + "content와 anonymous 값을 함께 변경할 수 있음" ) @PutMapping("/comment/{commentId}") public void updateComment( @@ -227,9 +215,8 @@ public void updateComment( // 댓글 삭제 @Operation( summary = "댓글 삭제", - description = "댓글을 삭제합니다. " - + "작성자 본인과 관리자만 삭제할 수 있습니다." - + "대댓글이 있는 경우 함께 삭제됩니다." + description = "댓글 삭제" + + "작성자 본인과 관리자만 삭제할 수 있으며, 대댓글이 있는 경우 함께 삭제됨" ) @DeleteMapping("/comment/{commentId}") public void deleteComment( @@ -238,7 +225,4 @@ public void deleteComment( UUID userId = customUserDetails.getUserId(); postInteractionService.deleteComment(commentId, userId); } - - - } diff --git a/backend/src/main/java/org/sejongisc/backend/board/dto/CommentRequest.java b/backend/src/main/java/org/sejongisc/backend/board/dto/CommentRequest.java index b54055ea..dac78cee 100644 --- a/backend/src/main/java/org/sejongisc/backend/board/dto/CommentRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/board/dto/CommentRequest.java @@ -25,6 +25,9 @@ public class CommentRequest { @NotBlank(message = "댓글 내용은 필수 항목입니다.") private String content; + @Builder.Default + private boolean anonymous = false; + @Schema(description = "부모 댓글 ID (대댓글인 경우에만 필요)") private UUID parentCommentId; } diff --git a/backend/src/main/java/org/sejongisc/backend/board/dto/CommentResponse.java b/backend/src/main/java/org/sejongisc/backend/board/dto/CommentResponse.java index a3f9e4c5..410e9cc7 100644 --- a/backend/src/main/java/org/sejongisc/backend/board/dto/CommentResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/board/dto/CommentResponse.java @@ -19,14 +19,22 @@ @Setter @Builder public class CommentResponse { - private UUID commentId; - private UserInfoResponse user; - private UUID postId; - private String content; - private LocalDateTime createdDate; - private LocalDateTime updatedDate; - private UUID parentCommentId; - private List replies; + private UUID commentId; + private UserInfoResponse user; + private UUID postId; + private String content; + private boolean anonymous; + private LocalDateTime createdDate; + private LocalDateTime updatedDate; + private UUID parentCommentId; + private List replies; + + private static UserInfoResponse getCommentUser(Comment comment) { + if (comment.isAnonymous()) { + return new UserInfoResponse(null, "익명", null, null, null, null, List.of()); + } + return UserInfoResponse.from(comment.getUser()); + } public static CommentResponse from(Comment comment) { UUID parentId = (comment.getParentComment() != null) @@ -35,9 +43,10 @@ public static CommentResponse from(Comment comment) { return CommentResponse.builder() .commentId(comment.getCommentId()) - .user(UserInfoResponse.from(comment.getUser())) + .user(getCommentUser(comment)) .postId(comment.getPost().getPostId()) .content(comment.getContent()) + .anonymous(comment.isAnonymous()) .createdDate(comment.getCreatedDate()) .updatedDate(comment.getUpdatedDate()) .parentCommentId(parentId) @@ -51,9 +60,10 @@ public static CommentResponse from(Comment comment, List replie return CommentResponse.builder() .commentId(comment.getCommentId()) - .user(UserInfoResponse.from(comment.getUser())) + .user(getCommentUser(comment)) .postId(comment.getPost().getPostId()) .content(comment.getContent()) + .anonymous(comment.isAnonymous()) .createdDate(comment.getCreatedDate()) .updatedDate(comment.getUpdatedDate()) .parentCommentId(parentCommentId) diff --git a/backend/src/main/java/org/sejongisc/backend/board/dto/PostRequest.java b/backend/src/main/java/org/sejongisc/backend/board/dto/PostRequest.java index e5134d32..6d218959 100644 --- a/backend/src/main/java/org/sejongisc/backend/board/dto/PostRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/board/dto/PostRequest.java @@ -29,5 +29,8 @@ public class PostRequest { @NotBlank(message = "내용은 필수 항목입니다.") private String content; + @Builder.Default + private boolean anonymous = false; + private List files; } diff --git a/backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java b/backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java index 3c60b94f..6768b5b9 100644 --- a/backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/board/dto/PostResponse.java @@ -25,6 +25,7 @@ public class PostResponse { private UserInfoResponse user; private String title; private String content; + private boolean anonymous; private Integer bookmarkCount; private Integer likeCount; private Integer commentCount; diff --git a/backend/src/main/java/org/sejongisc/backend/board/entity/Comment.java b/backend/src/main/java/org/sejongisc/backend/board/entity/Comment.java index 34adfc46..52b89485 100644 --- a/backend/src/main/java/org/sejongisc/backend/board/entity/Comment.java +++ b/backend/src/main/java/org/sejongisc/backend/board/entity/Comment.java @@ -42,6 +42,10 @@ public class Comment extends BasePostgresEntity { @Column(columnDefinition = "TEXT", nullable = false) private String content; + @Builder.Default + @Column(nullable = false, columnDefinition = "boolean default false") + private boolean anonymous = false; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_comment_id") private Comment parentComment; diff --git a/backend/src/main/java/org/sejongisc/backend/board/entity/Post.java b/backend/src/main/java/org/sejongisc/backend/board/entity/Post.java index 610aa1a1..7b7333ee 100644 --- a/backend/src/main/java/org/sejongisc/backend/board/entity/Post.java +++ b/backend/src/main/java/org/sejongisc/backend/board/entity/Post.java @@ -51,6 +51,10 @@ public class Post extends BasePostgresEntity { @Column(columnDefinition = "TEXT", nullable = false) private String content; + @Builder.Default + @Column(nullable = false, columnDefinition = "boolean default false") + private boolean anonymous = false; + // 북마크 수 @Builder.Default private Integer bookmarkCount = 0; diff --git a/backend/src/main/java/org/sejongisc/backend/board/service/PostInteractionService.java b/backend/src/main/java/org/sejongisc/backend/board/service/PostInteractionService.java index d8f1dc87..6e8fa60a 100644 --- a/backend/src/main/java/org/sejongisc/backend/board/service/PostInteractionService.java +++ b/backend/src/main/java/org/sejongisc/backend/board/service/PostInteractionService.java @@ -1,9 +1,6 @@ package org.sejongisc.backend.board.service; import jakarta.persistence.OptimisticLockException; -import java.util.List; -import java.util.Optional; -import java.util.UUID; import lombok.RequiredArgsConstructor; import org.sejongisc.backend.activity.entity.ActivityType; import org.sejongisc.backend.activity.event.ActivityEvent; @@ -18,9 +15,9 @@ import org.sejongisc.backend.board.repository.PostRepository; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; -import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; +import org.sejongisc.backend.user.repository.UserRepository; import org.springframework.context.ApplicationEventPublisher; import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.retry.annotation.Backoff; @@ -28,6 +25,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + @Service @RequiredArgsConstructor @Transactional @@ -77,6 +78,7 @@ public void createComment(CommentRequest request, UUID userId) { .post(post) .user(user) .content(request.getContent()) + .anonymous(request.isAnonymous()) .parentComment(parentComment) .build(); @@ -85,9 +87,11 @@ public void createComment(CommentRequest request, UUID userId) { // 게시글의 댓글 수 1 증가 post.setCommentCount(post.getCommentCount() + 1); + String username = request.isAnonymous() ? "익명" : user.getName(); + eventPublisher.publishEvent(new ActivityEvent( user.getUserId(), - user.getName(), + username, ActivityType.BOARD_COMMENT, "[" + post.getTitle() + "]에 댓글을 달았습니다.", post.getPostId(), @@ -108,6 +112,7 @@ public void updateComment(CommentRequest request, UUID commentId, UUID userId) { // 내용 업데이트 comment.setContent(request.getContent()); + comment.setAnonymous(request.isAnonymous()); } // 댓글 삭제 diff --git a/backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java b/backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java index 3e5ed7ad..e9020b46 100644 --- a/backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java +++ b/backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java @@ -1,26 +1,15 @@ package org.sejongisc.backend.board.service; -import java.util.List; -import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.activity.entity.ActivityType; import org.sejongisc.backend.activity.event.ActivityEvent; -import org.sejongisc.backend.board.dto.BoardResponse; -import org.sejongisc.backend.board.dto.CommentResponse; -import org.sejongisc.backend.board.dto.PostAttachmentResponse; -import org.sejongisc.backend.board.dto.PostRequest; -import org.sejongisc.backend.board.dto.PostResponse; +import org.sejongisc.backend.board.dto.*; import org.sejongisc.backend.board.entity.Board; import org.sejongisc.backend.board.entity.Comment; import org.sejongisc.backend.board.entity.Post; import org.sejongisc.backend.board.entity.PostAttachment; -import org.sejongisc.backend.board.repository.BoardRepository; -import org.sejongisc.backend.board.repository.CommentRepository; -import org.sejongisc.backend.board.repository.PostAttachmentRepository; -import org.sejongisc.backend.board.repository.PostBookmarkRepository; -import org.sejongisc.backend.board.repository.PostLikeRepository; -import org.sejongisc.backend.board.repository.PostRepository; +import org.sejongisc.backend.board.repository.*; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; import org.sejongisc.backend.user.dto.UserInfoResponse; @@ -36,6 +25,9 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.util.List; +import java.util.UUID; + @Service @RequiredArgsConstructor @Slf4j @@ -70,13 +62,17 @@ public void savePost(PostRequest request, UUID userId) { .board(board) .title(request.getTitle()) .content(request.getContent()) + .anonymous(request.isAnonymous()) .build(); post = postRepository.save(post); + User user = post.getUser(); + String username = request.isAnonymous() ? "익명" : user.getName(); + eventPublisher.publishEvent(new ActivityEvent( userId, - user.getName(), + username, ActivityType.BOARD_POST, "[" + post.getTitle() + "]글을 게시했습니다.", post.getPostId(), @@ -116,6 +112,7 @@ public void updatePost(PostRequest request, UUID postId, UUID userId) { post.setTitle(request.getTitle()); post.setContent(request.getContent()); + post.setAnonymous(request.isAnonymous()); // 기존 파일 조회 및 삭제 List existingAttachments = postAttachmentRepository.findAllByPostPostId(postId); @@ -307,7 +304,8 @@ public List getChildBoards() { private PostResponse.PostResponseBuilder getCommonPostBuilder(Post post, User user) { return PostResponse.builder() .postId(post.getPostId()) - .user(UserInfoResponse.from(post.getUser())) + .user(post.isAnonymous() ? getAnonymousUserInfo() : UserInfoResponse.from(post.getUser())) + .anonymous(post.isAnonymous()) .board(BoardResponse.from(post.getBoard())) .title(post.getTitle()) .content(post.getContent()) @@ -320,6 +318,14 @@ private PostResponse.PostResponseBuilder getCommonPostBuilder(Post post, User us .isBookmarked(postBookmarkRepository.existsByUserUserIdAndPostPostId(user.getUserId(), post.getPostId())); } + /** + * 회원 이름 "익명" 처리 + * 그 외 정보 모두 null로 + */ + private UserInfoResponse getAnonymousUserInfo() { + return new UserInfoResponse(null, "익명", null, null, null, null, List.of()); + } + private PostResponse mapToPostResponse(Post post, User user) { return getCommonPostBuilder(post, user) .build();