20260221 #229 게시판 관리 기능 이관#238
Hidden character warning
Conversation
Walkthrough게시판 생성 및 삭제 기능을 일반 Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java (1)
40-40:AdminBoardService테스트 커버리지 누락
createBoard_Success테스트가PostServiceImplTest에서 제거되었지만, 새로 추가된AdminBoardService에 대한 테스트 클래스(AdminBoardServiceTest)가 PR에 포함되어 있지 않습니다.createBoard및deleteBoard로직(PRESIDENT 역할 검증, 하위 게시판 계단식 삭제 등)에 대한 단위 테스트를 별도로 추가해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java` at line 40, PostServiceImplTest에서 빠진 createBoard 커버리지를 보완하려면 AdminBoardService에 대한 단위 테스트 클래스(AdminBoardServiceTest)를 추가하고 createBoard 및 deleteBoard 경로를 모두 검증하세요: AdminBoardService의 createBoard가 PRESIDENT 역할을 가진 사용자만 성공하도록 UserRepository(또는 UserService)를 모킹해서 성공 케이스와 권한 부족(권한 예외) 케이스를 각각 작성하고, deleteBoard는 하위 게시판을 계단식으로 삭제하는 로직을 BoardRepository(또는 BoardService)를 모킹해 하위 엔티티들이 모두 삭제되는 성공 케이스와 존재하지 않는 게시판/권한 실패 케이스를 추가해 검증하세요.backend/src/main/java/org/sejongisc/backend/board/service/AdminBoardService.java (1)
82-85:deletePost에 게시물 작성자 ID를 전달하는 우회 패턴
postService.deletePost(row.getPostId(), row.getUserId())는deletePost의 작성자 소유권 검사를 통과시키기 위해 포스트 작성자의userId를 그대로 전달합니다. 현재는 동작하지만, 향후deletePost에 감사(audit) 로그나 알림 로직이 추가될 경우 실제 수행자(회장)가 아닌 게시물 작성자 ID가 기록되는 문제가 생깁니다.관리자용 삭제 경로를 소유권 검사에 의존하지 않도록 분리하는 것이 좋습니다. 예를 들어
PostService에adminDeletePost(UUID postId)메서드를 추가하거나, 레포지토리를 직접 호출하는 전용 헬퍼를AdminBoardService내부에 두는 방식을 고려해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/main/java/org/sejongisc/backend/board/service/AdminBoardService.java` around lines 82 - 85, The current loop in AdminBoardService calls postService.deletePost(row.getPostId(), row.getUserId()) which incorrectly records the post author as the actor; instead add an admin-specific deletion path and call it here (e.g., implement PostService.adminDeletePost(UUID postId) and replace the deletePost call with postService.adminDeletePost(row.getPostId())), or perform repository-level deletion from AdminBoardService using postRepository.deleteById(row.getPostId()) to avoid owner-based checks and ensure audit/notification logic uses the admin actor; update usages of postRepository.findPostIdAndUserIdByBoardId and boardRepository.deleteById accordingly.backend/src/main/java/org/sejongisc/backend/board/controller/AdminBoardController.java (1)
54-61:ResponseEntity<?>대신 구체적인 타입 사용 권장
deleteBoard의 반환 타입이ResponseEntity<?>로 선언되어 있어 타입 안전성이 낮습니다. 응답 본문이String이므로ResponseEntity<String>으로 명시하거나,createBoard와 일관성을 맞춰ResponseEntity<Void>에 빈 바디로 통일하는 방향을 고려해 주세요.♻️ 수정 제안 (구체적 타입으로 변경)
- public ResponseEntity<?> deleteBoard( + public ResponseEntity<String> deleteBoard(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/main/java/org/sejongisc/backend/board/controller/AdminBoardController.java` around lines 54 - 61, Change the method signature in AdminBoardController.deleteBoard from ResponseEntity<?> to a concrete type (e.g., ResponseEntity<String>) and update the return to match; specifically, replace ResponseEntity<?> deleteBoard(...) with ResponseEntity<String> deleteBoard(...) and keep returning ResponseEntity.ok("게시판 삭제가 완료되었습니다."); (or alternatively switch to ResponseEntity<Void> and return ResponseEntity.noContent().build() if you prefer empty-body consistency with createBoard). Ensure imports and method signature references reflect the new concrete ResponseEntity type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@backend/src/main/java/org/sejongisc/backend/board/controller/AdminBoardController.java`:
- Around line 32-37: In AdminBoardController update the `@Operation` description
for the createBoard endpoint: replace the incorrect phrase "회장만 삭제할 수 있습니다."
with "회장만 생성할 수 있습니다." so the summary/description for the createBoard method
accurately states that only the president can create boards; locate the
annotation on the createBoard method and change that single word.
- Around line 20-27: Remove the unused empty AdminBoardController class in the
admin.controller package (the file
backend/src/main/java/org/sejongisc/backend/admin/controller/AdminBoardController.java)
to avoid the duplicate with
org.sejongisc.backend.board.controller.AdminBoardController; delete the
class/file and any unused imports or bean references pointing to that
admin.controller.AdminBoardController, ensuring the remaining
board.controller.AdminBoardController (annotated with `@RestController` and
`@RequestMapping`("/api/board/admin")) is the sole controller for admin board
endpoints.
In
`@backend/src/main/java/org/sejongisc/backend/board/service/AdminBoardService.java`:
- Around line 66-86: After role validation in AdminBoardService.deleteBoard,
explicitly verify the target board exists by calling
boardRepository.findById(boardId).orElseThrow(() -> new
CustomException(ErrorCode.BOARD_NOT_FOUND)) before using boardId to fetch child
boards or delete; this ensures a meaningful BOARD_NOT_FOUND error is thrown and
prevents downstream EmptyResultDataAccessException when calling
boardRepository.findAllByParentBoard_BoardId(boardId) or deleteById.
- Around line 43-51: When creating a board with request.getParentBoardId(),
verify the fetched parentBoard (from boardRepository.findById(...)) is a
top-level board by checking parentBoard.getParentBoard() == null; if it isn’t,
throw a CustomException (use an appropriate ErrorCode such as a new or existing
one for invalid parent/depth) before building the new Board so you cannot create
a 3rd-level board that would block posting. Ensure this check is placed
immediately after obtaining parentBoard and before using Board.builder().
---
Nitpick comments:
In
`@backend/src/main/java/org/sejongisc/backend/board/controller/AdminBoardController.java`:
- Around line 54-61: Change the method signature in
AdminBoardController.deleteBoard from ResponseEntity<?> to a concrete type
(e.g., ResponseEntity<String>) and update the return to match; specifically,
replace ResponseEntity<?> deleteBoard(...) with ResponseEntity<String>
deleteBoard(...) and keep returning ResponseEntity.ok("게시판 삭제가 완료되었습니다."); (or
alternatively switch to ResponseEntity<Void> and return
ResponseEntity.noContent().build() if you prefer empty-body consistency with
createBoard). Ensure imports and method signature references reflect the new
concrete ResponseEntity type.
In
`@backend/src/main/java/org/sejongisc/backend/board/service/AdminBoardService.java`:
- Around line 82-85: The current loop in AdminBoardService calls
postService.deletePost(row.getPostId(), row.getUserId()) which incorrectly
records the post author as the actor; instead add an admin-specific deletion
path and call it here (e.g., implement PostService.adminDeletePost(UUID postId)
and replace the deletePost call with
postService.adminDeletePost(row.getPostId())), or perform repository-level
deletion from AdminBoardService using postRepository.deleteById(row.getPostId())
to avoid owner-based checks and ensure audit/notification logic uses the admin
actor; update usages of postRepository.findPostIdAndUserIdByBoardId and
boardRepository.deleteById accordingly.
In
`@backend/src/test/java/org/sejongisc/backend/board/service/PostServiceImplTest.java`:
- Line 40: PostServiceImplTest에서 빠진 createBoard 커버리지를 보완하려면 AdminBoardService에
대한 단위 테스트 클래스(AdminBoardServiceTest)를 추가하고 createBoard 및 deleteBoard 경로를 모두
검증하세요: AdminBoardService의 createBoard가 PRESIDENT 역할을 가진 사용자만 성공하도록
UserRepository(또는 UserService)를 모킹해서 성공 케이스와 권한 부족(권한 예외) 케이스를 각각 작성하고,
deleteBoard는 하위 게시판을 계단식으로 삭제하는 로직을 BoardRepository(또는 BoardService)를 모킹해 하위
엔티티들이 모두 삭제되는 성공 케이스와 존재하지 않는 게시판/권한 실패 케이스를 추가해 검증하세요.
backend/src/main/java/org/sejongisc/backend/board/controller/AdminBoardController.java
Outdated
Show resolved
Hide resolved
backend/src/main/java/org/sejongisc/backend/board/controller/AdminBoardController.java
Outdated
Show resolved
Hide resolved
backend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.java
Show resolved
Hide resolved
backend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
backend/src/main/java/org/sejongisc/backend/admin/controller/AdminBoardController.java (2)
38-45: 생성 응답에201 Created사용 권장RESTful 관례상 리소스 생성 시
200 OK대신201 Created를 반환하는 것이 더 적합합니다.♻️ 수정 제안
- return ResponseEntity.ok().build(); + return ResponseEntity.status(HttpStatus.CREATED).build();
HttpStatusimport 추가 필요:import org.springframework.http.HttpStatus;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/main/java/org/sejongisc/backend/admin/controller/AdminBoardController.java` around lines 38 - 45, The createBoard method in AdminBoardController currently returns ResponseEntity.ok() (200); change it to return a 201 Created response when a board is created by using ResponseEntity.status(HttpStatus.CREATED).build() (or equivalent) in the createBoard method that calls adminBoardService.createBoard(request, userId); also add the import for org.springframework.http.HttpStatus to the file so the HttpStatus.CREATED symbol is available.
54-61:deleteBoard반환 타입 불일치 및 응답 코드
createBoard는ResponseEntity<Void>를 반환하지만,deleteBoard는ResponseEntity<?>를 반환합니다. 반환 타입의 일관성을 위해 구체적인 타입을 명시하는 것이 좋습니다. 또한 삭제 시에는204 No Content가 RESTful 관례에 부합합니다.♻️ 수정 제안
- public ResponseEntity<?> deleteBoard( + public ResponseEntity<Void> deleteBoard( `@PathVariable` UUID boardId, `@AuthenticationPrincipal` CustomUserDetails customUserDetails) { UUID userId = customUserDetails.getUserId(); adminBoardService.deleteBoard(boardId, userId); - return ResponseEntity.ok("게시판 삭제가 완료되었습니다."); + return ResponseEntity.noContent().build(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/main/java/org/sejongisc/backend/admin/controller/AdminBoardController.java` around lines 54 - 61, Change deleteBoard's signature to return ResponseEntity<Void> and update its response to use 204 No Content; keep calling adminBoardService.deleteBoard(boardId, userId) but replace the current ResponseEntity.ok(...) return with ResponseEntity.noContent().build() so it matches createBoard's explicit Void type and RESTful conventions.backend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.java (3)
26-29:PostRepository직접 의존 — 서비스 간 책임 경계
AdminBoardService는PostRepository를 직접 주입받아 게시물 조회에 사용하고 있습니다. 게시물 관련 조회/삭제는PostService를 통해 처리하는 것이 계층 간 책임 분리에 더 부합합니다. 벌크 삭제 메서드를PostService에 추가하면PostRepository직접 의존도 제거할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.java` around lines 26 - 29, AdminBoardService가 직접 PostRepository에 의존하고 있으니 PostRepository 주입을 제거하고 게시물 관련 작업은 PostService로 위임하세요: PostService에 게시물 일괄삭제/조회용 메서드(예: deletePostsByBoardIds(Collection<Long> boardIds) 또는 deleteByBoardId(Long boardId) 등)를 추가하고 기존 AdminBoardService에서 postRepository를 사용하던 모든 위치를 해당 PostService 메서드 호출로 대체한 뒤 생성자에서 PostRepository 필드를 제거하세요.
44-60:createBoard의 if/else 분기에서 빌더 코드 중복
parentBoard설정 여부만 다르고 나머지 빌더 코드가 동일합니다. 하나의 빌더 체인으로 통합하면 중복을 줄일 수 있습니다.♻️ 리팩터링 제안
- 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(); - } + Board parentBoard = null; + if (request.getParentBoardId() != null) { + parentBoard = boardRepository.findById(request.getParentBoardId()) + .orElseThrow(() -> new CustomException(ErrorCode.BOARD_NOT_FOUND)); + } + + Board board = Board.builder() + .boardName(request.getBoardName()) + .createdBy(user) + .parentBoard(parentBoard) + .build();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.java` around lines 44 - 60, The createBoard method duplicates the Board.builder() chain in both branches; instead, compute a single parentBoard reference (resolve via boardRepository.findById(request.getParentBoardId()).orElseThrow(...) when request.getParentBoardId() != null, otherwise set parentBoard = null) and then build the entity once with Board.builder().boardName(request.getBoardName()).createdBy(user).parentBoard(parentBoard).build(), keeping the existing error handling that throws CustomException(ErrorCode.BOARD_NOT_FOUND) when lookup fails.
85-89: 게시물 삭제 — N+1 성능 이슈
postService.deletePost()를 게시물마다 개별 호출하면, 게시판에 게시물이 많을수록 불필요한 DB 쿼리가 증가합니다.findPostIdAndUserIdByBoardId로 데이터를 이미 조회한 후,deletePost내부에서 다시 게시물을 조회하는 N+1 패턴이 발생합니다.관리자 전용 벌크 삭제 메서드(
deletePostsByBoardId등)를PostService에 추가하여, 게시판 삭제 시 게시물을 한 번에 삭제하도록 개선하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.java` around lines 85 - 89, The current loop uses postService.deletePost(...) per post causing N+1 queries: replace this with a bulk delete call on PostService (e.g., add and call PostService.deletePostsByBoardId or deletePostsByBoardIds) that accepts a boardId or list of boardIds and deletes posts in one repository operation; update the code that currently uses targetBoardIds.stream().flatMap(id -> postRepository.findPostIdAndUserIdByBoardId(id)...) and postService.deletePost(...) to instead call the new bulk method (and keep the subsequent boardRepository::deleteById after posts are removed).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@backend/src/main/java/org/sejongisc/backend/admin/controller/AdminBoardController.java`:
- Around line 20-27: The AdminBoardController endpoints under
"/api/board/admin/**" are not covered by the SecurityFilterChain and only rely
on a service-layer Role.PRESIDENT check; either add the path to the centralized
SecurityConstants.ADMIN_ONLY_URLS pattern or apply method-level security on
AdminBoardController (e.g., annotate controller methods or the class with
`@PreAuthorize`("hasAnyRole('PRESIDENT','SYSTEM_ADMIN')") to match
AdminUserController) so requests are rejected at the filter layer and
unnecessary service calls are avoided.
In
`@backend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.java`:
- Around line 78-83: The current collection of targetBoardIds in
AdminBoardService only includes the given boardId and its immediate children via
findAllByParentBoard_BoardId(boardId); replace this with a defensive recursive
or iterative descendant traversal to collect all descendant Board IDs (e.g.,
implement a helper method getAllDescendantBoardIds(UUID rootId) that uses
findAllByParentBoard_BoardId to fetch children, then recurses or BFS/DFS to
accumulate grandchildren etc.), then use that result (plus the rootId) where
targetBoardIds is used; alternatively, enforce depth validation in createBoard
to prevent >2 levels, but the recommended fix is to add the recursive/iterative
collector and reference it from the deletion logic in AdminBoardService.
---
Duplicate comments:
In
`@backend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.java`:
- Around line 44-52: AdminBoardService의 게시판 생성 로직(요청 블록에서
request.getParentBoardId() 처리)에서 parentBoard가 이미 부모를 가지고 있는지를 검증하지 않아 3단계 계층이
만들어질 수 있으므로, boardRepository.findById(request.getParentBoardId())로 조회한
parentBoard에 대해 parentBoard.getParentBoard() != null 인지 확인하고, 해당 경우에는 새 Board 생성
대신 CustomException을 던지도록 변경하세요(예: throw new
CustomException(ErrorCode.BOARD_DEPTH_EXCEEDED) 또는 적절한 ErrorCode 사용). 이 검증은
Board.builder(...)를 호출하기 전에 적용되어야 하며, 참조 대상은 AdminBoardService, Board,
request.getParentBoardId(), boardRepository.findById(...)입니다.
---
Nitpick comments:
In
`@backend/src/main/java/org/sejongisc/backend/admin/controller/AdminBoardController.java`:
- Around line 38-45: The createBoard method in AdminBoardController currently
returns ResponseEntity.ok() (200); change it to return a 201 Created response
when a board is created by using
ResponseEntity.status(HttpStatus.CREATED).build() (or equivalent) in the
createBoard method that calls adminBoardService.createBoard(request, userId);
also add the import for org.springframework.http.HttpStatus to the file so the
HttpStatus.CREATED symbol is available.
- Around line 54-61: Change deleteBoard's signature to return
ResponseEntity<Void> and update its response to use 204 No Content; keep calling
adminBoardService.deleteBoard(boardId, userId) but replace the current
ResponseEntity.ok(...) return with ResponseEntity.noContent().build() so it
matches createBoard's explicit Void type and RESTful conventions.
In
`@backend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.java`:
- Around line 26-29: AdminBoardService가 직접 PostRepository에 의존하고 있으니
PostRepository 주입을 제거하고 게시물 관련 작업은 PostService로 위임하세요: PostService에 게시물 일괄삭제/조회용
메서드(예: deletePostsByBoardIds(Collection<Long> boardIds) 또는 deleteByBoardId(Long
boardId) 등)를 추가하고 기존 AdminBoardService에서 postRepository를 사용하던 모든 위치를 해당
PostService 메서드 호출로 대체한 뒤 생성자에서 PostRepository 필드를 제거하세요.
- Around line 44-60: The createBoard method duplicates the Board.builder() chain
in both branches; instead, compute a single parentBoard reference (resolve via
boardRepository.findById(request.getParentBoardId()).orElseThrow(...) when
request.getParentBoardId() != null, otherwise set parentBoard = null) and then
build the entity once with
Board.builder().boardName(request.getBoardName()).createdBy(user).parentBoard(parentBoard).build(),
keeping the existing error handling that throws
CustomException(ErrorCode.BOARD_NOT_FOUND) when lookup fails.
- Around line 85-89: The current loop uses postService.deletePost(...) per post
causing N+1 queries: replace this with a bulk delete call on PostService (e.g.,
add and call PostService.deletePostsByBoardId or deletePostsByBoardIds) that
accepts a boardId or list of boardIds and deletes posts in one repository
operation; update the code that currently uses
targetBoardIds.stream().flatMap(id ->
postRepository.findPostIdAndUserIdByBoardId(id)...) and
postService.deletePost(...) to instead call the new bulk method (and keep the
subsequent boardRepository::deleteById after posts are removed).
Summary by CodeRabbit
Release Notes
Refactor
Bug Fixes
Tests