From ef8780d4ad3be5789c58fb96a0614d0a6b3ac47c Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Wed, 17 Dec 2025 11:17:43 +0900 Subject: [PATCH 1/6] feat(admin): implement admin dashboard stats endpoint with member and consultation metrics --- .../controller/AdminDashboardController.java | 24 +++++++++++ .../docs/AdminDashboardControllerDocs.java | 18 ++++++++ .../response/AdminDashboardStatsResponse.java | 12 ++++++ .../admin/service/AdminDashboardService.java | 41 +++++++++++++++++++ .../repository/RedotMemberRepository.java | 3 ++ 5 files changed, 98 insertions(+) create mode 100644 src/main/java/redot/redot_server/domain/admin/controller/AdminDashboardController.java create mode 100644 src/main/java/redot/redot_server/domain/admin/controller/docs/AdminDashboardControllerDocs.java create mode 100644 src/main/java/redot/redot_server/domain/admin/dto/response/AdminDashboardStatsResponse.java create mode 100644 src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java diff --git a/src/main/java/redot/redot_server/domain/admin/controller/AdminDashboardController.java b/src/main/java/redot/redot_server/domain/admin/controller/AdminDashboardController.java new file mode 100644 index 0000000..56e8fb9 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/controller/AdminDashboardController.java @@ -0,0 +1,24 @@ +package redot.redot_server.domain.admin.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import redot.redot_server.domain.admin.controller.docs.AdminDashboardControllerDocs; +import redot.redot_server.domain.admin.dto.response.AdminDashboardStatsResponse; +import redot.redot_server.domain.admin.service.AdminDashboardService; + +@RestController +@RequestMapping("/api/v1/redot/admin/dashboard") +@RequiredArgsConstructor +public class AdminDashboardController implements AdminDashboardControllerDocs { + + private final AdminDashboardService adminDashboardService; + + @GetMapping("/stats") + @Override + public ResponseEntity getDashboardStats() { + return ResponseEntity.ok(adminDashboardService.getDashboardStats()); + } +} diff --git a/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminDashboardControllerDocs.java b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminDashboardControllerDocs.java new file mode 100644 index 0000000..f061972 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminDashboardControllerDocs.java @@ -0,0 +1,18 @@ +package redot.redot_server.domain.admin.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import redot.redot_server.domain.admin.dto.response.AdminDashboardStatsResponse; + +@Tag(name = "Admin Dashboard", description = "관리자 대시보드 통계 API") +public interface AdminDashboardControllerDocs { + + @Operation(summary = "대시보드 통계 조회", description = "관리자 대시보드에 필요한 요약 통계를 제공합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = AdminDashboardStatsResponse.class))) + ResponseEntity getDashboardStats(); +} diff --git a/src/main/java/redot/redot_server/domain/admin/dto/response/AdminDashboardStatsResponse.java b/src/main/java/redot/redot_server/domain/admin/dto/response/AdminDashboardStatsResponse.java new file mode 100644 index 0000000..070b632 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/dto/response/AdminDashboardStatsResponse.java @@ -0,0 +1,12 @@ +package redot.redot_server.domain.admin.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record AdminDashboardStatsResponse( + @Schema(description = "전체 Redot 회원 수") long totalRedotMembers, + @Schema(description = "전일까지 누적된 Redot 회원 수") long redotMembersUntilYesterday, + @Schema(description = "전일 이후 가입한 Redot 회원 수") long newRedotMembersSinceYesterday, + @Schema(description = "누적 상담 요청 수") long consultationRequestCount, + @Schema(description = "등록된 관리자 수") long adminCount +) { +} diff --git a/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java b/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java new file mode 100644 index 0000000..312ce6a --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java @@ -0,0 +1,41 @@ +package redot.redot_server.domain.admin.service; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import redot.redot_server.domain.admin.dto.response.AdminDashboardStatsResponse; +import redot.redot_server.domain.admin.repository.AdminRepository; +import redot.redot_server.domain.redot.consultation.repository.ConsultationRepository; +import redot.redot_server.domain.redot.member.repository.RedotMemberRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AdminDashboardService { + + private final RedotMemberRepository redotMemberRepository; + private final ConsultationRepository consultationRepository; + private final AdminRepository adminRepository; + + public AdminDashboardStatsResponse getDashboardStats() { + LocalDateTime startOfToday = LocalDate.now(ZoneId.systemDefault()).atStartOfDay(); + + long totalRedotMembers = redotMemberRepository.count(); + long redotMembersUntilYesterday = redotMemberRepository.countByCreatedAtBefore(startOfToday); + long newRedotMembersSinceYesterday = Math.max(0L, totalRedotMembers - redotMembersUntilYesterday); + + long consultationRequestCount = consultationRepository.count(); + long adminCount = adminRepository.count(); + + return new AdminDashboardStatsResponse( + totalRedotMembers, + redotMembersUntilYesterday, + newRedotMembersSinceYesterday, + consultationRequestCount, + adminCount + ); + } +} diff --git a/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepository.java b/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepository.java index 3e0d197..6b96043 100644 --- a/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepository.java +++ b/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepository.java @@ -1,5 +1,6 @@ package redot.redot_server.domain.redot.member.repository; +import java.time.LocalDateTime; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import redot.redot_server.domain.redot.member.entity.RedotMember; @@ -11,4 +12,6 @@ public interface RedotMemberRepository extends JpaRepository, Optional findByEmail(String email); Optional findBySocialProviderAndSocialProviderId(SocialProvider provider, String socialProviderId); + + long countByCreatedAtBefore(LocalDateTime before); } From 32d0421062b36b332b50c9c051891b9452853a1b Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Wed, 17 Dec 2025 18:08:54 +0900 Subject: [PATCH 2/6] feat(admin): update admin dashboard stats to include pending consultation count --- .../admin/dto/response/AdminDashboardStatsResponse.java | 3 +-- .../domain/admin/service/AdminDashboardService.java | 7 +++---- .../consultation/repository/ConsultationRepository.java | 3 +++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/redot/redot_server/domain/admin/dto/response/AdminDashboardStatsResponse.java b/src/main/java/redot/redot_server/domain/admin/dto/response/AdminDashboardStatsResponse.java index 070b632..6073470 100644 --- a/src/main/java/redot/redot_server/domain/admin/dto/response/AdminDashboardStatsResponse.java +++ b/src/main/java/redot/redot_server/domain/admin/dto/response/AdminDashboardStatsResponse.java @@ -5,8 +5,7 @@ public record AdminDashboardStatsResponse( @Schema(description = "전체 Redot 회원 수") long totalRedotMembers, @Schema(description = "전일까지 누적된 Redot 회원 수") long redotMembersUntilYesterday, - @Schema(description = "전일 이후 가입한 Redot 회원 수") long newRedotMembersSinceYesterday, - @Schema(description = "누적 상담 요청 수") long consultationRequestCount, + @Schema(description = "상태가 PENDING 인 상담 요청 수") long pendingConsultationCount, @Schema(description = "등록된 관리자 수") long adminCount ) { } diff --git a/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java b/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java index 312ce6a..6245557 100644 --- a/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java +++ b/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java @@ -8,6 +8,7 @@ import org.springframework.transaction.annotation.Transactional; import redot.redot_server.domain.admin.dto.response.AdminDashboardStatsResponse; import redot.redot_server.domain.admin.repository.AdminRepository; +import redot.redot_server.domain.redot.consultation.entity.ConsultationStatus; import redot.redot_server.domain.redot.consultation.repository.ConsultationRepository; import redot.redot_server.domain.redot.member.repository.RedotMemberRepository; @@ -25,16 +26,14 @@ public AdminDashboardStatsResponse getDashboardStats() { long totalRedotMembers = redotMemberRepository.count(); long redotMembersUntilYesterday = redotMemberRepository.countByCreatedAtBefore(startOfToday); - long newRedotMembersSinceYesterday = Math.max(0L, totalRedotMembers - redotMembersUntilYesterday); - long consultationRequestCount = consultationRepository.count(); + long pendingConsultationCount = consultationRepository.countByStatus(ConsultationStatus.PENDING); long adminCount = adminRepository.count(); return new AdminDashboardStatsResponse( totalRedotMembers, redotMembersUntilYesterday, - newRedotMembersSinceYesterday, - consultationRequestCount, + pendingConsultationCount, adminCount ); } diff --git a/src/main/java/redot/redot_server/domain/redot/consultation/repository/ConsultationRepository.java b/src/main/java/redot/redot_server/domain/redot/consultation/repository/ConsultationRepository.java index d8f6500..ff7d556 100644 --- a/src/main/java/redot/redot_server/domain/redot/consultation/repository/ConsultationRepository.java +++ b/src/main/java/redot/redot_server/domain/redot/consultation/repository/ConsultationRepository.java @@ -2,6 +2,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import redot.redot_server.domain.redot.consultation.entity.Consultation; +import redot.redot_server.domain.redot.consultation.entity.ConsultationStatus; public interface ConsultationRepository extends JpaRepository, ConsultationRepositoryCustom { + + long countByStatus(ConsultationStatus status); } From 02bfe61ec1a0888cd60241b412099f1db5e85063 Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Wed, 17 Dec 2025 18:11:20 +0900 Subject: [PATCH 3/6] feat(admin): adjust timezone for admin dashboard stats to Asia/Seoul --- .../domain/admin/service/AdminDashboardService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java b/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java index 6245557..2bec8d7 100644 --- a/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java +++ b/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java @@ -22,7 +22,7 @@ public class AdminDashboardService { private final AdminRepository adminRepository; public AdminDashboardStatsResponse getDashboardStats() { - LocalDateTime startOfToday = LocalDate.now(ZoneId.systemDefault()).atStartOfDay(); + LocalDateTime startOfToday = LocalDate.now(ZoneId.of("Asia/Seoul")).atStartOfDay(); long totalRedotMembers = redotMemberRepository.count(); long redotMembersUntilYesterday = redotMemberRepository.countByCreatedAtBefore(startOfToday); From 37871a4b392e729143d57054eb79fbb46bf6805a Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Wed, 17 Dec 2025 18:11:26 +0900 Subject: [PATCH 4/6] feat(admin): add index on created_at column for redot_members table --- .../db/migration/V7__add_index_to_redot_members_created_at.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/main/resources/db/migration/V7__add_index_to_redot_members_created_at.sql diff --git a/src/main/resources/db/migration/V7__add_index_to_redot_members_created_at.sql b/src/main/resources/db/migration/V7__add_index_to_redot_members_created_at.sql new file mode 100644 index 0000000..e8a5e28 --- /dev/null +++ b/src/main/resources/db/migration/V7__add_index_to_redot_members_created_at.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS idx_redot_members_created_at ON redot_members(created_at); From 4f18f67953fa4537ff2fdc5389ed1231dd45638d Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Wed, 17 Dec 2025 18:20:29 +0900 Subject: [PATCH 5/6] feat(admin): refactor dashboard stats to use ZonedDateTime for accurate time handling --- .../domain/admin/service/AdminDashboardService.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java b/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java index 2bec8d7..3bd0a00 100644 --- a/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java +++ b/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java @@ -1,8 +1,8 @@ package redot.redot_server.domain.admin.service; import java.time.LocalDate; -import java.time.LocalDateTime; import java.time.ZoneId; +import java.time.ZonedDateTime; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,10 +22,12 @@ public class AdminDashboardService { private final AdminRepository adminRepository; public AdminDashboardStatsResponse getDashboardStats() { - LocalDateTime startOfToday = LocalDate.now(ZoneId.of("Asia/Seoul")).atStartOfDay(); + ZoneId kstZone = ZoneId.of("Asia/Seoul"); + ZonedDateTime startOfTodayKst = LocalDate.now(kstZone).atStartOfDay(kstZone); + ZonedDateTime startOfTodayUtc = startOfTodayKst.withZoneSameInstant(ZoneId.of("UTC")); long totalRedotMembers = redotMemberRepository.count(); - long redotMembersUntilYesterday = redotMemberRepository.countByCreatedAtBefore(startOfToday); + long redotMembersUntilYesterday = redotMemberRepository.countByCreatedAtBefore(startOfTodayUtc.toLocalDateTime()); long pendingConsultationCount = consultationRepository.countByStatus(ConsultationStatus.PENDING); long adminCount = adminRepository.count(); From d2d89ab612d1f298402e8bc44ca055db6dee971e Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Wed, 17 Dec 2025 18:26:10 +0900 Subject: [PATCH 6/6] feat(admin): refactor dashboard stats to use LocalDateTime for improved time handling --- .../domain/admin/service/AdminDashboardService.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java b/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java index 3bd0a00..97b6b62 100644 --- a/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java +++ b/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java @@ -1,8 +1,9 @@ package redot.redot_server.domain.admin.service; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.ZoneOffset; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,11 +24,13 @@ public class AdminDashboardService { public AdminDashboardStatsResponse getDashboardStats() { ZoneId kstZone = ZoneId.of("Asia/Seoul"); - ZonedDateTime startOfTodayKst = LocalDate.now(kstZone).atStartOfDay(kstZone); - ZonedDateTime startOfTodayUtc = startOfTodayKst.withZoneSameInstant(ZoneId.of("UTC")); + LocalDateTime startOfTodayKst = LocalDate.now(kstZone).atStartOfDay(); + LocalDateTime startOfTodayUtc = startOfTodayKst.atZone(kstZone) + .withZoneSameInstant(ZoneOffset.UTC) + .toLocalDateTime(); long totalRedotMembers = redotMemberRepository.count(); - long redotMembersUntilYesterday = redotMemberRepository.countByCreatedAtBefore(startOfTodayUtc.toLocalDateTime()); + long redotMembersUntilYesterday = redotMemberRepository.countByCreatedAtBefore(startOfTodayUtc); long pendingConsultationCount = consultationRepository.countByStatus(ConsultationStatus.PENDING); long adminCount = adminRepository.count();