✨ Feat: 어드민 기능 구현 - #173
Conversation
- 내가 쓴 게시글 목록 조회 API 추가 - 내가 쓴 댓글 목록 조회 API 추가 - Board/Comment Mapper 조회 쿼리 및 페이징 처리 구현 - 댓글 응답 DTO 및 MyBatis 매퍼 XML 추가 - Service/Controller 테스트 코드 작성
- BoardMapper.xml 내 중복 findBoardList 쿼리 제거 - 중복 countPublishedBoards 쿼리 제거 - 게시글 목록 조회 시 FREE 타입 조건 유지 - MyBatis 매퍼 중복 등록으로 인한 CI 실패 수정 Closes DoDo-Project#168
- develop 병합 후 재발생한 BoardMapper.xml 중복 쿼리 제거
- CommentServiceImpl Transactional 어노테이션 순서 정리 - CommentResponse Swagger 어노테이션 제거 및 Javadoc 정리 - comment 도메인 미사용 DTO 및 메서드 참조 확인 - BoardMapper.xml 중복 findBoardList/countPublishedBoards 쿼리 제거
# Conflicts: # src/main/java/com/dodo/backend/board/service/BoardServiceImpl.java
- ReportRequest DTO 추가 및 신고 사유 정의
- 게시글/댓글 신고 로직 및 대상 존재 검증 구현
- 중복 신고 방지 로직 적용
- POST /reports/boards/{boardId}, /reports/comments/{commentId} API 구현
- 신고 데이터 저장 처리
- Service/Controller 테스트 코드 작성
- 신고 중복 방지를 위한 유니크 제약 추가 - 자기 자신 신고 방지 검증 추가 - 중복 신고 에러 메시지 및 Swagger 설명 수정 - 댓글 신고 API 경로 단수형으로 변경 - 최신 원본 코드 병합 및 MainService 테스트 보정 - Service/Controller 테스트 코드 수정
- 관리자 신고 상세/목록 조회 API 구현 - 유저/게시글/댓글 관리 상태 변경 및 삭제 기능 추가 - 공지 작성/수정/삭제/목록/상세 조회 API 구현 - 관리자 권한 설정 및 예외 처리 추가 - 신고/공지 Repository 조회 메서드 추가 - Service/Controller 테스트 코드 작성 - 한글 인코딩 및 API 응답 메시지 정리
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive administration system, including the AdminController, AdminService, and associated DTOs, exceptions, and repositories to manage reports, user statuses, and announcements. Security configurations were also updated to restrict admin endpoints to authorized users. The review feedback highlights critical performance and consistency concerns in AdminServiceImpl, such as in-memory grouping and pagination of reports that could cause memory issues, an N+1 query problem when retrieving announcement images, and potential null pointer exceptions. Additionally, the reviewer recommended aligning board deletion with JPA to maintain cache consistency, ensuring associated reports are cleaned up when a board is deleted, and optimizing database deletion queries.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| public ReportListResponse getReportList(AdminReportType reportType, ReportStatus reportStatus, int page, int size, String sort) { | ||
| validatePageRequest(reportType, page, size); | ||
|
|
||
| List<Report> reports = findReports(reportType, reportStatus); | ||
| List<ReportListItemResponse> groupedItems = groupReports(reportType, reports); | ||
| groupedItems.sort(buildReportListComparator(sort)); | ||
|
|
||
| int fromIndex = Math.min(page * size, groupedItems.size()); | ||
| int toIndex = Math.min(fromIndex + size, groupedItems.size()); | ||
| List<ReportListItemResponse> pageItems = groupedItems.subList(fromIndex, toIndex); | ||
|
|
||
| return ReportListResponse.builder() | ||
| .pageInfo(PageInfoResponse.toDto(page, size, groupedItems.size())) | ||
| .data(pageItems) | ||
| .build(); | ||
| } |
| if (!boardRepository.existsById(boardId)) { | ||
| throw new AdminException(BOARD_NOT_FOUND); | ||
| } | ||
|
|
||
| boardMapper.deleteBoard(boardId, BoardStatus.DELETED.name()); |
There was a problem hiding this comment.
deleteAnnouncement에서는 JPA 엔티티의 updateBoardStatus를 사용하는 반면, deleteBoard에서는 boardMapper를 사용하여 직접 SQL 업데이트를 수행하고 있어 일관성이 깨집니다. 또한, MyBatis를 통한 직접 업데이트는 JPA 1차 캐시와 동기화되지 않으며 boardStatusUpdatedAt 필드가 누락될 위험이 있습니다. JPA를 사용하여 엔티티를 조회하고 상태를 변경하도록 개선하는 것을 권장합니다.
| if (!boardRepository.existsById(boardId)) { | |
| throw new AdminException(BOARD_NOT_FOUND); | |
| } | |
| boardMapper.deleteBoard(boardId, BoardStatus.DELETED.name()); | |
| Board board = boardRepository.findById(boardId) | |
| .orElseThrow(() -> new AdminException(BOARD_NOT_FOUND)); | |
| board.updateBoardStatus(BoardStatus.DELETED); |
| public void deleteBoard(Long boardId) { | ||
| validatePositiveId(boardId); | ||
| if (!boardRepository.existsById(boardId)) { | ||
| throw new AdminException(BOARD_NOT_FOUND); | ||
| } | ||
|
|
||
| boardMapper.deleteBoard(boardId, BoardStatus.DELETED.name()); | ||
| imageFileService.deleteBoardImages(boardId); | ||
| } |
There was a problem hiding this comment.
| List<AnnouncementItemResponse> items = page.getContent().stream() | ||
| .map(board -> AnnouncementItemResponse.toDto(board, firstImageUrl(board.getBoardId()))) | ||
| .toList(); |
| private String firstImageUrl(Long boardId) { | ||
| List<String> imageUrls = imageFileService.getBoardImageUrls(boardId); | ||
| return imageUrls.isEmpty() ? null : imageUrls.get(0); | ||
| } |
There was a problem hiding this comment.
imageFileService.getBoardImageUrls(boardId)가 null을 반환할 가능성이 있는 경우, imageUrls.isEmpty() 호출 시 NullPointerException이 발생할 수 있습니다. 안전한 방어적 프로그래밍을 위해 null 체크를 추가하는 것이 좋습니다.
| private String firstImageUrl(Long boardId) { | |
| List<String> imageUrls = imageFileService.getBoardImageUrls(boardId); | |
| return imageUrls.isEmpty() ? null : imageUrls.get(0); | |
| } | |
| private String firstImageUrl(Long boardId) { | |
| List<String> imageUrls = imageFileService.getBoardImageUrls(boardId); | |
| return imageUrls == null || imageUrls.isEmpty() ? null : imageUrls.get(0); | |
| } |
| * | ||
| * @param comment 삭제 대상 댓글 | ||
| */ | ||
| void deleteAllByComment(Comment comment); |
There was a problem hiding this comment.
Spring Data JPA의 기본 delete 메소드(deleteAllByComment)는 대상 엔티티들을 먼저 SELECT한 후 건별로 DELETE 쿼리를 실행하므로 성능 저하를 유발할 수 있습니다. @Modifying과 @Query를 사용하여 단일 벌크 삭제 쿼리로 실행되도록 개선하는 것이 효율적입니다. (참고: org.springframework.data.jpa.repository.Modifying 및 Query 임포트 필요)
@Modifying
@Query("delete from Report r where r.comment = :comment")
void deleteAllByComment(@Param("comment") Comment comment);
📄 작업 내용 (Description)
어드민 기능의 api를 구현합니다
🔗 관련 이슈 (Related Issues)
✅ 체크리스트 (Checklist)
Style)Test)📸 스크린샷 (Screenshots)
💬 기타 사항 (Etc)