From 32c286fc66a4b1782bbf83d35efb1b4215f89fcb Mon Sep 17 00:00:00 2001 From: fnzl54 Date: Wed, 11 Mar 2026 09:33:31 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(#135):=20=ED=86=B5=EA=B3=84=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EC=B2=AB=20=EB=B2=88=EC=A7=B8=20=EC=8B=9C=EB=8F=84?= =?UTF-8?q?=20=ED=95=84=ED=84=B0=EB=A7=81=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(isFirst=20=EC=B6=94=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ReadDailySummaryService.java | 2 +- .../service/ReadHourlySummaryService.java | 2 +- ...AggregateSolveHourlySummaryStepConfig.java | 2 +- .../core/domin/entity/SolveHistory.java | 6 +++ .../SolveHistoryStatisticsRepository.java | 38 +++++++------------ .../GradeMemberWrongQuizzesService.java | 1 + 6 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/quizly/quizly/account/service/ReadDailySummaryService.java b/src/main/java/org/quizly/quizly/account/service/ReadDailySummaryService.java index af0a3da..11034ed 100644 --- a/src/main/java/org/quizly/quizly/account/service/ReadDailySummaryService.java +++ b/src/main/java/org/quizly/quizly/account/service/ReadDailySummaryService.java @@ -78,7 +78,7 @@ private List getPastDailySummary(User use private List getTodayDailySummary(User user, LocalDate today) { return solveHistoryStatisticsRepository - .findDailySummaryByUserAndDate(user, today) + .findFirstAttemptsDailySummaryByUserAndDate(user, today) .stream() .map(summary -> new ReadDailySummaryResponse.DailySummary( summary.getDate(), diff --git a/src/main/java/org/quizly/quizly/account/service/ReadHourlySummaryService.java b/src/main/java/org/quizly/quizly/account/service/ReadHourlySummaryService.java index c387565..2c658cd 100644 --- a/src/main/java/org/quizly/quizly/account/service/ReadHourlySummaryService.java +++ b/src/main/java/org/quizly/quizly/account/service/ReadHourlySummaryService.java @@ -91,7 +91,7 @@ private Map getPastHourlyDataMap(User user, LocalDate startDat private Map getTodayHourlyDataMap(User user, LocalDate today) { Map map = new HashMap<>(); List todayHourlyData = - solveHistoryStatisticsRepository.findHourlySummaryByUserAndDate(user, today); + solveHistoryStatisticsRepository.findFirstAttemptsHourlySummaryByUserAndDate(user, today); for (SolveHistoryStatisticsRepository.HourlySummary summary : todayHourlyData) { Integer hour = summary.getHourOfDay(); diff --git a/src/main/java/org/quizly/quizly/batch/step/AggregateSolveHourlySummaryStepConfig.java b/src/main/java/org/quizly/quizly/batch/step/AggregateSolveHourlySummaryStepConfig.java index 1acc928..872b83e 100644 --- a/src/main/java/org/quizly/quizly/batch/step/AggregateSolveHourlySummaryStepConfig.java +++ b/src/main/java/org/quizly/quizly/batch/step/AggregateSolveHourlySummaryStepConfig.java @@ -58,7 +58,7 @@ public ItemProcessor> aggregateSolveHourlySummary LocalDate targetDate = LocalDate.parse(targetDateStr); List hourlySummaryList = - solveHistoryStatisticsRepository.findHourlySummaryByUserAndDate(user, targetDate); + solveHistoryStatisticsRepository.findFirstAttemptsHourlySummaryByUserAndDate(user, targetDate); List existSolveHourlySummaryList = solveHourlySummaryRepository.findByUserAndDate(user, targetDate); diff --git a/src/main/java/org/quizly/quizly/core/domin/entity/SolveHistory.java b/src/main/java/org/quizly/quizly/core/domin/entity/SolveHistory.java index 951ede9..3891059 100644 --- a/src/main/java/org/quizly/quizly/core/domin/entity/SolveHistory.java +++ b/src/main/java/org/quizly/quizly/core/domin/entity/SolveHistory.java @@ -6,7 +6,9 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -44,5 +46,9 @@ public class SolveHistory extends BaseEntity { @Column(nullable = true) private LocalDateTime submittedAt; + @Column(nullable = false) + @Builder.Default + @Setter(AccessLevel.NONE) + private boolean isFirst = true; } diff --git a/src/main/java/org/quizly/quizly/core/domin/repository/SolveHistoryStatisticsRepository.java b/src/main/java/org/quizly/quizly/core/domin/repository/SolveHistoryStatisticsRepository.java index 2803419..fb15534 100644 --- a/src/main/java/org/quizly/quizly/core/domin/repository/SolveHistoryStatisticsRepository.java +++ b/src/main/java/org/quizly/quizly/core/domin/repository/SolveHistoryStatisticsRepository.java @@ -15,20 +15,13 @@ public interface SolveHistoryStatisticsRepository extends JpaRepository= :startDateTime " + "AND sh.submittedAt < :endDateTime " + - "AND (sh.quiz.id, sh.createdAt) IN (" + - " SELECT sh2.quiz.id, MIN(sh2.createdAt) " + - " FROM SolveHistory sh2 " + - " WHERE sh2.user = :user " + - " AND sh2.submittedAt >= :startDateTime " + - " AND sh2.submittedAt < :endDateTime " + - " GROUP BY sh2.quiz.id" + - ") " + + "AND sh.isFirst = TRUE " + "GROUP BY q.quizType") List findFirstAttemptsByQuizTypeAndDateTimeRange( @Param("user") User user, @@ -51,20 +44,13 @@ interface QuizTypeSummary { @Query(""" SELECT q.topic as topic, COUNT(sh) as totalCount, - SUM(CASE WHEN sh.isCorrect = true THEN 1 ELSE 0 END) as correctCount + SUM(CASE WHEN sh.isCorrect = TRUE THEN 1 ELSE 0 END) as correctCount FROM SolveHistory sh JOIN sh.quiz q WHERE sh.user = :user AND sh.submittedAt >= :startDateTime AND sh.submittedAt < :endDateTime - AND (sh.quiz.id, sh.createdAt) IN ( - SELECT sh2.quiz.id, MIN(sh2.createdAt) - FROM SolveHistory sh2 - WHERE sh2.user = :user - AND sh2.submittedAt >= :startDateTime - AND sh2.submittedAt < :endDateTime - GROUP BY sh2.quiz.id - ) + AND sh.isFirst = TRUE GROUP BY q.topic """) List findMonthlyTopicSummary( @@ -86,18 +72,19 @@ interface TopicSummary { "WHERE sh.user = :user " + "AND sh.submittedAt >= :startDateTime " + "AND sh.submittedAt < :endDateTime " + + "AND sh.isFirst = TRUE " + "GROUP BY CAST(sh.submittedAt AS LocalDate) " + "ORDER BY CAST(sh.submittedAt AS LocalDate)") - List findDailySummaryByUserAndDateTimeRange( + List findFirstAttemptsDailySummaryByUserAndDateTimeRange( @Param("user") User user, @Param("startDateTime") LocalDateTime startDateTime, @Param("endDateTime") LocalDateTime endDateTime ); - default List findDailySummaryByUserAndDate(User user, LocalDate date) { + default List findFirstAttemptsDailySummaryByUserAndDate(User user, LocalDate date) { LocalDateTime startDateTime = date.atStartOfDay(); LocalDateTime endDateTime = date.plusDays(1).atStartOfDay(); - return findDailySummaryByUserAndDateTimeRange(user, startDateTime, endDateTime); + return findFirstAttemptsDailySummaryByUserAndDateTimeRange(user, startDateTime, endDateTime); } @Query("SELECT FUNCTION('HOUR', sh.submittedAt) as hourOfDay, " + @@ -106,17 +93,18 @@ default List findDailySummaryByUserAndDate(User user, LocalDate da "WHERE sh.user = :user " + "AND sh.submittedAt >= :startDateTime " + "AND sh.submittedAt < :endDateTime " + + "AND sh.isFirst = TRUE " + "GROUP BY FUNCTION('HOUR', sh.submittedAt)") - List findHourlySummaryByUserAndDateTimeRange( + List findFirstAttemptsHourlySummaryByUserAndDateTimeRange( @Param("user") User user, @Param("startDateTime") LocalDateTime startDateTime, @Param("endDateTime") LocalDateTime endDateTime ); - default List findHourlySummaryByUserAndDate(User user, LocalDate date) { + default List findFirstAttemptsHourlySummaryByUserAndDate(User user, LocalDate date) { LocalDateTime startDateTime = date.atStartOfDay(); LocalDateTime endDateTime = date.plusDays(1).atStartOfDay(); - return findHourlySummaryByUserAndDateTimeRange(user, startDateTime, endDateTime); + return findFirstAttemptsHourlySummaryByUserAndDateTimeRange(user, startDateTime, endDateTime); } interface DailySummary { @@ -128,4 +116,4 @@ interface HourlySummary { Integer getHourOfDay(); Long getSolvedCount(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/quizly/quizly/quiz/service/GradeMemberWrongQuizzesService.java b/src/main/java/org/quizly/quizly/quiz/service/GradeMemberWrongQuizzesService.java index 1e0d427..eb27cb4 100644 --- a/src/main/java/org/quizly/quizly/quiz/service/GradeMemberWrongQuizzesService.java +++ b/src/main/java/org/quizly/quizly/quiz/service/GradeMemberWrongQuizzesService.java @@ -121,6 +121,7 @@ private void saveSolveHistory( .userAnswer(userAnswer) .solveTime(solveTime) .submittedAt(LocalDateTime.now()) + .isFirst(false) .build(); solveHistoryRepository.save(solveHistory); From e6fa77211bf8d2d84e8fdb8f50e65e4e2731002d Mon Sep 17 00:00:00 2001 From: yeonchaepark Date: Thu, 19 Mar 2026 18:25:41 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(#138):=20=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=20qna=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../get/AdminReadInquiriesController.java | 22 +++++++++++++------ .../request/AdminReadInquiriesRequest.java | 15 +++++++++++++ .../response/AdminReadInquiriesResponse.java | 4 ++++ .../service/AdminReadInquiriesService.java | 22 ++++++++++++------- .../domin/repository/InquiryRepository.java | 12 ++++++---- 5 files changed, 56 insertions(+), 19 deletions(-) create mode 100644 src/main/java/org/quizly/quizly/admin/dto/request/AdminReadInquiriesRequest.java diff --git a/src/main/java/org/quizly/quizly/admin/controller/get/AdminReadInquiriesController.java b/src/main/java/org/quizly/quizly/admin/controller/get/AdminReadInquiriesController.java index d3c2122..8433934 100644 --- a/src/main/java/org/quizly/quizly/admin/controller/get/AdminReadInquiriesController.java +++ b/src/main/java/org/quizly/quizly/admin/controller/get/AdminReadInquiriesController.java @@ -4,16 +4,18 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.quizly.quizly.admin.dto.request.AdminReadInquiriesRequest; import org.quizly.quizly.admin.dto.response.AdminReadInquiriesResponse; import org.quizly.quizly.admin.service.AdminReadInquiriesService; import org.quizly.quizly.configuration.swagger.ApiErrorCode; import org.quizly.quizly.core.application.BaseResponse; import org.quizly.quizly.core.domin.entity.Inquiry; import org.quizly.quizly.core.exception.error.GlobalErrorCode; +import org.quizly.quizly.core.presentation.Pagination; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -27,18 +29,21 @@ public class AdminReadInquiriesController { @Operation( summary = "관리자 문의 전체 조회 API", - description = "사용자가 등록한 문의를 전체 조회합니다.\n\n", + description = "사용자가 등록한 문의를 전체 조회합니다.\n\n" + + "- `page`: 페이지 번호 (기본값 1)\n" + + "- `pageSize`: 그룹 단위 페이지 크기 (기본값 10)\n", operationId = "/admin/inquiries" ) @GetMapping("/admin/inquiries") @PreAuthorize("hasRole('ADMIN')") @ApiErrorCode(errorCodes = {GlobalErrorCode.class, AdminReadInquiriesService.AdminReadInquiriesErrorCode.class}) public ResponseEntity adminReadInquiries( - @RequestParam(required = false) Inquiry.Status status - ){ + @ModelAttribute AdminReadInquiriesRequest request) + { AdminReadInquiriesService.AdminReadInquiriesResponse serviceResponse = adminReadInquiriesService.execute( AdminReadInquiriesService.AdminReadInquiriesRequest.builder() - .status(status) + .status(request.getStatus()) + .pageRequest(request.toPageRequest()) .build() ); if (serviceResponse == null || !serviceResponse.isSuccess()) { @@ -51,10 +56,12 @@ public ResponseEntity adminReadInquiries( }); } - return ResponseEntity.ok(toResponse(serviceResponse.getInquiryList())); + return ResponseEntity.ok(toResponse(serviceResponse.getInquiryList(),serviceResponse.getPagination())); } - private AdminReadInquiriesResponse toResponse(List inquiryList){ + private AdminReadInquiriesResponse toResponse( + List inquiryList, + Pagination pagination){ List details = inquiryList.stream() .map(inquiry -> new AdminReadInquiriesResponse.AdminInquiryDetail( inquiry.getId(), @@ -71,6 +78,7 @@ private AdminReadInquiriesResponse toResponse(List inquiryList){ return AdminReadInquiriesResponse.builder() .inquiryList(details) + .pagination(pagination) .build(); } diff --git a/src/main/java/org/quizly/quizly/admin/dto/request/AdminReadInquiriesRequest.java b/src/main/java/org/quizly/quizly/admin/dto/request/AdminReadInquiriesRequest.java new file mode 100644 index 0000000..d89f33d --- /dev/null +++ b/src/main/java/org/quizly/quizly/admin/dto/request/AdminReadInquiriesRequest.java @@ -0,0 +1,15 @@ +package org.quizly.quizly.admin.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.quizly.quizly.core.domin.entity.Inquiry; +import org.quizly.quizly.core.presentation.BasePaginationRequest; +@Getter +@Setter +@Schema(description = "관리자 문의 조회 요청") +public class AdminReadInquiriesRequest extends BasePaginationRequest { + + @Schema(description = "문의 답변 상태", example = "WAITING") + private Inquiry.Status status; +} diff --git a/src/main/java/org/quizly/quizly/admin/dto/response/AdminReadInquiriesResponse.java b/src/main/java/org/quizly/quizly/admin/dto/response/AdminReadInquiriesResponse.java index 7b29c44..9d1f08f 100644 --- a/src/main/java/org/quizly/quizly/admin/dto/response/AdminReadInquiriesResponse.java +++ b/src/main/java/org/quizly/quizly/admin/dto/response/AdminReadInquiriesResponse.java @@ -7,6 +7,7 @@ import org.quizly.quizly.core.application.BaseResponse; import org.quizly.quizly.core.domin.entity.Inquiry; import org.quizly.quizly.core.exception.error.GlobalErrorCode; +import org.quizly.quizly.core.presentation.Pagination; import java.time.LocalDateTime; import java.util.List; @@ -19,6 +20,9 @@ public class AdminReadInquiriesResponse extends BaseResponse { @Schema(description = "전체 문의 목록") private List inquiryList; + @Schema(description = "페이지네이션 정보") + private Pagination pagination; + @Schema(description = "전체 문의 목록 상세") public record AdminInquiryDetail( @Schema(description = "문의 ID", example = "1") diff --git a/src/main/java/org/quizly/quizly/admin/service/AdminReadInquiriesService.java b/src/main/java/org/quizly/quizly/admin/service/AdminReadInquiriesService.java index 81569e9..fbbaea6 100644 --- a/src/main/java/org/quizly/quizly/admin/service/AdminReadInquiriesService.java +++ b/src/main/java/org/quizly/quizly/admin/service/AdminReadInquiriesService.java @@ -9,6 +9,10 @@ import org.quizly.quizly.core.domin.repository.InquiryRepository; import org.quizly.quizly.core.exception.DomainException; import org.quizly.quizly.core.exception.error.BaseErrorCode; +import org.quizly.quizly.core.presentation.Pagination; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @@ -21,7 +25,7 @@ public class AdminReadInquiriesService implements BaseService { private final InquiryRepository inquiryRepository; - + private static final String SORT_BY_LATEST = "createdAt"; @Override public AdminReadInquiriesResponse execute(AdminReadInquiriesRequest request) { @@ -31,20 +35,20 @@ public AdminReadInquiriesResponse execute(AdminReadInquiriesRequest request) { .errorCode(AdminReadInquiriesErrorCode.NOT_EXIST_REQUIRED_PARAMETER) .build(); } + Pageable pageRequest = request.getPageRequest().withSort(Sort.by(Sort.Direction.DESC, SORT_BY_LATEST)); - Sort sort = Sort.by(Sort.Direction.DESC,"createdAt"); - - ListinquiryList; + Page inquiryPage; if(request.getStatus() == null){ - inquiryList = inquiryRepository.findAllWithUser(sort); + inquiryPage = inquiryRepository.findAllWithUser(pageRequest); }else{ - inquiryList = inquiryRepository.findAllByStatusWithUser(request.getStatus(),sort); + inquiryPage = inquiryRepository.findAllByStatusWithUser(request.getStatus(),pageRequest); } return AdminReadInquiriesResponse.builder() .success(true) - .inquiryList(inquiryList) + .inquiryList(inquiryPage.getContent()) + .pagination(Pagination.getPaginationFromPage(inquiryPage)) .build(); } @@ -73,11 +77,12 @@ public DomainException toException() { public static class AdminReadInquiriesRequest implements BaseRequest { private Inquiry.Status status; + private PageRequest pageRequest; @Override public boolean isValid() { - return true; + return pageRequest != null; } } @@ -90,6 +95,7 @@ public boolean isValid() { public static class AdminReadInquiriesResponse extends BaseResponse { private List inquiryList; + private Pagination pagination; } } diff --git a/src/main/java/org/quizly/quizly/core/domin/repository/InquiryRepository.java b/src/main/java/org/quizly/quizly/core/domin/repository/InquiryRepository.java index d4e4fc3..270a559 100644 --- a/src/main/java/org/quizly/quizly/core/domin/repository/InquiryRepository.java +++ b/src/main/java/org/quizly/quizly/core/domin/repository/InquiryRepository.java @@ -2,6 +2,8 @@ import org.quizly.quizly.core.domin.entity.Inquiry; import org.quizly.quizly.core.domin.entity.User; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -12,9 +14,11 @@ public interface InquiryRepository extends JpaRepository { List findAllByUser(User user); - @Query("SELECT i FROM Inquiry i JOIN FETCH i.user ") - List findAllWithUser(Sort sort); + @Query(value = "SELECT i FROM Inquiry i JOIN FETCH i.user", + countQuery = "SELECT count(i) FROM Inquiry i") + Page findAllWithUser(Pageable pageable); - @Query("SELECT i FROM Inquiry i JOIN FETCH i.user WHERE i.status =:status") - List findAllByStatusWithUser(@Param("status") Inquiry.Status status, Sort sort); + @Query(value = "SELECT i FROM Inquiry i JOIN FETCH i.user WHERE i.status = :status", + countQuery = "SELECT count(i) FROM Inquiry i WHERE i.status = :status") + Page findAllByStatusWithUser(@Param("status") Inquiry.Status status, Pageable pageable); }