Skip to content

Commit aae0cff

Browse files
authored
20260221 #229 게시판 관리 기능 이관 (#238)
* [BE][FEAT] 게시판 생성 및 삭제 기능 이관 * [BE][FEAT] AdminBoardController 태그 추가 * [BE][FEAT] 게시판 존재 검증
1 parent 4546176 commit aae0cff

7 files changed

Lines changed: 152 additions & 129 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,62 @@
11
package org.sejongisc.backend.admin.controller;
22

3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.tags.Tag;
5+
import jakarta.validation.Valid;
6+
import java.util.UUID;
7+
import lombok.RequiredArgsConstructor;
8+
import org.sejongisc.backend.board.dto.BoardRequest;
9+
import org.sejongisc.backend.admin.service.AdminBoardService;
10+
import org.sejongisc.backend.common.auth.dto.CustomUserDetails;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
13+
import org.springframework.web.bind.annotation.DeleteMapping;
14+
import org.springframework.web.bind.annotation.PathVariable;
15+
import org.springframework.web.bind.annotation.PostMapping;
16+
import org.springframework.web.bind.annotation.RequestBody;
17+
import org.springframework.web.bind.annotation.RequestMapping;
18+
import org.springframework.web.bind.annotation.RestController;
19+
20+
@RestController
21+
@RequiredArgsConstructor
22+
@RequestMapping("/api/board/admin")
23+
@Tag(
24+
name = "게시판 관리 API",
25+
description = "게시판 생성 및 삭제 관련 API 제공"
26+
)
327
public class AdminBoardController {
28+
29+
private final AdminBoardService adminBoardService;
30+
31+
// 게시판 생성
32+
@Operation(
33+
summary = "게시판 생성",
34+
description = "게시판 이름과 상위 게시판 ID를 포함한 새로운 게시판을 생성합니다."
35+
+ "상위 게시판의 ID가 null 이면 최상위 게시판으로 생성됩니다."
36+
+ "회장만 생성할 수 있습니다."
37+
)
38+
@PostMapping
39+
public ResponseEntity<Void> createBoard(
40+
@RequestBody @Valid BoardRequest request,
41+
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
42+
UUID userId = customUserDetails.getUserId();
43+
adminBoardService.createBoard(request, userId);
44+
return ResponseEntity.ok().build();
45+
}
46+
47+
// 게시판 삭제
48+
@Operation(
49+
summary = "게시판 삭제",
50+
description = "게시판 ID를 통해 게시판을 삭제합니다."
51+
+ "회장만 삭제할 수 있습니다."
52+
+ "관련 첨부파일 및 댓글 등도 함께 삭제됩니다."
53+
)
54+
@DeleteMapping("/{boardId}")
55+
public ResponseEntity<?> deleteBoard(
56+
@PathVariable UUID boardId,
57+
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
58+
UUID userId = customUserDetails.getUserId();
59+
adminBoardService.deleteBoard(boardId, userId);
60+
return ResponseEntity.ok("게시판 삭제가 완료되었습니다.");
61+
}
462
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package org.sejongisc.backend.admin.service;
2+
3+
import java.util.List;
4+
import java.util.UUID;
5+
import java.util.stream.Stream;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.sejongisc.backend.board.dto.BoardRequest;
9+
import org.sejongisc.backend.board.entity.Board;
10+
import org.sejongisc.backend.board.repository.BoardRepository;
11+
import org.sejongisc.backend.board.repository.PostRepository;
12+
import org.sejongisc.backend.board.service.PostService;
13+
import org.sejongisc.backend.common.exception.CustomException;
14+
import org.sejongisc.backend.common.exception.ErrorCode;
15+
import org.sejongisc.backend.user.entity.Role;
16+
import org.sejongisc.backend.user.entity.User;
17+
import org.sejongisc.backend.user.repository.UserRepository;
18+
import org.springframework.stereotype.Service;
19+
import org.springframework.transaction.annotation.Transactional;
20+
21+
@Service
22+
@RequiredArgsConstructor
23+
@Slf4j
24+
public class AdminBoardService {
25+
26+
private final UserRepository userRepository;
27+
private final PostRepository postRepository;
28+
private final BoardRepository boardRepository;
29+
private final PostService postService;
30+
31+
// 게시판 생성
32+
@Transactional
33+
public void createBoard(BoardRequest request, UUID userId) {
34+
User user = userRepository.findById(userId)
35+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
36+
37+
// 회장만 게시판 생성 가능
38+
if (!user.getRole().equals(Role.PRESIDENT)) {
39+
throw new CustomException(ErrorCode.BOARD_ACCESS_DENIED);
40+
}
41+
42+
Board board;
43+
// 하위 게시판인 경우
44+
if (request.getParentBoardId() != null) {
45+
Board parentBoard = boardRepository.findById(request.getParentBoardId())
46+
.orElseThrow(() -> new CustomException(ErrorCode.BOARD_NOT_FOUND));
47+
48+
board = Board.builder()
49+
.boardName(request.getBoardName())
50+
.createdBy(user)
51+
.parentBoard(parentBoard)
52+
.build();
53+
} else {
54+
// 상위 게시판인 경우
55+
board = Board.builder()
56+
.boardName(request.getBoardName())
57+
.createdBy(user)
58+
.parentBoard(null)
59+
.build();
60+
}
61+
62+
boardRepository.save(board);
63+
}
64+
65+
// 게시판 삭제
66+
@Transactional
67+
public void deleteBoard(UUID boardId, UUID boardUserId) {
68+
User user = userRepository.findById(boardUserId).orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
69+
70+
// 회장만 게시판 삭제 가능
71+
if (!user.getRole().equals(Role.PRESIDENT)) {
72+
throw new CustomException(ErrorCode.BOARD_ACCESS_DENIED);
73+
}
74+
75+
boardRepository.findById(boardId)
76+
.orElseThrow(() -> new CustomException(ErrorCode.BOARD_NOT_FOUND));
77+
78+
// 상위 게시판이면 하위 게시판 목록을 조회
79+
List<UUID> targetBoardIds = Stream.concat(
80+
Stream.of(boardId), // 자신 포함
81+
boardRepository.findAllByParentBoard_BoardId(boardId).stream()
82+
.map(Board::getBoardId)
83+
).toList();
84+
85+
// 각 boardId마다 postId/userId 조회해서 삭제
86+
targetBoardIds.stream()
87+
.flatMap(id -> postRepository.findPostIdAndUserIdByBoardId(id).stream())
88+
.forEach(row -> postService.deletePost(row.getPostId(), row.getUserId()));
89+
targetBoardIds.forEach(boardRepository::deleteById);
90+
}
91+
}

backend/src/main/java/org/sejongisc/backend/board/controller/BoardController.java

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import java.util.List;
77
import java.util.UUID;
88
import lombok.RequiredArgsConstructor;
9-
import org.sejongisc.backend.board.dto.BoardRequest;
109
import org.sejongisc.backend.board.dto.BoardResponse;
1110
import org.sejongisc.backend.board.dto.CommentRequest;
1211
import org.sejongisc.backend.board.dto.PostRequest;
@@ -140,21 +139,6 @@ public ResponseEntity<PostResponse> getPostDetail(
140139
return ResponseEntity.ok(response);
141140
}
142141

143-
// 게시판 생성
144-
@Operation(
145-
summary = "게시판 생성",
146-
description = "게시판 이름과 상위 게시판 ID를 포함한 새로운 게시판을 생성합니다."
147-
+ "상위 게시판의 ID가 null 이면 최상위 게시판으로 생성됩니다."
148-
)
149-
@PostMapping
150-
public ResponseEntity<Void> createBoard(
151-
@RequestBody @Valid BoardRequest request,
152-
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
153-
UUID userId = customUserDetails.getUserId();
154-
postService.createBoard(request, userId);
155-
return ResponseEntity.ok().build();
156-
}
157-
158142
// 최상위 게시판 목록 조회
159143
@Operation(
160144
summary = "부모 게시판 목록 조회",
@@ -178,22 +162,6 @@ public ResponseEntity<List<BoardResponse>> getChildBoards(
178162
return ResponseEntity.ok(postService.getChildBoards());
179163
}
180164

181-
// 게시판 삭제
182-
@Operation(
183-
summary = "게시판 삭제",
184-
description = "게시판 ID를 통해 게시판을 삭제합니다."
185-
+ "회장만 삭제할 수 있습니다."
186-
+ "관련 첨부파일 및 댓글 등도 함께 삭제됩니다."
187-
)
188-
@DeleteMapping("/{boardId}")
189-
public ResponseEntity<?> deleteBoard(
190-
@PathVariable UUID boardId,
191-
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
192-
UUID userId = customUserDetails.getUserId();
193-
postService.deleteBoard(boardId, userId);
194-
return ResponseEntity.ok("게시판 삭제가 완료되었습니다.");
195-
}
196-
197165
// 좋아요 토글
198166
@Operation(
199167
summary = "좋아요 등록 및 취소",

backend/src/main/java/org/sejongisc/backend/board/service/PostService.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import java.util.List;
44
import java.util.UUID;
5-
import org.sejongisc.backend.board.dto.BoardRequest;
65
import org.sejongisc.backend.board.dto.BoardResponse;
76
import org.sejongisc.backend.board.dto.PostRequest;
87
import org.sejongisc.backend.board.dto.PostResponse;
@@ -28,15 +27,9 @@ public interface PostService {
2827
// 게시물 상세 조회
2928
PostResponse getPostDetail(UUID postId, UUID userId, int pageNumber, int pageSize);
3029

31-
// 게시판 생성
32-
void createBoard(BoardRequest request, UUID userId);
33-
3430
// 부모 게시판 목록 조회
3531
List<BoardResponse> getParentBoards();
3632

3733
// 하위 게시판 목록 조회
3834
List<BoardResponse> getChildBoards();
39-
40-
// 게시판 삭제
41-
void deleteBoard(UUID boardId, UUID boardUserId);
4235
}

backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java

Lines changed: 1 addition & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22

33
import java.util.List;
44
import java.util.UUID;
5-
import java.util.stream.Stream;
65
import lombok.RequiredArgsConstructor;
76
import lombok.extern.slf4j.Slf4j;
8-
import org.sejongisc.backend.board.dto.BoardRequest;
97
import org.sejongisc.backend.board.dto.BoardResponse;
108
import org.sejongisc.backend.board.dto.CommentResponse;
119
import org.sejongisc.backend.board.dto.PostAttachmentResponse;
@@ -23,10 +21,9 @@
2321
import org.sejongisc.backend.board.repository.PostRepository;
2422
import org.sejongisc.backend.common.exception.CustomException;
2523
import org.sejongisc.backend.common.exception.ErrorCode;
26-
import org.sejongisc.backend.user.repository.UserRepository;
2724
import org.sejongisc.backend.user.dto.UserInfoResponse;
28-
import org.sejongisc.backend.user.entity.Role;
2925
import org.sejongisc.backend.user.entity.User;
26+
import org.sejongisc.backend.user.repository.UserRepository;
3027
import org.springframework.data.domain.Page;
3128
import org.springframework.data.domain.PageRequest;
3229
import org.springframework.data.domain.Pageable;
@@ -171,30 +168,6 @@ public void deletePost(UUID postId, UUID userId) {
171168
postRepository.delete(post);
172169
}
173170

174-
// 게시판 삭제
175-
@Override
176-
@Transactional
177-
public void deleteBoard(UUID boardId, UUID boardUserId) {
178-
User user = userRepository.findById(boardUserId).orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
179-
if(!user.getRole().equals(Role.PRESIDENT)){
180-
throw new CustomException(ErrorCode.INVALID_BOARD_OWNER);
181-
}
182-
//상위 게시판이면 하위 게시판 목록을 조회
183-
// 1. 부모 + 자식 boardId 목록 만들기
184-
List<UUID> targetBoardIds = Stream.concat(
185-
Stream.of(boardId), // 자신 포함
186-
boardRepository.findAllByParentBoard_BoardId(boardId).stream()
187-
.map(Board::getBoardId)
188-
).toList();
189-
190-
// 2. 각 boardId마다 postId/userId 조회해서 삭제
191-
targetBoardIds.stream()
192-
.flatMap(id -> postRepository.findPostIdAndUserIdByBoardId(id).stream())
193-
.forEach(row -> deletePost(row.getPostId(), row.getUserId()));
194-
targetBoardIds.forEach(boardRepository::deleteById);
195-
return;
196-
}
197-
198171
// 게시물 조회 (해당 게시판의 게시물)
199172
@Override
200173
@Transactional(readOnly = true)
@@ -297,36 +270,6 @@ public PostResponse getPostDetail(UUID postId, UUID userId, int pageNumber, int
297270
.build();
298271
}
299272

300-
// 게시판 생성
301-
@Override
302-
@Transactional
303-
public void createBoard(BoardRequest request, UUID userId) {
304-
User user = userRepository.findById(userId)
305-
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
306-
307-
Board board;
308-
// 하위 게시판인 경우
309-
if (request.getParentBoardId() != null) {
310-
Board parentBoard = boardRepository.findById(request.getParentBoardId())
311-
.orElseThrow(() -> new CustomException(ErrorCode.BOARD_NOT_FOUND));
312-
313-
board = Board.builder()
314-
.boardName(request.getBoardName())
315-
.createdBy(user)
316-
.parentBoard(parentBoard)
317-
.build();
318-
} else {
319-
// 상위 게시판인 경우
320-
board = Board.builder()
321-
.boardName(request.getBoardName())
322-
.createdBy(user)
323-
.parentBoard(null)
324-
.build();
325-
}
326-
327-
boardRepository.save(board);
328-
}
329-
330273
// 부모 게시판 조회
331274
@Override
332275
@Transactional(readOnly = true)

backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public enum ErrorCode {
118118

119119
POST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 게시물을 찾을 수 없습니다."),
120120

121-
INVALID_BOARD_OWNER(HttpStatus.FORBIDDEN, "게시판 수정/삭제 권한이 없습니다."),
121+
BOARD_ACCESS_DENIED(HttpStatus.FORBIDDEN, "게시판 수정/삭제 권한이 없습니다."),
122122

123123
INVALID_POST_OWNER(HttpStatus.FORBIDDEN, "게시물 수정/삭제 권한이 없습니다."),
124124

backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,9 @@
1717
import org.junit.jupiter.api.DisplayName;
1818
import org.junit.jupiter.api.Test;
1919
import org.junit.jupiter.api.extension.ExtendWith;
20-
import org.mockito.ArgumentCaptor;
2120
import org.mockito.InjectMocks;
2221
import org.mockito.Mock;
2322
import org.mockito.junit.jupiter.MockitoExtension;
24-
import org.sejongisc.backend.board.dto.BoardRequest;
2523
import org.sejongisc.backend.board.dto.CommentResponse;
2624
import org.sejongisc.backend.board.dto.PostRequest;
2725
import org.sejongisc.backend.board.dto.PostResponse;
@@ -37,9 +35,9 @@
3735
import org.sejongisc.backend.board.repository.PostRepository;
3836
import org.sejongisc.backend.common.exception.CustomException;
3937
import org.sejongisc.backend.common.exception.ErrorCode;
40-
import org.sejongisc.backend.user.repository.UserRepository;
4138
import org.sejongisc.backend.user.entity.Role;
4239
import org.sejongisc.backend.user.entity.User;
40+
import org.sejongisc.backend.user.repository.UserRepository;
4341
import org.springframework.data.domain.Page;
4442
import org.springframework.data.domain.PageImpl;
4543
import org.springframework.data.domain.PageRequest;
@@ -334,32 +332,4 @@ void getPostDetail_Success_WithReplies() {
334332
// N+1 쿼리 호출 검증 (부모 댓글 수만큼 findByParentComment 호출)
335333
verify(commentRepository, times(1)).findByParentComment(parentComment);
336334
}
337-
338-
@Test
339-
@DisplayName("게시판 생성 - 성공")
340-
void createBoard_Success() {
341-
// given
342-
BoardRequest request = BoardRequest.builder()
343-
.boardName("새 게시판")
344-
.parentBoardId(mockParentBoard.getBoardId())
345-
.build();
346-
347-
// Mocking
348-
when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));
349-
when(boardRepository.findById(mockParentBoard.getBoardId())).thenReturn(Optional.of(mockParentBoard));
350-
351-
// ArgumentCaptor
352-
ArgumentCaptor<Board> boardCaptor = ArgumentCaptor.forClass(Board.class);
353-
354-
// when
355-
postService.createBoard(request, userId);
356-
357-
// then
358-
verify(boardRepository).save(boardCaptor.capture());
359-
Board savedBoard = boardCaptor.getValue();
360-
361-
assertThat(savedBoard.getBoardName()).isEqualTo("새 게시판");
362-
assertThat(savedBoard.getCreatedBy()).isEqualTo(mockUser);
363-
assertThat(savedBoard.getParentBoard().getBoardId()).isEqualTo(mockParentBoard.getBoardId());
364-
}
365335
}

0 commit comments

Comments
 (0)