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
@@ -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<Void> 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("게시판 삭제가 완료되었습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -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<UUID> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -140,21 +139,6 @@ public ResponseEntity<PostResponse> getPostDetail(
return ResponseEntity.ok(response);
}

// 게시판 생성
@Operation(
summary = "게시판 생성",
description = "게시판 이름과 상위 게시판 ID를 포함한 새로운 게시판을 생성합니다."
+ "상위 게시판의 ID가 null 이면 최상위 게시판으로 생성됩니다."
)
@PostMapping
public ResponseEntity<Void> createBoard(
@RequestBody @Valid BoardRequest request,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
UUID userId = customUserDetails.getUserId();
postService.createBoard(request, userId);
return ResponseEntity.ok().build();
}

// 최상위 게시판 목록 조회
@Operation(
summary = "부모 게시판 목록 조회",
Expand All @@ -178,22 +162,6 @@ public ResponseEntity<List<BoardResponse>> 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 = "좋아요 등록 및 취소",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,15 +27,9 @@ public interface PostService {
// 게시물 상세 조회
PostResponse getPostDetail(UUID postId, UUID userId, int pageNumber, int pageSize);

// 게시판 생성
void createBoard(BoardRequest request, UUID userId);

// 부모 게시판 목록 조회
List<BoardResponse> getParentBoards();

// 하위 게시판 목록 조회
List<BoardResponse> getChildBoards();

// 게시판 삭제
void deleteBoard(UUID boardId, UUID boardUserId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<UUID> 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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, "게시물 수정/삭제 권한이 없습니다."),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<Board> 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());
}
}