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..6073470 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/dto/response/AdminDashboardStatsResponse.java @@ -0,0 +1,11 @@ +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 = "상태가 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 new file mode 100644 index 0000000..97b6b62 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/service/AdminDashboardService.java @@ -0,0 +1,45 @@ +package redot.redot_server.domain.admin.service; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +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.entity.ConsultationStatus; +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() { + ZoneId kstZone = ZoneId.of("Asia/Seoul"); + LocalDateTime startOfTodayKst = LocalDate.now(kstZone).atStartOfDay(); + LocalDateTime startOfTodayUtc = startOfTodayKst.atZone(kstZone) + .withZoneSameInstant(ZoneOffset.UTC) + .toLocalDateTime(); + + long totalRedotMembers = redotMemberRepository.count(); + long redotMembersUntilYesterday = redotMemberRepository.countByCreatedAtBefore(startOfTodayUtc); + + long pendingConsultationCount = consultationRepository.countByStatus(ConsultationStatus.PENDING); + long adminCount = adminRepository.count(); + + return new AdminDashboardStatsResponse( + totalRedotMembers, + redotMembersUntilYesterday, + 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); } 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); } 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);