Skip to content

✨ Feat: 어드민 기능 구현 - #173

Merged
limhb708 merged 17 commits into
DoDo-Project:developfrom
limhb708:feature/172
Jun 17, 2026
Merged

✨ Feat: 어드민 기능 구현#173
limhb708 merged 17 commits into
DoDo-Project:developfrom
limhb708:feature/172

Conversation

@limhb708

@limhb708 limhb708 commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

📄 작업 내용 (Description)

이번 PR에서 변경되거나 추가된 주요 작업을 간단히 설명해주세요.

어드민 기능의 api를 구현합니다


🔗 관련 이슈 (Related Issues)

작업한 이슈 번호를 아래 형식으로 PULL REQUEST BODY에 작성해주세요.
(PR 머지 시 해당 이슈가 자동으로 종료됩니다.)


✅ 체크리스트 (Checklist)

PR을 보내기 전 아래 항목들을 모두 확인해주세요.

  • PR 제목은 커밋 컨벤션을 따랐습니다.
  • 관련 이슈를 연결했습니다.
  • 스스로 코드를 검토하고 불필요한 코드를 제거했습니다.
  • 코드 스타일이 프로젝트 규칙과 일치합니다. (Style)
  • 새로운 기능에 대한 테스트 코드를 추가했거나, 기존 테스트가 모두 통과했습니다. (Test)

📸 스크린샷 (Screenshots)

작업 내용과 관련된 스크린샷이 있다면 첨부해주세요. (UI 변경이 있는 경우)

Before After

💬 기타 사항 (Etc)

리뷰어에게 전달하고 싶은 추가 정보가 있다면 자유롭게 작성해주세요.

limhb708 and others added 16 commits June 12, 2026 19:25
- 내가 쓴 게시글 목록 조회 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 응답 메시지 정리
@limhb708 limhb708 self-assigned this Jun 16, 2026
@limhb708 limhb708 added ✨ Feature 새 기능 혹은 요구 사항 🦥 임현빈 임현빈 파트 labels Jun 16, 2026
@limhb708
limhb708 requested a review from WhiteBin-bin June 16, 2026 11:24

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +139 to +154
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();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

현재 getReportList 메서드는 데이터베이스에서 모든 신고 내역을 조회한 후, 메모리 상에서 그룹화, 정렬, 페이징을 수행하고 있습니다. 데이터가 늘어남에 따라 심각한 메모리 부하와 성능 저하(Out Of Memory 등)를 유발할 수 있으므로, 데이터베이스 수준에서 GROUP BY와 집계 함수를 사용하고 Pageable을 통해 필요한 만큼만 조회하도록 쿼리를 개선해야 합니다.

Comment on lines +180 to +184
if (!boardRepository.existsById(boardId)) {
throw new AdminException(BOARD_NOT_FOUND);
}

boardMapper.deleteBoard(boardId, BoardStatus.DELETED.name());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

deleteAnnouncement에서는 JPA 엔티티의 updateBoardStatus를 사용하는 반면, deleteBoard에서는 boardMapper를 사용하여 직접 SQL 업데이트를 수행하고 있어 일관성이 깨집니다. 또한, MyBatis를 통한 직접 업데이트는 JPA 1차 캐시와 동기화되지 않으며 boardStatusUpdatedAt 필드가 누락될 위험이 있습니다. JPA를 사용하여 엔티티를 조회하고 상태를 변경하도록 개선하는 것을 권장합니다.

Suggested change
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);

Comment on lines +178 to +186
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

댓글 삭제(deleteComment) 시에는 관련 신고 내역을 모두 삭제(reportRepository.deleteAllByComment)하는 반면, 게시글 삭제(deleteBoard) 시에는 관련 신고 내역을 처리하는 로직이 누락되어 있습니다. 이로 인해 삭제된 게시글에 대한 신고 내역이 PENDING 상태로 계속 남아 관리자 페이지를 어지럽힐 수 있으므로, 게시글 삭제 시에도 관련 신고 내역을 삭제하거나 상태를 COMPLETED로 변경하는 로직을 추가하는 것이 좋습니다.

Comment on lines +284 to +286
List<AnnouncementItemResponse> items = page.getContent().stream()
.map(board -> AnnouncementItemResponse.toDto(board, firstImageUrl(board.getBoardId())))
.toList();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

page.getContent().stream().map(...) 내부에서 firstImageUrl(board.getBoardId())를 호출하고 있어, 조회된 공지사항 개수만큼 추가적인 이미지 조회 쿼리가 발생하는 N+1 문제가 존재합니다. 공지사항 ID 목록을 이용해 이미지를 bulk-fetch 하거나, 조인을 통해 한 번에 조회하도록 개선하는 것이 좋습니다.

Comment on lines +402 to +405
private String firstImageUrl(Long boardId) {
List<String> imageUrls = imageFileService.getBoardImageUrls(boardId);
return imageUrls.isEmpty() ? null : imageUrls.get(0);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

imageFileService.getBoardImageUrls(boardId)null을 반환할 가능성이 있는 경우, imageUrls.isEmpty() 호출 시 NullPointerException이 발생할 수 있습니다. 안전한 방어적 프로그래밍을 위해 null 체크를 추가하는 것이 좋습니다.

Suggested change
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Spring Data JPA의 기본 delete 메소드(deleteAllByComment)는 대상 엔티티들을 먼저 SELECT한 후 건별로 DELETE 쿼리를 실행하므로 성능 저하를 유발할 수 있습니다. @Modifying@Query를 사용하여 단일 벌크 삭제 쿼리로 실행되도록 개선하는 것이 효율적입니다. (참고: org.springframework.data.jpa.repository.ModifyingQuery 임포트 필요)

    @Modifying
    @Query("delete from Report r where r.comment = :comment")
    void deleteAllByComment(@Param("comment") Comment comment);

@limhb708
limhb708 merged commit 200fde0 into DoDo-Project:develop Jun 17, 2026
1 check passed
WhiteBin-bin pushed a commit that referenced this pull request Jun 22, 2026
WhiteBin-bin pushed a commit that referenced this pull request Jun 22, 2026
WhiteBin-bin pushed a commit that referenced this pull request Jun 22, 2026
WhiteBin-bin pushed a commit that referenced this pull request Jun 23, 2026
WhiteBin-bin pushed a commit that referenced this pull request Jun 23, 2026
WhiteBin-bin pushed a commit that referenced this pull request Jun 23, 2026
WhiteBin-bin pushed a commit that referenced this pull request Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새 기능 혹은 요구 사항 🦥 임현빈 임현빈 파트

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 어드민 기능 개발

2 participants