-
Notifications
You must be signed in to change notification settings - Fork 2
20260226 #236 관리자 페이지 통계 대시보드 실시간 활동 로그 추가 #266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
discipline24
merged 19 commits into
main
from
20260226_#236_관리자_페이지_통계_대시보드_실시간_활동_로그_추가
Mar 2, 2026
The head ref may contain hidden characters: "20260226_#236_\uAD00\uB9AC\uC790_\uD398\uC774\uC9C0_\uD1B5\uACC4_\uB300\uC2DC\uBCF4\uB4DC_\uC2E4\uC2DC\uAC04_\uD65C\uB3D9_\uB85C\uADF8_\uCD94\uAC00"
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
e4d7c01
[BE] [FEAT] activityLog 추가 및 이벤트 관련 코드 추가
discipline24 2dfdcc2
[BE] [FIX] deploy 관련 설정 변경
discipline24 3a5c8ce
[BE] [CHORE] db 설정 파일 주석 수정
discipline24 343c592
[BE] [FEAT] 댓글, 좋아요시 이벤트 발행 추가
discipline24 4c450af
[BE] [FIX] securityConfig에 세션 설정 stateless 로 변경
discipline24 ddded86
[BE] [FIX] 토끼 리뷰 반영
discipline24 b442969
Update deploy.yml
discipline24 a11403d
[BE] [FIX] message 형식 변경
discipline24 ae77ba2
[BE] [FIX] 회원가입 메서드 user -> auth로 이관
discipline24 082a15a
[BE] [FIX] auth -> user 메서드 이관
discipline24 6a5db7d
[BE] [CHORES] 비밀번호 초기화 docs 수정
discipline24 62feee0
[BE] [FEAT] 마이페이지 본인 관련 로그 조회 구현
discipline24 e6bc815
Update docker-compose.yml
discipline24 90c43f2
Merge branch 'main' into 20260226_#236_관리자_페이지_통계_대시보드_실시간_활동_로그_추가
discipline24 c0fb2eb
[BE] [FIX] 토끼 리뷰 반영
discipline24 19ed65d
[BE] [FIX] 비밀번호 초기화 로직 오류 수정
discipline24 8a40860
Merge branch 'main' into 20260226_#236_관리자_페이지_통계_대시보드_실시간_활동_로그_추가
discipline24 2a255a7
[BE] [FEAT] 대시보드 API 구현
discipline24 b2f5d9e
[BE] [FEAT] @PreAuthorize 이용한 관리자 API 명시적 권한 검증 추가
discipline24 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 78 additions & 1 deletion
79
backend/src/main/java/org/sejongisc/backend/admin/controller/AdminDashboardController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,81 @@ | ||
| package org.sejongisc.backend.admin.controller; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.sejongisc.backend.activity.entity.ActivityLog; | ||
| import org.sejongisc.backend.admin.dto.dashboard.BoardActivityResponse; | ||
| import org.sejongisc.backend.admin.dto.dashboard.SummaryResponse; | ||
| import org.sejongisc.backend.admin.dto.dashboard.VisitorTrendResponse; | ||
| import org.sejongisc.backend.admin.service.AdminDashboardService; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.domain.Slice; | ||
| import org.springframework.data.domain.Sort; | ||
| import org.springframework.data.web.PageableDefault; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.access.prepost.PreAuthorize; | ||
| import org.springframework.web.bind.annotation.*; | ||
| import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||
|
|
||
| import java.util.List; | ||
|
|
||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/admin/dashboard") | ||
| @Tag(name = "00. 관리자 대시보드 API", description = "대시보드 통계 및 실시간 로그 API") | ||
| public class AdminDashboardController { | ||
| } | ||
|
|
||
| private final AdminDashboardService adminDashboardService; | ||
|
|
||
| // --- [통계 요약 섹션] --- | ||
|
|
||
| @Operation(summary = "주간 게시판 활동 요약", description = "이번 주(일~현재) 게시글 활동 수와 전주 대비 증감률(%)을 반환합니다.") | ||
| @GetMapping("/stats/boards/summary") | ||
| @PreAuthorize("hasAnyRole('PRESIDENT', 'SYSTEM_ADMIN')") | ||
| public ResponseEntity<SummaryResponse> getWeeklyBoardSummary() { | ||
| return ResponseEntity.ok(adminDashboardService.getWeeklyBoardSummary()); | ||
| } | ||
|
|
||
| @Operation(summary = "주간 방문자 요약", description = "이번 주(일~현재) 누적 방문자 수와 전주 대비 증감률(%)을 반환합니다.") | ||
| @GetMapping("/stats/visitors/summary") | ||
| @PreAuthorize("hasAnyRole('PRESIDENT', 'SYSTEM_ADMIN')") | ||
| public ResponseEntity<SummaryResponse> getWeeklyVisitorSummary() { | ||
| return ResponseEntity.ok(adminDashboardService.getWeeklyVisitorSummary()); | ||
| } | ||
|
|
||
| // --- [차트 데이터 섹션] --- | ||
|
|
||
| @Operation(summary = "방문자 추이 데이터", description = "최근 n일간의 일별 방문자 수 데이터를 반환합니다.") | ||
| @GetMapping("/stats/visitors/trend") | ||
| @PreAuthorize("hasAnyRole('PRESIDENT', 'SYSTEM_ADMIN')") | ||
| public ResponseEntity<List<VisitorTrendResponse>> getVisitorTrend(@RequestParam(defaultValue = "7") int days) { | ||
| return ResponseEntity.ok(adminDashboardService.getVisitorTrend(days)); | ||
| } | ||
|
|
||
| @Operation(summary = "게시판별 활동 분포", description = "최근 n일간의 게시판별 활동량(게시글+댓글+좋아요) 집계를 반환합니다.") | ||
| @GetMapping("/stats/boards/distribution") | ||
| @PreAuthorize("hasAnyRole('PRESIDENT', 'SYSTEM_ADMIN')") | ||
| public ResponseEntity<List<BoardActivityResponse>> getBoardActivityStats(@RequestParam(defaultValue = "7") int days) { | ||
| return ResponseEntity.ok(adminDashboardService.getBoardActivityStats(days)); | ||
discipline24 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // --- [활동 로그 섹션] --- | ||
|
|
||
| @Operation(summary = "실시간 활동 로그 스트림 (SSE)", description = "관리자용 실시간 활동 로그 구독 세션을 엽니다.") | ||
| @GetMapping(value = "/activities/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) | ||
| @PreAuthorize("hasAnyRole('PRESIDENT', 'SYSTEM_ADMIN')") | ||
| public ResponseEntity<SseEmitter> streamRealtimeLog() { | ||
| return ResponseEntity.ok(adminDashboardService.subscribeActivityStream()); | ||
| } | ||
|
|
||
| @Operation(summary = "최근 활동 로그 목록 (페이징)", description = "대시보드 하단 로그를 최신순으로 조회합니다. hasNext 필드로 무한 스크롤을 구현하세요.") | ||
| @GetMapping("/activities") | ||
| @PreAuthorize("hasAnyRole('PRESIDENT', 'SYSTEM_ADMIN')") | ||
| public ResponseEntity<Slice<ActivityLog>> getRecentActivities( | ||
| @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable | ||
| ) { | ||
| return ResponseEntity.ok(adminDashboardService.getRecentActivities(pageable)); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
backend/src/main/java/org/sejongisc/backend/admin/dto/dashboard/BoardActivityResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package org.sejongisc.backend.admin.dto.dashboard; | ||
|
|
||
| public record BoardActivityResponse( | ||
| String boardName, | ||
| long activityCount | ||
| ) {} |
6 changes: 6 additions & 0 deletions
6
backend/src/main/java/org/sejongisc/backend/admin/dto/dashboard/SummaryResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package org.sejongisc.backend.admin.dto.dashboard; | ||
|
|
||
| public record SummaryResponse( | ||
| long count, | ||
| double percentageComparedToLastWeek | ||
| ) {} |
6 changes: 6 additions & 0 deletions
6
backend/src/main/java/org/sejongisc/backend/admin/dto/dashboard/VisitorTrendResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package org.sejongisc.backend.admin.dto.dashboard; | ||
|
|
||
| public record VisitorTrendResponse( | ||
| String date, // "YYYY-MM-DD" | ||
| long visitorCount | ||
| ) {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
125 changes: 125 additions & 0 deletions
125
backend/src/main/java/org/sejongisc/backend/admin/service/AdminDashboardService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| package org.sejongisc.backend.admin.service; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.sejongisc.backend.activity.entity.ActivityLog; | ||
| import org.sejongisc.backend.activity.entity.ActivityType; | ||
| import org.sejongisc.backend.activity.repository.ActivityLogRepository; | ||
| import org.sejongisc.backend.admin.dto.dashboard.BoardActivityResponse; | ||
| import org.sejongisc.backend.admin.dto.dashboard.SummaryResponse; | ||
| import org.sejongisc.backend.admin.dto.dashboard.VisitorTrendResponse; | ||
| import org.sejongisc.backend.common.sse.SseService; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.domain.Slice; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
| import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||
|
|
||
| import java.sql.Date; | ||
| import java.time.DayOfWeek; | ||
| import java.time.LocalDate; | ||
| import java.time.LocalDateTime; | ||
| import java.time.temporal.TemporalAdjusters; | ||
| import java.util.List; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| @Slf4j | ||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class AdminDashboardService { | ||
|
|
||
| private final ActivityLogRepository activityLogRepository; | ||
| private final SseService sseService; | ||
|
|
||
| public static final String ADMIN_CHANNEL = "ADMIN_DASHBOARD"; | ||
| // 공통 시간 계산용 Record | ||
| private record WeekRange(LocalDateTime start, LocalDateTime now, LocalDateTime lastStart, LocalDateTime lastEnd) {} | ||
|
|
||
| // 이번 주 일요일부터 현재까지의 범위 | ||
| private WeekRange calculateWeekRange() { | ||
| LocalDateTime startOfThisWeek = LocalDate.now() | ||
| .with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY)) | ||
| .atStartOfDay(); | ||
| LocalDateTime now = LocalDateTime.now(); | ||
|
|
||
| return new WeekRange(startOfThisWeek, now, startOfThisWeek.minusWeeks(1), now.minusWeeks(1)); | ||
| } | ||
|
|
||
| // 금주 활동량 요약 (일~토 기준) | ||
| @Transactional(readOnly = true) | ||
| public SummaryResponse getWeeklyBoardSummary() { | ||
| WeekRange range = calculateWeekRange(); | ||
| List<ActivityType> boardTypes = List.of(ActivityType.BOARD_POST, ActivityType.BOARD_COMMENT, ActivityType.BOARD_LIKE); | ||
|
|
||
| long thisWeek = activityLogRepository.countActivitiesByTypeAndPeriod(boardTypes, range.start(), range.now()); | ||
| long lastWeek = activityLogRepository.countActivitiesByTypeAndPeriod(boardTypes, range.lastStart(), range.lastEnd()); | ||
|
|
||
| return new SummaryResponse(thisWeek, calculatePercentage(thisWeek, lastWeek)); | ||
| } | ||
|
|
||
| // 금주 누적 방문자 요약 (일~토 기준 전주 대비) | ||
| @Transactional(readOnly = true) | ||
| public SummaryResponse getWeeklyVisitorSummary() { | ||
| WeekRange range = calculateWeekRange(); | ||
|
|
||
| long thisWeekVisitors = activityLogRepository.countDailyUniqueVisitors(range.start(), range.now()); | ||
| long lastWeekVisitors = activityLogRepository.countDailyUniqueVisitors(range.lastStart(), range.lastEnd()); | ||
|
|
||
| return new SummaryResponse(thisWeekVisitors, calculatePercentage(thisWeekVisitors, lastWeekVisitors)); | ||
| } | ||
|
|
||
| // N 일 방문자 추이 (차트용) | ||
| @Transactional(readOnly = true) | ||
| public List<VisitorTrendResponse> getVisitorTrend(int days) { | ||
| LocalDateTime startDate = LocalDate.now().minusDays(days - 1).atStartOfDay(); | ||
| List<Object[]> results = activityLogRepository.getDailyVisitorTrendNative(startDate); | ||
discipline24 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return results.stream() | ||
| .map(result -> new VisitorTrendResponse( | ||
| ((Date) result[0]).toString(), // java.sql.Date -> String | ||
| ((Number) result[1]).longValue() | ||
| )) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| // 게시판별 활동량 집계 (차트용) | ||
| @Transactional(readOnly = true) | ||
| public List<BoardActivityResponse> getBoardActivityStats(int days) { | ||
| LocalDateTime start = LocalDate.now().minusDays(days - 1).atStartOfDay(); | ||
| LocalDateTime end = LocalDateTime.now(); | ||
|
|
||
| List<Object[]> results = activityLogRepository.countActivityByBoard(start, end); | ||
|
|
||
| return results.stream() | ||
| .map(result -> new BoardActivityResponse( | ||
| (String) result[0], | ||
| ((Number) result[1]).longValue() | ||
| )) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| // 실시간 로그 스트림 구독 | ||
| public SseEmitter subscribeActivityStream() { | ||
| SseEmitter emitter = sseService.subscribe(ADMIN_CHANNEL); | ||
| try { | ||
| // 연결 시 503 에러 방지를 위한 더미 이벤트 발송 | ||
| emitter.send(SseEmitter.event().name("CONNECT").data("Connected to Admin SSE Stream")); | ||
| } catch (Exception e) { | ||
| sseService.removeEmitter(ADMIN_CHANNEL, emitter); | ||
| } | ||
| return emitter; | ||
| } | ||
|
|
||
| // 최근 로그 20개 조회 (최초 렌더링용) | ||
| @Transactional(readOnly = true) | ||
| public Slice<ActivityLog> getRecentActivities(Pageable pageable) { | ||
| return activityLogRepository.findAllByOrderByCreatedAtDesc(pageable); | ||
| } | ||
|
|
||
| // 증감률 계산 공통 메서드 | ||
| private double calculatePercentage(long current, long previous) { | ||
| if (previous == 0) return current > 0 ? 100.0 : 0.0; | ||
| double percentage = ((double) (current - previous) / previous) * 100; | ||
| return Math.round(percentage * 100.0) / 100.0; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.