Skip to content

Commit e9eec98

Browse files
authored
Merge pull request #147 from prgrms-aibe-devcourse/feat/admin
feat: admin LLM 지출분석 만족도 추이 표시
2 parents e3779a4 + 76c6ee7 commit e9eec98

8 files changed

Lines changed: 335 additions & 1 deletion

File tree

src/main/java/store/lastdance/controller/admin/AdminController.java

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import store.lastdance.domain.user.UserRole;
1919
import store.lastdance.dto.admin.*;
2020
import store.lastdance.dto.common.ErrorResponseDTO;
21+
import store.lastdance.dto.expense.ExpenseAnalysisHistoryDTO;
2122
import store.lastdance.security.oauth.CustomOAuth2User;
2223
import store.lastdance.service.admin.AdminService;
2324
import store.lastdance.dto.response.ApiResponse;
@@ -537,4 +538,116 @@ public ResponseEntity<ApiResponse<AiJudgmentStatsDTO>> getAiJudgmentStats(
537538
return ResponseEntity.ok(ApiResponse.success(stats, "AI 피드백 통계 조회 성공"));
538539
}
539540

541+
@Operation(
542+
summary = "LLM 지출분석 피드백 통계 조회",
543+
description = "LLM 지출분석 피드백에 대한 통계 정보를 조회합니다. 만족도, 불만족 카운트, 추세 등을 포함합니다.",
544+
security = @SecurityRequirement(name = "bearerAuth")
545+
)
546+
@ApiResponses(value = {
547+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
548+
responseCode = "200",
549+
description = "LLM 지출분석 피드백 통계 조회 성공",
550+
content = @Content(schema = @Schema(implementation = ExpenseAnalyzerFeedbackStatsDTO.class))
551+
),
552+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
553+
responseCode = "400",
554+
description = "잘못된 기간 파라미터",
555+
content = @Content(schema = @Schema(implementation = ErrorResponseDTO.class))
556+
),
557+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
558+
responseCode = "403",
559+
description = "관리자 권한 없음",
560+
content = @Content(schema = @Schema(implementation = ErrorResponseDTO.class))
561+
)
562+
})
563+
@GetMapping("/expense/analyzer/stats")
564+
public ResponseEntity<ApiResponse<ExpenseAnalyzerFeedbackStatsDTO>> getExpenseAnalyzerFeedbackStats(
565+
@Parameter(description = "조회 기간 (daily, weekly, monthly)") @RequestParam(value = "period", defaultValue = "weekly") String period,
566+
@AuthenticationPrincipal CustomOAuth2User user) {
567+
568+
log.info("LLM 지출분석 피드백 통계 요청: period={}, adminEmail={}", period, user.getEmail());
569+
570+
ExpenseAnalyzerFeedbackStatsDTO stats = adminService.getExpenseAnalyzerFeedbackStats(user.getUserId(), period);
571+
572+
log.info("LLM 지출분석 피드백 통계 응답: {}", stats);
573+
574+
return ResponseEntity.ok(ApiResponse.success(stats, "LLM 지출분석 피드백 통계 조회 성공"));
575+
}
576+
577+
@Operation(
578+
summary = "LLM 지출분석 내역 조회",
579+
description = "LLM 지출분석 내역을 조회합니다. 사용자, 평가, 날짜 등으로 필터링할 수 있습니다.",
580+
security = @SecurityRequirement(name = "bearerAuth")
581+
)
582+
@ApiResponses(value = {
583+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
584+
responseCode = "200",
585+
description = "LLM 지출분석 내역 조회 성공",
586+
content = @Content(schema = @Schema(implementation = AdminExpenseAnalyzerHistoryResponseDTO.class))
587+
),
588+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
589+
responseCode = "403",
590+
description = "관리자 권한 없음",
591+
content = @Content(schema = @Schema(implementation = ErrorResponseDTO.class))
592+
)
593+
})
594+
@GetMapping("/expense/analyzer")
595+
public ResponseEntity<ApiResponse<AdminExpenseAnalyzerHistoryResponseDTO>> getExpenseAnalyzerHistory(
596+
@Parameter(description = "페이지 번호 (1부터 시작)") @RequestParam(value = "page", defaultValue = "1") int page,
597+
@Parameter(description = "페이지당 조회할 항목 수") @RequestParam(value = "limit", defaultValue = "20") int limit,
598+
@Parameter(description = "검색 키워드 (사용자 이메일, 닉네임)") @RequestParam(value = "search", required = false) String search,
599+
@Parameter(description = "평가 필터 (UP, DOWN)") @RequestParam(value = "rating", required = false) String rating,
600+
@Parameter(description = "시작 날짜 (YYYY-MM-DD)") @RequestParam(value = "dateFrom", required = false) String dateFrom,
601+
@Parameter(description = "종료 날짜 (YYYY-MM-DD)") @RequestParam(value = "dateTo", required = false) String dateTo,
602+
@AuthenticationPrincipal CustomOAuth2User user) {
603+
604+
log.info("LLM 지출분석 내역 요청: page={}, limit={}, search={}, rating={}, dateFrom={}, dateTo={}, adminEmail={}",
605+
page, limit, search, rating, dateFrom, dateTo, user.getEmail());
606+
607+
AdminExpenseAnalyzerHistoryResponseDTO historyList = adminService.getExpenseAnalyzerHistory(
608+
user.getUserId(), page, limit, search, rating, dateFrom, dateTo
609+
);
610+
611+
if (historyList != null && historyList.histories() != null) {
612+
log.info("LLM 지출분석 내역 응답: {}건 조회", historyList.histories().size());
613+
}
614+
615+
return ResponseEntity.ok(ApiResponse.success(historyList, "LLM 지출분석 내역 조회 성공"));
616+
}
617+
618+
@Operation(
619+
summary = "LLM 지출분석 상세 조회",
620+
description = "특정 LLM 지출분석의 상세 정보를 조회합니다.",
621+
security = @SecurityRequirement(name = "bearerAuth")
622+
)
623+
@ApiResponses(value = {
624+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
625+
responseCode = "200",
626+
description = "LLM 지출분석 상세 조회 성공",
627+
content = @Content(schema = @Schema(implementation = ExpenseAnalysisHistoryDTO.class))
628+
),
629+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
630+
responseCode = "403",
631+
description = "관리자 권한 없음",
632+
content = @Content(schema = @Schema(implementation = ErrorResponseDTO.class))
633+
),
634+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
635+
responseCode = "404",
636+
description = "내역을 찾을 수 없음",
637+
content = @Content(schema = @Schema(implementation = ErrorResponseDTO.class))
638+
)
639+
})
640+
@GetMapping("/expense/analyzer/{historyId}")
641+
public ResponseEntity<ApiResponse<ExpenseAnalysisHistoryDTO>> getExpenseAnalyzerHistoryDetail(
642+
@Parameter(description = "조회할 내역 ID", required = true) @PathVariable Long historyId,
643+
@AuthenticationPrincipal CustomOAuth2User user) {
644+
645+
log.info("LLM 지출분석 상세 요청: historyId={}, adminEmail={}", historyId, user.getEmail());
646+
647+
ExpenseAnalysisHistoryDTO historyDetail = adminService.getExpenseAnalyzerHistoryDetail(user.getUserId(), historyId);
648+
649+
log.info("LLM 지출분석 상세 응답: {}", historyDetail);
650+
651+
return ResponseEntity.ok(ApiResponse.success(historyDetail, "LLM 지출분석 상세 조회 성공"));
652+
}
540653
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package store.lastdance.dto.admin;
2+
3+
import com.fasterxml.jackson.annotation.JsonFormat;
4+
import store.lastdance.domain.expense.ExpenseAnalysisHistory;
5+
6+
import java.time.LocalDateTime;
7+
8+
public record AdminExpenseAnalyzerHistoryDTO(
9+
Long id,
10+
String email,
11+
String nickname,
12+
LocalDateTime createdAt,
13+
Boolean up,
14+
Boolean down
15+
) {
16+
public static AdminExpenseAnalyzerHistoryDTO from(ExpenseAnalysisHistory history) {
17+
return new AdminExpenseAnalyzerHistoryDTO(
18+
history.getId(),
19+
history.getUser().getEmail(),
20+
history.getUser().getNickname(),
21+
history.getCreatedAt(),
22+
history.getUp(),
23+
history.getDown()
24+
);
25+
}
26+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package store.lastdance.dto.admin;
2+
3+
import java.util.List;
4+
5+
public record AdminExpenseAnalyzerHistoryResponseDTO(
6+
List<AdminExpenseAnalyzerHistoryDTO> histories,
7+
PaginationDTO pagination
8+
) {
9+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package store.lastdance.dto.admin;
2+
3+
import java.util.List;
4+
5+
public record ExpenseAnalyzerFeedbackStatsDTO(
6+
long totalFeedbacks,
7+
Long upCount,
8+
Long downCount,
9+
double satisfactionRate,
10+
List<FeedbackTrendDTO> trends
11+
) {
12+
}
13+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package store.lastdance.dto.admin;
2+
3+
public record FeedbackTrendDTO(
4+
int year,
5+
int month,
6+
int day,
7+
Long totalCount,
8+
Long upCount,
9+
Long downCount
10+
) {}

src/main/java/store/lastdance/repository/expense/ExpenseAnalysisHistoryRepository.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22

33
import org.springframework.data.domain.Page;
44
import org.springframework.data.domain.Pageable;
5+
import org.springframework.data.jpa.domain.Specification;
56
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
69
import store.lastdance.domain.expense.ExpenseAnalysisHistory;
710
import store.lastdance.domain.user.User;
11+
import store.lastdance.dto.admin.ExpenseAnalyzerFeedbackStatsDTO;
812

13+
import java.time.LocalDateTime;
914
import java.util.List;
1015
import java.util.UUID;
1116

@@ -15,4 +20,50 @@ public interface ExpenseAnalysisHistoryRepository extends JpaRepository<ExpenseA
1520
List<ExpenseAnalysisHistory> findByUserOrderByCreatedAtDesc(User user);
1621

1722
Page<ExpenseAnalysisHistory> findByUser(User user, Pageable pageable);
18-
}
23+
24+
List<ExpenseAnalysisHistory> findByCreatedAtBetween(LocalDateTime startDate, LocalDateTime endDate);
25+
26+
Page<ExpenseAnalysisHistory> findAll(Specification<ExpenseAnalysisHistory> spec, Pageable pageable);
27+
28+
@Query("SELECT new store.lastdance.dto.admin.ExpenseAnalyzerFeedbackStatsDTO(" +
29+
" COALESCE(SUM(CASE WHEN e.up = true OR e.down = true THEN 1 ELSE 0 END), 0L), " +
30+
" COALESCE(SUM(CASE WHEN e.up = true THEN 1 ELSE 0 END), 0L), " +
31+
" COALESCE(SUM(CASE WHEN e.down = true THEN 1 ELSE 0 END), 0L), " +
32+
" 0.0, " + // satisfactionRate will be calculated in service layer
33+
" null) " + // trends will be calculated in service layer
34+
"FROM ExpenseAnalysisHistory e")
35+
ExpenseAnalyzerFeedbackStatsDTO getFeedbackStatsSummary();
36+
37+
@Query("SELECT new store.lastdance.dto.admin.ExpenseAnalyzerFeedbackStatsDTO(" +
38+
" COALESCE(SUM(CASE WHEN e.up = true OR e.down = true THEN 1 ELSE 0 END), 0L), " +
39+
" COALESCE(SUM(CASE WHEN e.up = true THEN 1 ELSE 0 END), 0L), " +
40+
" COALESCE(SUM(CASE WHEN e.down = true THEN 1 ELSE 0 END), 0L), " +
41+
" 0.0, " + // satisfactionRate will be calculated in service layer
42+
" null) " + // trends will be calculated in service layer
43+
"FROM ExpenseAnalysisHistory e WHERE e.createdAt BETWEEN :startDate AND :endDate")
44+
ExpenseAnalyzerFeedbackStatsDTO getFeedbackStatsSummaryBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
45+
@Query("SELECT new store.lastdance.dto.admin.FeedbackTrendDTO(" +
46+
" YEAR(e.createdAt), " +
47+
" MONTH(e.createdAt), " +
48+
" DAY(e.createdAt), " +
49+
" COUNT(e), " +
50+
" COALESCE(SUM(CASE WHEN e.up = true THEN 1 ELSE 0 END), 0L), " +
51+
" COALESCE(SUM(CASE WHEN e.down = true THEN 1 ELSE 0 END), 0L)) " +
52+
"FROM ExpenseAnalysisHistory e " +
53+
"WHERE e.createdAt BETWEEN :startDate AND :endDate " +
54+
"GROUP BY YEAR(e.createdAt), MONTH(e.createdAt), DAY(e.createdAt) " +
55+
"ORDER BY YEAR(e.createdAt), MONTH(e.createdAt), DAY(e.createdAt)")
56+
List<store.lastdance.dto.admin.FeedbackTrendDTO> findFeedbackTrendsBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
57+
58+
@Query("SELECT new store.lastdance.dto.admin.FeedbackTrendDTO(" +
59+
" YEAR(e.createdAt), " +
60+
" MONTH(e.createdAt), " +
61+
" DAY(e.createdAt), " +
62+
" COUNT(e), " +
63+
" COALESCE(SUM(CASE WHEN e.up = true THEN 1 ELSE 0 END), 0L), " +
64+
" COALESCE(SUM(CASE WHEN e.down = true THEN 1 ELSE 0 END), 0L)) " +
65+
"FROM ExpenseAnalysisHistory e " +
66+
"GROUP BY YEAR(e.createdAt), MONTH(e.createdAt), DAY(e.createdAt) " +
67+
"ORDER BY YEAR(e.createdAt), MONTH(e.createdAt), DAY(e.createdAt)")
68+
List<store.lastdance.dto.admin.FeedbackTrendDTO> findFeedbackTrends();
69+
}

src/main/java/store/lastdance/service/admin/AdminService.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import store.lastdance.domain.admin.ReportType;
55
import store.lastdance.domain.user.UserRole;
66
import store.lastdance.dto.admin.*;
7+
import store.lastdance.dto.expense.ExpenseAnalysisHistoryDTO;
78

89
import java.util.List;
910
import java.util.UUID;
@@ -35,4 +36,10 @@ public interface AdminService {
3536
AiJudgmentDetailDTO getAiJudgmentDetail(UUID userId, UUID judgmentId);
3637

3738
AiJudgmentStatsDTO getAiJudgmentStats(UUID userId, String period);
39+
40+
ExpenseAnalyzerFeedbackStatsDTO getExpenseAnalyzerFeedbackStats(UUID userId, String period);
41+
42+
AdminExpenseAnalyzerHistoryResponseDTO getExpenseAnalyzerHistory(UUID userId, int page, int limit, String search, String rating, String dateFrom, String dateTo);
43+
44+
ExpenseAnalysisHistoryDTO getExpenseAnalyzerHistoryDetail(UUID userId, Long historyId);
3845
}

0 commit comments

Comments
 (0)