diff --git a/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminBoardController.java b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminBoardController.java index f1a371b7..b3159d67 100644 --- a/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminBoardController.java +++ b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminBoardController.java @@ -1,4 +1,62 @@ package org.sejongisc.backend.admin.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.sejongisc.backend.board.dto.BoardRequest; +import org.sejongisc.backend.admin.service.AdminBoardService; +import org.sejongisc.backend.common.auth.dto.CustomUserDetails; +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.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; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/board/admin") +@Tag( + name = "게시판 관리 API", + description = "게시판 생성 및 삭제 관련 API 제공" +) public class AdminBoardController { + + private final AdminBoardService adminBoardService; + + // 게시판 생성 + @Operation( + summary = "게시판 생성", + description = "게시판 이름과 상위 게시판 ID를 포함한 새로운 게시판을 생성합니다." + + "상위 게시판의 ID가 null 이면 최상위 게시판으로 생성됩니다." + + "회장만 생성할 수 있습니다." + ) + @PostMapping + public ResponseEntity createBoard( + @RequestBody @Valid BoardRequest request, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { + UUID userId = customUserDetails.getUserId(); + adminBoardService.createBoard(request, userId); + return ResponseEntity.ok().build(); + } + + // 게시판 삭제 + @Operation( + summary = "게시판 삭제", + description = "게시판 ID를 통해 게시판을 삭제합니다." + + "회장만 삭제할 수 있습니다." + + "관련 첨부파일 및 댓글 등도 함께 삭제됩니다." + ) + @DeleteMapping("/{boardId}") + public ResponseEntity deleteBoard( + @PathVariable UUID boardId, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { + UUID userId = customUserDetails.getUserId(); + adminBoardService.deleteBoard(boardId, userId); + return ResponseEntity.ok("게시판 삭제가 완료되었습니다."); + } } diff --git a/backend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.java b/backend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.java new file mode 100644 index 00000000..14a3f3cf --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.java @@ -0,0 +1,91 @@ +package org.sejongisc.backend.admin.service; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.sejongisc.backend.board.dto.BoardRequest; +import org.sejongisc.backend.board.entity.Board; +import org.sejongisc.backend.board.repository.BoardRepository; +import org.sejongisc.backend.board.repository.PostRepository; +import org.sejongisc.backend.board.service.PostService; +import org.sejongisc.backend.common.exception.CustomException; +import org.sejongisc.backend.common.exception.ErrorCode; +import org.sejongisc.backend.user.entity.Role; +import org.sejongisc.backend.user.entity.User; +import org.sejongisc.backend.user.repository.UserRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class AdminBoardService { + + private final UserRepository userRepository; + private final PostRepository postRepository; + private final BoardRepository boardRepository; + private final PostService postService; + + // 게시판 생성 + @Transactional + public void createBoard(BoardRequest request, UUID userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // 회장만 게시판 생성 가능 + if (!user.getRole().equals(Role.PRESIDENT)) { + throw new CustomException(ErrorCode.BOARD_ACCESS_DENIED); + } + + Board board; + // 하위 게시판인 경우 + if (request.getParentBoardId() != null) { + Board parentBoard = boardRepository.findById(request.getParentBoardId()) + .orElseThrow(() -> new CustomException(ErrorCode.BOARD_NOT_FOUND)); + + board = Board.builder() + .boardName(request.getBoardName()) + .createdBy(user) + .parentBoard(parentBoard) + .build(); + } else { + // 상위 게시판인 경우 + board = Board.builder() + .boardName(request.getBoardName()) + .createdBy(user) + .parentBoard(null) + .build(); + } + + boardRepository.save(board); + } + + // 게시판 삭제 + @Transactional + public void deleteBoard(UUID boardId, UUID boardUserId) { + User user = userRepository.findById(boardUserId).orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // 회장만 게시판 삭제 가능 + if (!user.getRole().equals(Role.PRESIDENT)) { + throw new CustomException(ErrorCode.BOARD_ACCESS_DENIED); + } + + boardRepository.findById(boardId) + .orElseThrow(() -> new CustomException(ErrorCode.BOARD_NOT_FOUND)); + + // 상위 게시판이면 하위 게시판 목록을 조회 + List targetBoardIds = Stream.concat( + Stream.of(boardId), // 자신 포함 + boardRepository.findAllByParentBoard_BoardId(boardId).stream() + .map(Board::getBoardId) + ).toList(); + + // 각 boardId마다 postId/userId 조회해서 삭제 + targetBoardIds.stream() + .flatMap(id -> postRepository.findPostIdAndUserIdByBoardId(id).stream()) + .forEach(row -> postService.deletePost(row.getPostId(), row.getUserId())); + targetBoardIds.forEach(boardRepository::deleteById); + } +} 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 a1f1edf0..0379572d 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 @@ -6,7 +6,6 @@ import java.util.List; import java.util.UUID; import lombok.RequiredArgsConstructor; -import org.sejongisc.backend.board.dto.BoardRequest; import org.sejongisc.backend.board.dto.BoardResponse; import org.sejongisc.backend.board.dto.CommentRequest; import org.sejongisc.backend.board.dto.PostRequest; @@ -140,21 +139,6 @@ public ResponseEntity getPostDetail( return ResponseEntity.ok(response); } - // 게시판 생성 - @Operation( - summary = "게시판 생성", - description = "게시판 이름과 상위 게시판 ID를 포함한 새로운 게시판을 생성합니다." - + "상위 게시판의 ID가 null 이면 최상위 게시판으로 생성됩니다." - ) - @PostMapping - public ResponseEntity createBoard( - @RequestBody @Valid BoardRequest request, - @AuthenticationPrincipal CustomUserDetails customUserDetails) { - UUID userId = customUserDetails.getUserId(); - postService.createBoard(request, userId); - return ResponseEntity.ok().build(); - } - // 최상위 게시판 목록 조회 @Operation( summary = "부모 게시판 목록 조회", @@ -178,22 +162,6 @@ public ResponseEntity> getChildBoards( return ResponseEntity.ok(postService.getChildBoards()); } - // 게시판 삭제 - @Operation( - summary = "게시판 삭제", - description = "게시판 ID를 통해 게시판을 삭제합니다." - + "회장만 삭제할 수 있습니다." - + "관련 첨부파일 및 댓글 등도 함께 삭제됩니다." - ) - @DeleteMapping("/{boardId}") - public ResponseEntity deleteBoard( - @PathVariable UUID boardId, - @AuthenticationPrincipal CustomUserDetails customUserDetails) { - UUID userId = customUserDetails.getUserId(); - postService.deleteBoard(boardId, userId); - return ResponseEntity.ok("게시판 삭제가 완료되었습니다."); - } - // 좋아요 토글 @Operation( summary = "좋아요 등록 및 취소", diff --git a/backend/src/main/java/org/sejongisc/backend/board/service/PostService.java b/backend/src/main/java/org/sejongisc/backend/board/service/PostService.java index de0625f0..0087386d 100644 --- a/backend/src/main/java/org/sejongisc/backend/board/service/PostService.java +++ b/backend/src/main/java/org/sejongisc/backend/board/service/PostService.java @@ -2,7 +2,6 @@ import java.util.List; import java.util.UUID; -import org.sejongisc.backend.board.dto.BoardRequest; import org.sejongisc.backend.board.dto.BoardResponse; import org.sejongisc.backend.board.dto.PostRequest; import org.sejongisc.backend.board.dto.PostResponse; @@ -28,15 +27,9 @@ public interface PostService { // 게시물 상세 조회 PostResponse getPostDetail(UUID postId, UUID userId, int pageNumber, int pageSize); - // 게시판 생성 - void createBoard(BoardRequest request, UUID userId); - // 부모 게시판 목록 조회 List getParentBoards(); // 하위 게시판 목록 조회 List getChildBoards(); - - // 게시판 삭제 - void deleteBoard(UUID boardId, UUID boardUserId); } 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 382d8f91..34056d57 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 @@ -2,10 +2,8 @@ import java.util.List; import java.util.UUID; -import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.sejongisc.backend.board.dto.BoardRequest; import org.sejongisc.backend.board.dto.BoardResponse; import org.sejongisc.backend.board.dto.CommentResponse; import org.sejongisc.backend.board.dto.PostAttachmentResponse; @@ -23,10 +21,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.dto.UserInfoResponse; -import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.User; +import org.sejongisc.backend.user.repository.UserRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -171,30 +168,6 @@ public void deletePost(UUID postId, UUID userId) { postRepository.delete(post); } - // 게시판 삭제 - @Override - @Transactional - public void deleteBoard(UUID boardId, UUID boardUserId) { - User user = userRepository.findById(boardUserId).orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - if(!user.getRole().equals(Role.PRESIDENT)){ - throw new CustomException(ErrorCode.INVALID_BOARD_OWNER); - } - //상위 게시판이면 하위 게시판 목록을 조회 - // 1. 부모 + 자식 boardId 목록 만들기 - List targetBoardIds = Stream.concat( - Stream.of(boardId), // 자신 포함 - boardRepository.findAllByParentBoard_BoardId(boardId).stream() - .map(Board::getBoardId) - ).toList(); - - // 2. 각 boardId마다 postId/userId 조회해서 삭제 - targetBoardIds.stream() - .flatMap(id -> postRepository.findPostIdAndUserIdByBoardId(id).stream()) - .forEach(row -> deletePost(row.getPostId(), row.getUserId())); - targetBoardIds.forEach(boardRepository::deleteById); - return; - } - // 게시물 조회 (해당 게시판의 게시물) @Override @Transactional(readOnly = true) @@ -297,36 +270,6 @@ public PostResponse getPostDetail(UUID postId, UUID userId, int pageNumber, int .build(); } - // 게시판 생성 - @Override - @Transactional - public void createBoard(BoardRequest request, UUID userId) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - Board board; - // 하위 게시판인 경우 - if (request.getParentBoardId() != null) { - Board parentBoard = boardRepository.findById(request.getParentBoardId()) - .orElseThrow(() -> new CustomException(ErrorCode.BOARD_NOT_FOUND)); - - board = Board.builder() - .boardName(request.getBoardName()) - .createdBy(user) - .parentBoard(parentBoard) - .build(); - } else { - // 상위 게시판인 경우 - board = Board.builder() - .boardName(request.getBoardName()) - .createdBy(user) - .parentBoard(null) - .build(); - } - - boardRepository.save(board); - } - // 부모 게시판 조회 @Override @Transactional(readOnly = true) diff --git a/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java b/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java index 21885d93..4ba064be 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java +++ b/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java @@ -118,7 +118,7 @@ public enum ErrorCode { POST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 게시물을 찾을 수 없습니다."), - INVALID_BOARD_OWNER(HttpStatus.FORBIDDEN, "게시판 수정/삭제 권한이 없습니다."), + BOARD_ACCESS_DENIED(HttpStatus.FORBIDDEN, "게시판 수정/삭제 권한이 없습니다."), INVALID_POST_OWNER(HttpStatus.FORBIDDEN, "게시물 수정/삭제 권한이 없습니다."), diff --git a/backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java b/backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java index efd1f4fb..477cd2ef 100644 --- a/backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java +++ b/backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java @@ -17,11 +17,9 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.sejongisc.backend.board.dto.BoardRequest; import org.sejongisc.backend.board.dto.CommentResponse; import org.sejongisc.backend.board.dto.PostRequest; import org.sejongisc.backend.board.dto.PostResponse; @@ -37,9 +35,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.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; @@ -334,32 +332,4 @@ void getPostDetail_Success_WithReplies() { // N+1 쿼리 호출 검증 (부모 댓글 수만큼 findByParentComment 호출) verify(commentRepository, times(1)).findByParentComment(parentComment); } - - @Test - @DisplayName("게시판 생성 - 성공") - void createBoard_Success() { - // given - BoardRequest request = BoardRequest.builder() - .boardName("새 게시판") - .parentBoardId(mockParentBoard.getBoardId()) - .build(); - - // Mocking - when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); - when(boardRepository.findById(mockParentBoard.getBoardId())).thenReturn(Optional.of(mockParentBoard)); - - // ArgumentCaptor - ArgumentCaptor boardCaptor = ArgumentCaptor.forClass(Board.class); - - // when - postService.createBoard(request, userId); - - // then - verify(boardRepository).save(boardCaptor.capture()); - Board savedBoard = boardCaptor.getValue(); - - assertThat(savedBoard.getBoardName()).isEqualTo("새 게시판"); - assertThat(savedBoard.getCreatedBy()).isEqualTo(mockUser); - assertThat(savedBoard.getParentBoard().getBoardId()).isEqualTo(mockParentBoard.getBoardId()); - } } \ No newline at end of file