20260226 #236 관리자 페이지 통계 대시보드 실시간 활동 로그 추가#266
Hidden character warning
Conversation
회원가입, 로그인, 베팅 참여, 게시물 작성, 출석 체크인에 이벤트 추가 완료 백테스팅, 댓글, 좋아요 이벤트 추가 고려 QrStreamService에 SseService 코드 사용 고려
백테스팅, 퀀트봇에 이벤트 발생 고려 QrStreamService에 SseService 코드 사용 고려 yml 수정에 따른 securityConfig 수정
oauth2를 더이상 쓰지 않으므로 변경
targetId String -> UUID로 변경 회원가입 시 출석 type 사용 오류 수정
모든 메시지 형식에 행위 대상 제외
이관된 메서드 비밀번호 초기화 및 변경 uri 변경에 따른 인증 화이트리스트 변경
redis 관련 코드 리팩토링
# Conflicts: # docker-compose.yml
주간 출석률 제외하고 구현 SecurityConfig에 @PreAuthorize 관련 코드 추가
|
Caution Review failedThe pull request is closed. ℹ️ Recent review infoConfiguration used: Organization UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (3)
둘러보기관리자 대시보드 통계·트렌드 엔드포인트와 실시간 SSE 활동 스트리밍을 추가하고, 보드 삭제 로직을 사용자 권한 검증 없이 하위 보드·게시물 연쇄 삭제로 변경하며, SSE 채널을 상수화하고 메서드 수준 보안을 활성화했습니다. 변경 사항
시퀀스 다이어그램sequenceDiagram
participant Client
participant AdminDashboardController
participant AdminDashboardService
participant ActivityLogRepository
participant SseService
participant ActivityEventListener
rect rgba(100, 150, 200, 0.5)
Note over Client,AdminDashboardService: 주간 보드 요약 조회
Client->>AdminDashboardController: GET /api/admin/dashboard/stats/boards/summary
AdminDashboardController->>AdminDashboardService: getWeeklyBoardSummary()
AdminDashboardService->>ActivityLogRepository: 기간별 집계 쿼리 호출
ActivityLogRepository-->>AdminDashboardService: 집계 결과
AdminDashboardService-->>AdminDashboardController: SummaryResponse
AdminDashboardController-->>Client: 200 OK
end
rect rgba(150, 100, 200, 0.5)
Note over Client,SseService: 실시간 활동 스트리밍 구독
Client->>AdminDashboardController: GET /api/admin/dashboard/activities/stream
AdminDashboardController->>AdminDashboardService: subscribeActivityStream()
AdminDashboardService->>SseService: subscribe(ADMIN_CHANNEL)
SseService-->>AdminDashboardService: SseEmitter
AdminDashboardService->>SseService: send CONNECT 이벤트
AdminDashboardService-->>AdminDashboardController: SseEmitter
AdminDashboardController-->>Client: SseEmitter 응답
Client->>ActivityEventListener: (외부) 활동 발생 이벤트
ActivityEventListener->>SseService: send(ADMIN_CHANNEL, payload)
SseService-->>Client: 실시간 푸시
end
rect rgba(200, 100, 150, 0.5)
Note over Client,ActivityLogRepository: 최근 활동 페이징 조회
Client->>AdminDashboardController: GET /api/admin/dashboard/activities?page=0&size=20
AdminDashboardController->>AdminDashboardService: getRecentActivities(pageable)
AdminDashboardService->>ActivityLogRepository: findAllByOrderByCreatedAtDesc(Pageable)
ActivityLogRepository-->>AdminDashboardService: Slice<ActivityLog>
AdminDashboardService-->>AdminDashboardController: Slice<ActivityLog>
AdminDashboardController-->>Client: 200 OK
end
예상 코드 리뷰 노력🎯 3 (보통) | ⏱️ ~25분 관련 가능성 있는 PR
시
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.java (1)
72-76:⚠️ Potential issue | 🔴 Critical부모 보드를 먼저 삭제하면 무결성 제약으로 실패할 수 있습니다.
Line 82에서 현재 순서대로 삭제하면 부모가 자식보다 먼저 삭제됩니다. 또한 Line 72-76은 직계 하위만 수집하므로 손자 보드가 있으면 누락됩니다. 전체 하위 트리를 수집하고
leaf -> root순으로 삭제해야 안전합니다.🛠 제안 수정안
- List<UUID> targetBoardIds = Stream.concat( - Stream.of(boardId), // 자신 포함 - boardRepository.findAllByParentBoard_BoardId(boardId).stream() - .map(Board::getBoardId) - ).toList(); + List<UUID> targetBoardIds = collectDescendantBoardIds(boardId); // 자신 + 모든 하위 보드 @@ - targetBoardIds.forEach(boardRepository::deleteById); + for (int i = targetBoardIds.size() - 1; i >= 0; i--) { + boardRepository.deleteById(targetBoardIds.get(i)); // leaf -> root + }private List<UUID> collectDescendantBoardIds(UUID rootId) { List<UUID> all = new java.util.ArrayList<>(); java.util.ArrayDeque<UUID> queue = new java.util.ArrayDeque<>(); queue.add(rootId); while (!queue.isEmpty()) { UUID current = queue.removeFirst(); all.add(current); boardRepository.findAllByParentBoard_BoardId(current).stream() .map(Board::getBoardId) .forEach(queue::addLast); } return all; }Also applies to: 82-82
🤖 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 72 - 76, The current deletion collects only direct children and deletes in parent-first order, causing FK violations; implement a traversal to collect the full descendant tree (use boardRepository.findAllByParentBoard_BoardId and Board::getBoardId) such as a BFS/DFS method (e.g., collectDescendantBoardIds(UUID rootId)) that returns all descendant IDs, then delete in reverse order (leaf -> root) so children are removed before their parents.
🧹 Nitpick comments (4)
backend/src/main/java/org/sejongisc/backend/admin/service/AdminDashboardService.java (2)
104-109: SSE 초기 전송 실패 시 로깅/완료 처리를 추가하는 것이 좋습니다.현재 예외가 조용히 흡수되어 원인 추적이 어렵습니다. 실패 원인 로그와
completeWithError를 같이 처리하면 운영 가시성이 좋아집니다.🛠 제안 수정안
try { // 연결 시 503 에러 방지를 위한 더미 이벤트 발송 emitter.send(SseEmitter.event().name("CONNECT").data("Connected to Admin SSE Stream")); } catch (Exception e) { + log.warn("Admin SSE CONNECT event send failed", e); sseService.removeEmitter(ADMIN_CHANNEL, emitter); + emitter.completeWithError(e); }🤖 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/AdminDashboardService.java` around lines 104 - 109, In AdminDashboardService inside the try/catch around emitter.send (the CONNECT dummy event), replace the silent catch with logging the exception and then calling emitter.completeWithError(e) in addition to sseService.removeEmitter(ADMIN_CHANNEL, emitter); specifically, catch Exception e, call your logger (e.g., log.error(...) or a configured logger in AdminDashboardService) with a descriptive message including e, invoke emitter.completeWithError(e) to mark the stream failed, and then call sseService.removeEmitter(ADMIN_CHANNEL, emitter) to clean up the emitter.
34-34: SSE 채널 상수의 위치를 공용 상수 클래스로 분리하는 것을 권장합니다.현재 상수를 서비스 클래스에 두면서 다른 도메인(
activity)이admin.service에 정적 의존하게 됩니다. 공용 계층(예:common.sse)으로 옮기면 모듈 경계가 더 안정적입니다.🤖 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/AdminDashboardService.java` at line 34, ADMIN_CHANNEL constant is defined inside AdminDashboardService causing unwanted static dependency; extract it into a shared constant class (e.g., create SseChannels or CommonSseConstants in the common.sse package) and move the value "ADMIN_DASHBOARD" there. Replace the ADMIN_CHANNEL field in AdminDashboardService with a reference to the new shared constant (e.g., SseChannels.ADMIN_CHANNEL) and update all imports/usages across modules (including the activity domain) to use the new constant; remove the old static field from AdminDashboardService afterwards.backend/src/main/java/org/sejongisc/backend/activity/repository/ActivityLogRepository.java (1)
40-44:countActivityByBoard조건식 단순화로 조회 성능을 개선할 수 있습니다.Line 42-43의
(:start IS NULL OR ...)패턴은 인덱스 활용을 저해할 수 있습니다. 호출부가 항상 기간 값을 넘긴다면BETWEEN :start AND :end로 고정하는 편이 안전합니다.♻️ 제안 수정안
- `@Query`("SELECT a.boardName, COUNT(a) FROM ActivityLog a " + - "WHERE a.activityType IN ('BOARD_POST', 'BOARD_COMMENT', 'BOARD_LIKE') " + - "AND (:start IS NULL OR a.createdAt >= :start) " + - "AND (:end IS NULL OR a.createdAt <= :end) " + - "GROUP BY a.boardName") + `@Query`("SELECT a.boardName, COUNT(a) FROM ActivityLog a " + + "WHERE a.activityType IN ('BOARD_POST', 'BOARD_COMMENT', 'BOARD_LIKE') " + + "AND a.createdAt BETWEEN :start AND :end " + + "GROUP BY a.boardName")🤖 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/activity/repository/ActivityLogRepository.java` around lines 40 - 44, 현재 ActivityLogRepository의 countActivityByBoard 쿼리에서 사용된 "(:start IS NULL OR a.createdAt >= :start) AND (:end IS NULL OR a.createdAt <= :end)" 조건은 인덱스 활용을 저해하므로, 호출부가 항상 기간을 전달한다는 전제하에 countActivityByBoard의 `@Query를` 간단히 변경해 a.createdAt BETWEEN :start AND :end로 고정하고 불필요한 NULL 체크를 제거하세요; 쿼리 안의 activityType IN 절과 GROUP BY a.boardName은 그대로 유지하고, 호출 코드가 null을 넘기지 않도록 호출자(메서드 사용처)를 확인해 기간 파라미터를 항상 전달하도록 보장하세요.backend/src/main/java/org/sejongisc/backend/admin/controller/AdminDashboardController.java (1)
70-74: 엔티티(ActivityLog)를 API 응답으로 직접 노출하지 않는 편이 안전합니다.응답 스키마가 엔티티 구조와 강결합되고, 필드 확장 시 의도치 않은 노출이 발생할 수 있습니다. DTO로 매핑해 반환하는 형태를 권장합니다.
🧩 제안 diff
- public ResponseEntity<Slice<ActivityLog>> getRecentActivities( + public ResponseEntity<Slice<ActivityLogResponse>> getRecentActivities( `@PageableDefault`(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable ) { - return ResponseEntity.ok(adminDashboardService.getRecentActivities(pageable)); + return ResponseEntity.ok(adminDashboardService.getRecentActivityResponses(pageable)); }🤖 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/AdminDashboardController.java` around lines 70 - 74, The controller currently returns the JPA entity ActivityLog directly from getRecentActivities, which tightly couples the API to the entity; change the endpoint to map the Slice<ActivityLog> returned by adminDashboardService.getRecentActivities(pageable) into a Slice of a DTO (e.g., ActivityLogDto) and return ResponseEntity<Slice<ActivityLogDto>> instead; create an ActivityLogDto with only the fields to expose, add a mapper method (e.g., ActivityLogMapper.toDto(ActivityLog) or a constructor/factory on ActivityLogDto) and apply the mapping before building the ResponseEntity in AdminDashboardController.getRecentActivities so the service can still return entities but the controller maps them to DTOs.
🤖 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 55-56: Add an explicit security check to
AdminBoardController.deleteBoard by annotating the deleteBoard(UUID boardId)
method with the same `@PreAuthorize` expression used by other admin endpoints
(restrict to PRESIDENT and SYSTEM_ADMIN), and ensure the PreAuthorize annotation
is imported (org.springframework.security.access.prepost.PreAuthorize); this
aligns deleteBoard with AdminUserController's protected methods and prevents
accidental exposure if global path-based rules change.
In
`@backend/src/main/java/org/sejongisc/backend/admin/controller/AdminDashboardController.java`:
- Around line 49-56: The methods getVisitorTrend(...) and
getBoardActivityStats(...) accept an unrestricted days param which can be
negative/zero/huge; add server-side validation in AdminDashboardController to
enforce a safe range (e.g., minDays = 1 and maxDays = 365) by checking the
incoming days at the start of each method and throwing a
ResponseStatusException(HttpStatus.BAD_REQUEST, "...") when days < minDays or
days > maxDays; keep the existing `@RequestParam` defaultValue, perform the check
in both getVisitorTrend and getBoardActivityStats before calling
adminDashboardService, and return/propagate the validated days to the service.
In
`@backend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.java`:
- Around line 67-70: The deleteBoard method in AdminBoardService lacks
authorization checks allowing unauthorized deletes; add method-level security by
annotating the deleteBoard(UUID boardId) method with
`@PreAuthorize`("hasRole('PRESIDENT')") (and ensure
`@EnableGlobalMethodSecurity`(prePostEnabled = true) is configured), or
alternatively perform an explicit role check inside
AdminBoardService.deleteBoard (e.g., inject Authentication/Principal or a
SecurityService and verify the caller has role PRESIDENT before calling
boardRepository.findById(...)); update references to the deleteBoard method to
reflect the added security requirement.
In
`@backend/src/main/java/org/sejongisc/backend/admin/service/AdminDashboardService.java`:
- Around line 73-75: Validate the days parameter in
AdminDashboardService.getVisitorTrend by adding a guard that checks if days <= 0
and throws a clear exception (e.g., IllegalArgumentException or a
BadRequest-like exception) with a message like "days must be >= 1" before
computing startDate; apply the same validation to any other method in
AdminDashboardService that computes startDate using
LocalDate.now().minusDays(...) (the blocks around where startDate is created) so
you never pass a non-positive value into minusDays.
In
`@backend/src/main/java/org/sejongisc/backend/common/exception/controller/GlobalExceptionHandler.java`:
- Line 21: The current log call in GlobalExceptionHandler logs only
e.getMessage(), losing the stack trace; change the log invocation to include the
exception object (e) so the full stack trace is recorded (e.g., call log.error
with the message and pass e as a throwable parameter, e.g., on the
log.error("CustomException 발생: {}", e.getMessage(), e) or
log.error("CustomException 발생", e) used inside the exception handler method that
references variable e).
---
Outside diff comments:
In
`@backend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.java`:
- Around line 72-76: The current deletion collects only direct children and
deletes in parent-first order, causing FK violations; implement a traversal to
collect the full descendant tree (use
boardRepository.findAllByParentBoard_BoardId and Board::getBoardId) such as a
BFS/DFS method (e.g., collectDescendantBoardIds(UUID rootId)) that returns all
descendant IDs, then delete in reverse order (leaf -> root) so children are
removed before their parents.
---
Nitpick comments:
In
`@backend/src/main/java/org/sejongisc/backend/activity/repository/ActivityLogRepository.java`:
- Around line 40-44: 현재 ActivityLogRepository의 countActivityByBoard 쿼리에서 사용된
"(:start IS NULL OR a.createdAt >= :start) AND (:end IS NULL OR a.createdAt <=
:end)" 조건은 인덱스 활용을 저해하므로, 호출부가 항상 기간을 전달한다는 전제하에 countActivityByBoard의 `@Query를`
간단히 변경해 a.createdAt BETWEEN :start AND :end로 고정하고 불필요한 NULL 체크를 제거하세요; 쿼리 안의
activityType IN 절과 GROUP BY a.boardName은 그대로 유지하고, 호출 코드가 null을 넘기지 않도록 호출자(메서드
사용처)를 확인해 기간 파라미터를 항상 전달하도록 보장하세요.
In
`@backend/src/main/java/org/sejongisc/backend/admin/controller/AdminDashboardController.java`:
- Around line 70-74: The controller currently returns the JPA entity ActivityLog
directly from getRecentActivities, which tightly couples the API to the entity;
change the endpoint to map the Slice<ActivityLog> returned by
adminDashboardService.getRecentActivities(pageable) into a Slice of a DTO (e.g.,
ActivityLogDto) and return ResponseEntity<Slice<ActivityLogDto>> instead; create
an ActivityLogDto with only the fields to expose, add a mapper method (e.g.,
ActivityLogMapper.toDto(ActivityLog) or a constructor/factory on ActivityLogDto)
and apply the mapping before building the ResponseEntity in
AdminDashboardController.getRecentActivities so the service can still return
entities but the controller maps them to DTOs.
In
`@backend/src/main/java/org/sejongisc/backend/admin/service/AdminDashboardService.java`:
- Around line 104-109: In AdminDashboardService inside the try/catch around
emitter.send (the CONNECT dummy event), replace the silent catch with logging
the exception and then calling emitter.completeWithError(e) in addition to
sseService.removeEmitter(ADMIN_CHANNEL, emitter); specifically, catch Exception
e, call your logger (e.g., log.error(...) or a configured logger in
AdminDashboardService) with a descriptive message including e, invoke
emitter.completeWithError(e) to mark the stream failed, and then call
sseService.removeEmitter(ADMIN_CHANNEL, emitter) to clean up the emitter.
- Line 34: ADMIN_CHANNEL constant is defined inside AdminDashboardService
causing unwanted static dependency; extract it into a shared constant class
(e.g., create SseChannels or CommonSseConstants in the common.sse package) and
move the value "ADMIN_DASHBOARD" there. Replace the ADMIN_CHANNEL field in
AdminDashboardService with a reference to the new shared constant (e.g.,
SseChannels.ADMIN_CHANNEL) and update all imports/usages across modules
(including the activity domain) to use the new constant; remove the old static
field from AdminDashboardService afterwards.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
backend/src/main/java/org/sejongisc/backend/activity/listener/ActivityEventListener.javabackend/src/main/java/org/sejongisc/backend/activity/repository/ActivityLogRepository.javabackend/src/main/java/org/sejongisc/backend/admin/controller/AdminBoardController.javabackend/src/main/java/org/sejongisc/backend/admin/controller/AdminDashboardController.javabackend/src/main/java/org/sejongisc/backend/admin/dto/dashboard/BoardActivityResponse.javabackend/src/main/java/org/sejongisc/backend/admin/dto/dashboard/SummaryResponse.javabackend/src/main/java/org/sejongisc/backend/admin/dto/dashboard/VisitorTrendResponse.javabackend/src/main/java/org/sejongisc/backend/admin/service/AdminBoardService.javabackend/src/main/java/org/sejongisc/backend/admin/service/AdminDashboardService.javabackend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConfig.javabackend/src/main/java/org/sejongisc/backend/common/exception/controller/GlobalExceptionHandler.java
Summary by CodeRabbit
새로운 기능
기타 / 보안