From e496fb6af5a98f23f1f0b735b75da01f1aa7647f Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 25 Apr 2026 15:36:32 +0900 Subject: [PATCH 01/43] =?UTF-8?q?[docs]=20Agent=20=ED=8C=8C=EC=9D=BC=20ign?= =?UTF-8?q?ore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 56e89db9..64f367e6 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ monitoring/README.md ### macOS .DS_Store + +### Local agent guidance +/AGENTS.md From dfdbb114a5cd13c7a70daaefb0290c3980e4728e Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 25 Apr 2026 21:30:12 +0900 Subject: [PATCH 02/43] =?UTF-8?q?[feat]=20=EC=9C=A0=EC=A0=80=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EC=A0=95=EB=A0=AC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/controller/AdminUserController.java | 46 +++++++++++++-- src/main/resources/templates/users.html | 56 +++++++++++++++++-- 2 files changed, 92 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminUserController.java b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminUserController.java index bfd197c1..17def2a1 100644 --- a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminUserController.java +++ b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminUserController.java @@ -18,7 +18,11 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; +import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; @Slf4j @RequiredArgsConstructor @@ -36,21 +40,39 @@ public class AdminUserController { public String getAllUsers( @RequestParam(defaultValue = "0", name = "page") int page, @RequestParam(defaultValue = "20", name = "size") int size, + @RequestParam(defaultValue = "createdAt", name = "sortBy") String sortBy, + @RequestParam(defaultValue = "desc", name = "direction") String direction, Model model ) { List allUsers = userService.findAllUsers(); + Map problemCountByUserId = allUsers.stream() + .collect(Collectors.toMap( + UserResponseDto::userId, + user -> problemService.findProblemCountByUser(user.userId()) + )); + + Comparator comparator = getUserComparator(sortBy, problemCountByUserId); + if ("desc".equalsIgnoreCase(direction)) { + comparator = comparator.reversed(); + } + + List sortedUsers = allUsers.stream() + .sorted(comparator.thenComparing(UserResponseDto::userId, Comparator.reverseOrder())) + .toList(); // 페이징 계산 - int totalUsers = allUsers.size(); + int totalUsers = sortedUsers.size(); int totalPages = (int) Math.ceil((double) totalUsers / size); int startIndex = page * size; int endIndex = Math.min(startIndex + size, totalUsers); - List pagedUsers = allUsers.subList(startIndex, endIndex); + List pagedUsers = startIndex >= totalUsers + ? List.of() + : sortedUsers.subList(startIndex, endIndex); // 각 유저의 문제 개수 계산 List problemCounts = pagedUsers.stream() - .map(user -> problemService.findProblemCountByUser(user.userId())) + .map(user -> problemCountByUserId.get(user.userId())) .toList(); model.addAttribute("users", pagedUsers); @@ -59,10 +81,26 @@ public String getAllUsers( model.addAttribute("totalPages", totalPages); model.addAttribute("totalUsers", totalUsers); model.addAttribute("size", size); + model.addAttribute("sortBy", sortBy); + model.addAttribute("direction", direction); return "users"; } + private Comparator getUserComparator(String sortBy, Map problemCountByUserId) { + Function sortKey = switch (sortBy) { + case "level" -> UserResponseDto::totalStudyLevel; + case "problemCount" -> user -> problemCountByUserId.getOrDefault(user.userId(), 0L); + default -> null; + }; + + if (sortKey == null) { + return Comparator.comparing(UserResponseDto::createdAt); + } + + return Comparator.comparing(sortKey); + } + @GetMapping("/user/{userId}") public String getUserDetailsById(@PathVariable(name = "userId") Long userId, Model model) { UserResponseDto user = userService.findUser(userId); @@ -117,4 +155,4 @@ public String updateUserLevel( public void deleteUserInfo(@PathVariable(name = "userId") Long userId) { userService.deleteUserById(userId); } -} \ No newline at end of file +} diff --git a/src/main/resources/templates/users.html b/src/main/resources/templates/users.html index 3ba8ea6d..ada8a187 100644 --- a/src/main/resources/templates/users.html +++ b/src/main/resources/templates/users.html @@ -47,6 +47,38 @@

유저 관리

0명의 유저

+
+
+ +
+ + +
+
+ + +
+
+ + +
+ +
+
+
@@ -56,8 +88,20 @@

유저 관리

유저 ID 이름 이메일 - 총 학습 레벨 - 문제 수 + + + 총 학습 레벨 + 내림차순 + + + + + 문제 수 + 내림차순 + + 가입일 @@ -97,7 +141,7 @@

유저 관리

이전 @@ -114,7 +158,7 @@

유저 관리

1 1 @@ -123,7 +167,7 @@

유저 관리

다음 @@ -135,4 +179,4 @@

유저 관리

- \ No newline at end of file + From fd8da3a0fca197aec54536cbc4a3c2a84206525d Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 25 Apr 2026 21:30:34 +0900 Subject: [PATCH 03/43] =?UTF-8?q?[feat]=20=ED=86=B5=EA=B3=84=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B8=B0=EA=B0=84=20=EC=84=A4=EC=A0=95=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminAnalysisController.java | 32 ++++++++++++---- .../MissionLogRepositoryCustom.java | 2 + .../repository/MissionLogRepositoryImpl.java | 10 +++-- .../mission/service/MissionLogService.java | 5 +++ .../OnO/backend/user/service/UserService.java | 10 +++-- src/main/resources/templates/analysis.html | 37 ++++++++++++++++--- 6 files changed, 77 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminAnalysisController.java b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminAnalysisController.java index 24952228..c237280c 100644 --- a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminAnalysisController.java +++ b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminAnalysisController.java @@ -30,20 +30,36 @@ public class AdminAnalysisController { private final MissionLogService missionLogService; @GetMapping("/analysis") - public String getAllAnalysis(Model model) { + public String getAllAnalysis( + @RequestParam(name = "startDate", required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam(name = "endDate", required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, + Model model + ) { int allUserCount = userService.findAllUsers().size(); int allProblemCount = problemService.findAllProblems().size(); - // 최근 30일간 날짜별 출석 유저 수 및 신규 가입자 수 - Map dailyActiveUsers = missionLogService.getDailyActiveUsersCount(30); - Map dailyNewUsers = userService.getDailyNewUsersCount(30); + LocalDate today = LocalDate.now(); + LocalDate selectedStartDate = startDate != null ? startDate : today.minusDays(29); + LocalDate selectedEndDate = endDate != null ? endDate : today; - // 최근 30일 신규 가입자 총합 + if (selectedStartDate.isAfter(selectedEndDate)) { + LocalDate temp = selectedStartDate; + selectedStartDate = selectedEndDate; + selectedEndDate = temp; + } + + // 선택 기간 날짜별 출석 유저 수 및 신규 가입자 수 + Map dailyActiveUsers = missionLogService.getDailyActiveUsersCount(selectedStartDate, selectedEndDate); + Map dailyNewUsers = userService.getDailyNewUsersCount(selectedStartDate, selectedEndDate); + + // 선택 기간 신규 가입자 총합 long recentNewUsersCount = dailyNewUsers.values().stream() .mapToLong(Long::longValue) .sum(); - // 하루 평균 방문자 수 (최근 30일) + // 하루 평균 방문자 수 (선택 기간) double averageDailyVisitors = dailyActiveUsers.values().stream() .mapToLong(Long::longValue) .average() @@ -55,6 +71,8 @@ public String getAllAnalysis(Model model) { model.addAttribute("dailyNewUsers", dailyNewUsers); model.addAttribute("recentNewUsersCount", recentNewUsersCount); model.addAttribute("averageDailyVisitors", averageDailyVisitors); + model.addAttribute("startDate", selectedStartDate); + model.addAttribute("endDate", selectedEndDate); return "analysis"; } @@ -91,4 +109,4 @@ public String getDailyActiveUsers( return "daily-users"; } -} \ No newline at end of file +} diff --git a/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryCustom.java b/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryCustom.java index 541e4bbf..33a6cbea 100644 --- a/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryCustom.java +++ b/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryCustom.java @@ -19,5 +19,7 @@ public interface MissionLogRepositoryCustom { Map getDailyActiveUsersCount(int days); + Map getDailyActiveUsersCount(LocalDate startDate, LocalDate endDate); + List getActiveUsersByDate(LocalDate date); } diff --git a/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryImpl.java b/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryImpl.java index 20c3a722..e4121667 100644 --- a/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryImpl.java +++ b/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryImpl.java @@ -87,12 +87,16 @@ public Long getPointSumToday(Long userId){ @Override public Map getDailyActiveUsersCount(int days) { - Map result = new LinkedHashMap<>(); LocalDate today = LocalDate.now(); + return getDailyActiveUsersCount(today.minusDays(days - 1L), today); + } + + @Override + public Map getDailyActiveUsersCount(LocalDate startDate, LocalDate endDate) { + Map result = new LinkedHashMap<>(); // 최근 날짜가 위로 오도록 역순으로 조회 - for (int i = 0; i < days; i++) { - LocalDate date = today.minusDays(i); + for (LocalDate date = endDate; !date.isBefore(startDate); date = date.minusDays(1)) { LocalDateTime startOfDay = date.atStartOfDay(); LocalDateTime endOfDay = date.atTime(LocalTime.MAX); diff --git a/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java b/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java index facc7b10..730924ae 100644 --- a/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java +++ b/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java @@ -158,6 +158,11 @@ public Map getDailyActiveUsersCount(int days) { return missionLogRepository.getDailyActiveUsersCount(days); } + @Transactional(readOnly = true) + public Map getDailyActiveUsersCount(LocalDate startDate, LocalDate endDate) { + return missionLogRepository.getDailyActiveUsersCount(startDate, endDate); + } + @Transactional(readOnly = true) public List getActiveUsersByDate(LocalDate date) { return missionLogRepository.getActiveUsersByDate(date); diff --git a/src/main/java/com/aisip/OnO/backend/user/service/UserService.java b/src/main/java/com/aisip/OnO/backend/user/service/UserService.java index 4164bdd0..3c940d0a 100644 --- a/src/main/java/com/aisip/OnO/backend/user/service/UserService.java +++ b/src/main/java/com/aisip/OnO/backend/user/service/UserService.java @@ -156,13 +156,17 @@ public void updateUserLevel(Long userId, String levelType, Long levelValue, Long @Transactional(readOnly = true) public Map getDailyNewUsersCount(int days) { - Map result = new LinkedHashMap<>(); LocalDate today = LocalDate.now(); + return getDailyNewUsersCount(today.minusDays(days - 1L), today); + } + + @Transactional(readOnly = true) + public Map getDailyNewUsersCount(LocalDate startDate, LocalDate endDate) { + Map result = new LinkedHashMap<>(); List allUsers = userRepository.findAll(); // 최근 날짜가 위로 오도록 역순으로 조회 - for (int i = 0; i < days; i++) { - LocalDate date = today.minusDays(i); + for (LocalDate date = endDate; !date.isBefore(startDate); date = date.minusDays(1)) { LocalDateTime startOfDay = date.atStartOfDay(); LocalDateTime endOfDay = date.atTime(LocalTime.MAX); diff --git a/src/main/resources/templates/analysis.html b/src/main/resources/templates/analysis.html index 7c6186b8..66015b23 100644 --- a/src/main/resources/templates/analysis.html +++ b/src/main/resources/templates/analysis.html @@ -47,6 +47,31 @@

시스템 통계

OnO 애플리케이션 통계 개요

+
+
+
+ + +
+
+ + +
+ +
+

+ 현재 기간: + 2024-01-01 + 부터 + 2024-01-30 + 까지 +

+
+
@@ -100,15 +125,15 @@

시스템 통계

-

최근 30일 기준

+

선택 기간 기준

- +
-

최근 30일 가입자 수

+

선택 기간 가입자 수

0

@@ -118,7 +143,7 @@

시스템 통계

-

지난 30일간 신규 가입

+

선택 기간 신규 가입

@@ -186,7 +211,7 @@

-

날짜별 출석 유저 수 (최근 30일)

+

날짜별 출석 유저 수

@@ -280,4 +305,4 @@

빠른 이동

- \ No newline at end of file + From 1d4dcb2db20f2d023f0ee88612564dd698958a62 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 25 Apr 2026 21:45:47 +0900 Subject: [PATCH 04/43] =?UTF-8?q?[feat]=20=EB=B3=B5=EC=8A=B5=EB=85=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminPracticeNoteController.java | 51 ++++++ .../dto/AdminPracticeLogResponseDto.java | 29 +++ .../dto/AdminPracticeNoteResponseDto.java | 31 ++++ .../repository/MissionLogRepository.java | 6 + .../mission/service/MissionLogService.java | 27 +++ .../service/PracticeNoteService.java | 25 +++ src/main/resources/templates/admin.html | 30 +++- src/main/resources/templates/analysis.html | 17 +- src/main/resources/templates/daily-users.html | 5 +- .../resources/templates/practice-notes.html | 167 ++++++++++++++++++ src/main/resources/templates/problem.html | 5 +- src/main/resources/templates/problems.html | 5 +- src/main/resources/templates/user.html | 5 +- src/main/resources/templates/users.html | 3 + 14 files changed, 398 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/aisip/OnO/backend/admin/controller/AdminPracticeNoteController.java create mode 100644 src/main/java/com/aisip/OnO/backend/admin/dto/AdminPracticeLogResponseDto.java create mode 100644 src/main/java/com/aisip/OnO/backend/admin/dto/AdminPracticeNoteResponseDto.java create mode 100644 src/main/resources/templates/practice-notes.html diff --git a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminPracticeNoteController.java b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminPracticeNoteController.java new file mode 100644 index 00000000..750deaee --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminPracticeNoteController.java @@ -0,0 +1,51 @@ +package com.aisip.OnO.backend.admin.controller; + +import com.aisip.OnO.backend.admin.dto.AdminPracticeLogResponseDto; +import com.aisip.OnO.backend.admin.dto.AdminPracticeNoteResponseDto; +import com.aisip.OnO.backend.mission.service.MissionLogService; +import com.aisip.OnO.backend.practicenote.service.PracticeNoteService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Slf4j +@RequiredArgsConstructor +@Controller +@RequestMapping("/admin") +public class AdminPracticeNoteController { + + private final PracticeNoteService practiceNoteService; + private final MissionLogService missionLogService; + + @GetMapping("/practice-notes") + public String getPracticeNotes( + @RequestParam(defaultValue = "0", name = "notePage") int notePage, + @RequestParam(defaultValue = "0", name = "logPage") int logPage, + @RequestParam(defaultValue = "20", name = "size") int size, + Model model + ) { + int selectedNotePage = Math.max(notePage, 0); + int selectedLogPage = Math.max(logPage, 0); + int selectedSize = Math.max(size, 1); + + Page practiceNotes = practiceNoteService.findAdminPracticeNotes(selectedNotePage, selectedSize); + Page practiceLogs = missionLogService.findAdminPracticeLogs(selectedLogPage, selectedSize); + + model.addAttribute("practiceNotes", practiceNotes.getContent()); + model.addAttribute("practiceLogs", practiceLogs.getContent()); + model.addAttribute("notePage", selectedNotePage); + model.addAttribute("logPage", selectedLogPage); + model.addAttribute("noteTotalPages", practiceNotes.getTotalPages()); + model.addAttribute("logTotalPages", practiceLogs.getTotalPages()); + model.addAttribute("totalPracticeNotes", practiceNotes.getTotalElements()); + model.addAttribute("totalPracticeLogs", practiceLogs.getTotalElements()); + model.addAttribute("size", selectedSize); + + return "practice-notes"; + } +} diff --git a/src/main/java/com/aisip/OnO/backend/admin/dto/AdminPracticeLogResponseDto.java b/src/main/java/com/aisip/OnO/backend/admin/dto/AdminPracticeLogResponseDto.java new file mode 100644 index 00000000..03d2b5ac --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/admin/dto/AdminPracticeLogResponseDto.java @@ -0,0 +1,29 @@ +package com.aisip.OnO.backend.admin.dto; + +import com.aisip.OnO.backend.mission.entity.MissionLog; +import com.aisip.OnO.backend.practicenote.entity.PracticeNote; +import java.time.LocalDateTime; + +public record AdminPracticeLogResponseDto( + Long missionLogId, + Long userId, + String userName, + String userEmail, + Long practiceNoteId, + String practiceTitle, + Long point, + LocalDateTime createdAt +) { + public static AdminPracticeLogResponseDto from(MissionLog missionLog, PracticeNote practiceNote) { + return new AdminPracticeLogResponseDto( + missionLog.getId(), + missionLog.getUser().getId(), + missionLog.getUser().getName(), + missionLog.getUser().getEmail(), + missionLog.getReferenceId(), + practiceNote != null ? practiceNote.getTitle() : "-", + missionLog.getPoint(), + missionLog.getCreatedAt() + ); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/admin/dto/AdminPracticeNoteResponseDto.java b/src/main/java/com/aisip/OnO/backend/admin/dto/AdminPracticeNoteResponseDto.java new file mode 100644 index 00000000..1a1423cf --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/admin/dto/AdminPracticeNoteResponseDto.java @@ -0,0 +1,31 @@ +package com.aisip.OnO.backend.admin.dto; + +import com.aisip.OnO.backend.practicenote.entity.PracticeNote; +import com.aisip.OnO.backend.user.entity.User; +import java.time.LocalDateTime; + +public record AdminPracticeNoteResponseDto( + Long practiceNoteId, + Long userId, + String userName, + String userEmail, + String practiceTitle, + Long problemCount, + Long practiceCount, + LocalDateTime lastSolvedAt, + LocalDateTime createdAt +) { + public static AdminPracticeNoteResponseDto from(PracticeNote practiceNote, User user, Long problemCount) { + return new AdminPracticeNoteResponseDto( + practiceNote.getId(), + practiceNote.getUserId(), + user != null ? user.getName() : "-", + user != null ? user.getEmail() : "-", + practiceNote.getTitle(), + problemCount, + practiceNote.getPracticeCount(), + practiceNote.getLastSolvedAt(), + practiceNote.getCreatedAt() + ); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepository.java b/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepository.java index 7d355cd7..5c853845 100644 --- a/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepository.java +++ b/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepository.java @@ -1,9 +1,15 @@ package com.aisip.OnO.backend.mission.repository; +import com.aisip.OnO.backend.mission.entity.MissionType; import com.aisip.OnO.backend.mission.entity.MissionLog; import java.util.List; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface MissionLogRepository extends JpaRepository, MissionLogRepositoryCustom { List findAllByUserId(Long userId); + + List findAllByMissionType(MissionType missionType, Pageable pageable); + + long countByMissionType(MissionType missionType); } diff --git a/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java b/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java index 730924ae..f4654928 100644 --- a/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java +++ b/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java @@ -1,11 +1,14 @@ package com.aisip.OnO.backend.mission.service; +import com.aisip.OnO.backend.admin.dto.AdminPracticeLogResponseDto; import com.aisip.OnO.backend.common.exception.ApplicationException; import com.aisip.OnO.backend.mission.dto.MissionRegisterDto; import com.aisip.OnO.backend.mission.entity.MissionLog; import com.aisip.OnO.backend.mission.entity.MissionType; import com.aisip.OnO.backend.mission.exception.MissionErrorCase; import com.aisip.OnO.backend.mission.repository.MissionLogRepository; +import com.aisip.OnO.backend.practicenote.entity.PracticeNote; +import com.aisip.OnO.backend.practicenote.repository.PracticeNoteRepository; import com.aisip.OnO.backend.user.entity.User; import com.aisip.OnO.backend.user.repository.UserRepository; import java.time.LocalDate; @@ -13,6 +16,10 @@ import java.util.Map; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,6 +32,8 @@ public class MissionLogService { private final UserRepository userRepository; + private final PracticeNoteRepository practiceNoteRepository; + private static final Long DAILY_MISSION_POINT_LIMIT = 200L; public Long registerMissionLog(@NotNull MissionRegisterDto missionRegisterDto) { @@ -167,4 +176,22 @@ public Map getDailyActiveUsersCount(LocalDate startDate, LocalD public List getActiveUsersByDate(LocalDate date) { return missionLogRepository.getActiveUsersByDate(date); } + + @Transactional(readOnly = true) + public Page findAdminPracticeLogs(int page, int size) { + PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + List missionLogs = missionLogRepository.findAllByMissionType(MissionType.NOTE_PRACTICE, pageRequest); + long total = missionLogRepository.countByMissionType(MissionType.NOTE_PRACTICE); + + List content = missionLogs.stream() + .map(missionLog -> { + PracticeNote practiceNote = missionLog.getReferenceId() != null + ? practiceNoteRepository.findById(missionLog.getReferenceId()).orElse(null) + : null; + return AdminPracticeLogResponseDto.from(missionLog, practiceNote); + }) + .toList(); + + return new PageImpl<>(content, pageRequest, total); + } } diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java b/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java index 624e554b..cb940ef4 100644 --- a/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java +++ b/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java @@ -1,5 +1,6 @@ package com.aisip.OnO.backend.practicenote.service; +import com.aisip.OnO.backend.admin.dto.AdminPracticeNoteResponseDto; import com.aisip.OnO.backend.common.exception.ApplicationException; import com.aisip.OnO.backend.common.response.CursorPageResponse; import com.aisip.OnO.backend.mission.service.MissionLogService; @@ -13,8 +14,14 @@ import com.aisip.OnO.backend.problem.exception.ProblemErrorCase; import com.aisip.OnO.backend.problem.repository.ProblemRepository; import com.aisip.OnO.backend.practicenote.repository.PracticeNoteRepository; +import com.aisip.OnO.backend.user.entity.User; +import com.aisip.OnO.backend.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -39,6 +46,8 @@ public class PracticeNoteService { private final MissionLogService missionLogService; + private final UserRepository userRepository; + private PracticeNote getPracticeEntity(Long practiceId){ return practiceNoteRepository.findById(practiceId) @@ -226,4 +235,20 @@ public CursorPageResponse findPracticeThumbnai log.info("userId: {} find practice thumbnails with cursor: {}, size: {}, hasNext: {}", userId, cursor, size, hasNext); return CursorPageResponse.of(dtoList, nextCursor, hasNext, size); } + + @Transactional(readOnly = true) + public Page findAdminPracticeNotes(int page, int size) { + PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + Page practiceNotePage = practiceNoteRepository.findAll(pageRequest); + + List content = practiceNotePage.getContent().stream() + .map(practiceNote -> { + User user = userRepository.findById(practiceNote.getUserId()).orElse(null); + Long problemCount = (long) practiceNoteRepository.findProblemIdListByPracticeNoteId(practiceNote.getId()).size(); + return AdminPracticeNoteResponseDto.from(practiceNote, user, problemCount); + }) + .toList(); + + return new PageImpl<>(content, pageRequest, practiceNotePage.getTotalElements()); + } } diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html index c5ad9724..ee8307c6 100644 --- a/src/main/resources/templates/admin.html +++ b/src/main/resources/templates/admin.html @@ -18,6 +18,9 @@

OnO 관리자

대시보드 + + 복습 +
-
+
@@ -74,6 +77,24 @@

문제 관리

+ + +
+
+
+ + + +
+ + + +
+

복습 관리

+

복습노트 및 복습 기록 조회

+
+
+
@@ -96,13 +117,16 @@

통계 분석

빠른 이동

-
- \ No newline at end of file + diff --git a/src/main/resources/templates/analysis.html b/src/main/resources/templates/analysis.html index 66015b23..7b10391f 100644 --- a/src/main/resources/templates/analysis.html +++ b/src/main/resources/templates/analysis.html @@ -26,6 +26,9 @@

OnO 관리자

문제 + + 복습 + 통계 @@ -265,7 +268,7 @@

날짜별 출석 유저 수

빠른 이동

-
+ - \ No newline at end of file + diff --git a/src/main/resources/templates/practice-notes.html b/src/main/resources/templates/practice-notes.html new file mode 100644 index 00000000..2a3bdf3a --- /dev/null +++ b/src/main/resources/templates/practice-notes.html @@ -0,0 +1,167 @@ + + + + + + OnO 관리자 - 복습 관리 + + + + + +
+
+

복습 관리

+

+ 복습노트 0개, + 복습 기록 0개 +

+
+ +
+
+

총 복습노트 수

+

0

+
+
+

총 복습 기록 수

+

0

+
+
+ +
+
+

복습노트 목록

+
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
복습노트 ID유저제목문제 수복습 횟수마지막 복습일작성일
1 + 유저 +

user@example.com

+
복습노트00 + 2024-01-01 00:00 + - + 2024-01-01 00:00
복습노트가 없습니다
+
+ +
+ 이전 + 이전 + + 1 / 1 + + 다음 + 다음 +
+
+ +
+
+

복습 기록

+
+
+ + + + + + + + + + + + + + + + + + + + + + +
기록 ID유저복습노트포인트완료일
1 + 유저 +

user@example.com

+
+ 복습노트 + #1 + 15pt2024-01-01 00:00
복습 기록이 없습니다
+
+ +
+ 이전 + 이전 + + 1 / 1 + + 다음 + 다음 +
+
+
+ + diff --git a/src/main/resources/templates/problem.html b/src/main/resources/templates/problem.html index 1eaa1dc4..6595da26 100644 --- a/src/main/resources/templates/problem.html +++ b/src/main/resources/templates/problem.html @@ -26,6 +26,9 @@

OnO 관리자

문제 + + 복습 + 통계 @@ -227,4 +230,4 @@

AI 분석 결과

- \ No newline at end of file + diff --git a/src/main/resources/templates/problems.html b/src/main/resources/templates/problems.html index 4ff00d15..716e7148 100644 --- a/src/main/resources/templates/problems.html +++ b/src/main/resources/templates/problems.html @@ -26,6 +26,9 @@

OnO 관리자

문제 + + 복습 + 통계 @@ -177,4 +180,4 @@

문제 관리

- \ No newline at end of file + diff --git a/src/main/resources/templates/user.html b/src/main/resources/templates/user.html index 064f835a..e99f0fed 100644 --- a/src/main/resources/templates/user.html +++ b/src/main/resources/templates/user.html @@ -26,6 +26,9 @@

OnO 관리자

문제 + + 복습 + 통계 @@ -454,4 +457,4 @@

레벨 수정 } - \ No newline at end of file + diff --git a/src/main/resources/templates/users.html b/src/main/resources/templates/users.html index ada8a187..c10e2bb9 100644 --- a/src/main/resources/templates/users.html +++ b/src/main/resources/templates/users.html @@ -26,6 +26,9 @@

OnO 관리자

문제 + + 복습 + 통계 From aa6c805631ac241cfc8de9d880cd3af72e6544af Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 25 Apr 2026 22:36:47 +0900 Subject: [PATCH 05/43] =?UTF-8?q?[feat]=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=86=B5=EA=B3=84=20=EC=A1=B0=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminAnalysisController.java | 22 +++++- .../repository/MissionLogRepository.java | 22 ++++++ .../repository/MissionLogRepositoryImpl.java | 42 +++++++---- .../mission/service/MissionLogService.java | 44 ++++++++++++ .../repository/PracticeNoteRepository.java | 14 ++++ .../service/PracticeNoteService.java | 42 +++++++++++ .../problem/service/ProblemService.java | 5 ++ .../user/repository/UserRepository.java | 17 +++++ .../OnO/backend/user/service/UserService.java | 53 +++++++------- src/main/resources/templates/analysis.html | 69 +++++++++++++++++-- 10 files changed, 285 insertions(+), 45 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminAnalysisController.java b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminAnalysisController.java index c237280c..17ca66a9 100644 --- a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminAnalysisController.java +++ b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminAnalysisController.java @@ -1,6 +1,7 @@ package com.aisip.OnO.backend.admin.controller; import com.aisip.OnO.backend.mission.service.MissionLogService; +import com.aisip.OnO.backend.practicenote.service.PracticeNoteService; import com.aisip.OnO.backend.problem.service.ProblemService; import com.aisip.OnO.backend.user.dto.UserResponseDto; import com.aisip.OnO.backend.user.entity.User; @@ -28,6 +29,7 @@ public class AdminAnalysisController { private final UserService userService; private final ProblemService problemService; private final MissionLogService missionLogService; + private final PracticeNoteService practiceNoteService; @GetMapping("/analysis") public String getAllAnalysis( @@ -37,8 +39,10 @@ public String getAllAnalysis( @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, Model model ) { - int allUserCount = userService.findAllUsers().size(); - int allProblemCount = problemService.findAllProblems().size(); + long allUserCount = userService.countAllUsers(); + long allProblemCount = problemService.countAllProblems(); + long allPracticeNoteCount = practiceNoteService.countAllPracticeNotes(); + long allPracticeLogCount = missionLogService.countNotePracticeLogs(); LocalDate today = LocalDate.now(); LocalDate selectedStartDate = startDate != null ? startDate : today.minusDays(29); @@ -53,11 +57,19 @@ public String getAllAnalysis( // 선택 기간 날짜별 출석 유저 수 및 신규 가입자 수 Map dailyActiveUsers = missionLogService.getDailyActiveUsersCount(selectedStartDate, selectedEndDate); Map dailyNewUsers = userService.getDailyNewUsersCount(selectedStartDate, selectedEndDate); + Map dailyPracticeNotes = practiceNoteService.getDailyPracticeNotesCount(selectedStartDate, selectedEndDate); + Map dailyPracticeLogs = missionLogService.getDailyNotePracticeLogsCount(selectedStartDate, selectedEndDate); // 선택 기간 신규 가입자 총합 long recentNewUsersCount = dailyNewUsers.values().stream() .mapToLong(Long::longValue) .sum(); + long periodPracticeNoteCount = dailyPracticeNotes.values().stream() + .mapToLong(Long::longValue) + .sum(); + long periodPracticeLogCount = dailyPracticeLogs.values().stream() + .mapToLong(Long::longValue) + .sum(); // 하루 평균 방문자 수 (선택 기간) double averageDailyVisitors = dailyActiveUsers.values().stream() @@ -67,9 +79,15 @@ public String getAllAnalysis( model.addAttribute("allUserCount", allUserCount); model.addAttribute("allProblemCount", allProblemCount); + model.addAttribute("allPracticeNoteCount", allPracticeNoteCount); + model.addAttribute("allPracticeLogCount", allPracticeLogCount); model.addAttribute("dailyActiveUsers", dailyActiveUsers); model.addAttribute("dailyNewUsers", dailyNewUsers); + model.addAttribute("dailyPracticeNotes", dailyPracticeNotes); + model.addAttribute("dailyPracticeLogs", dailyPracticeLogs); model.addAttribute("recentNewUsersCount", recentNewUsersCount); + model.addAttribute("periodPracticeNoteCount", periodPracticeNoteCount); + model.addAttribute("periodPracticeLogCount", periodPracticeLogCount); model.addAttribute("averageDailyVisitors", averageDailyVisitors); model.addAttribute("startDate", selectedStartDate); model.addAttribute("endDate", selectedEndDate); diff --git a/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepository.java b/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepository.java index 5c853845..05c0866e 100644 --- a/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepository.java +++ b/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepository.java @@ -2,9 +2,12 @@ import com.aisip.OnO.backend.mission.entity.MissionType; import com.aisip.OnO.backend.mission.entity.MissionLog; +import java.time.LocalDateTime; import java.util.List; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface MissionLogRepository extends JpaRepository, MissionLogRepositoryCustom { List findAllByUserId(Long userId); @@ -12,4 +15,23 @@ public interface MissionLogRepository extends JpaRepository, M List findAllByMissionType(MissionType missionType, Pageable pageable); long countByMissionType(MissionType missionType); + + long countByMissionTypeAndCreatedAtBetween( + MissionType missionType, + LocalDateTime startDateTime, + LocalDateTime endDateTime + ); + + @Query(""" + SELECT FUNCTION('DATE', m.createdAt), COUNT(m) + FROM MissionLog m + WHERE m.missionType = :missionType + AND m.createdAt BETWEEN :startDateTime AND :endDateTime + GROUP BY FUNCTION('DATE', m.createdAt) + """) + List countDailyByMissionType( + @Param("missionType") MissionType missionType, + @Param("startDateTime") LocalDateTime startDateTime, + @Param("endDateTime") LocalDateTime endDateTime + ); } diff --git a/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryImpl.java b/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryImpl.java index e4121667..903b86e4 100644 --- a/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryImpl.java +++ b/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryImpl.java @@ -2,6 +2,10 @@ import com.aisip.OnO.backend.mission.entity.MissionType; import com.aisip.OnO.backend.user.entity.User; +import com.querydsl.core.Tuple; +import com.querydsl.core.types.dsl.DateExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; @@ -94,21 +98,33 @@ public Map getDailyActiveUsersCount(int days) { @Override public Map getDailyActiveUsersCount(LocalDate startDate, LocalDate endDate) { Map result = new LinkedHashMap<>(); + DateExpression createdDate = Expressions.dateTemplate( + LocalDate.class, + "date({0})", + missionLog.createdAt + ); + NumberExpression activeUserCount = missionLog.user.id.countDistinct(); + + java.util.List dailyCounts = queryFactory + .select(createdDate, activeUserCount) + .from(missionLog) + .where(missionLog.missionType.eq(MissionType.USER_LOGIN) + .and(missionLog.createdAt.between(startDate.atStartOfDay(), endDate.atTime(LocalTime.MAX))) + ) + .groupBy(createdDate) + .fetch(); + + Map countByDate = new LinkedHashMap<>(); + for (Tuple row : dailyCounts) { + LocalDate date = row.get(createdDate); + Long count = row.get(activeUserCount); + if (date != null) { + countByDate.put(date, count != null ? count : 0L); + } + } - // 최근 날짜가 위로 오도록 역순으로 조회 for (LocalDate date = endDate; !date.isBefore(startDate); date = date.minusDays(1)) { - LocalDateTime startOfDay = date.atStartOfDay(); - LocalDateTime endOfDay = date.atTime(LocalTime.MAX); - - Long count = queryFactory - .select(missionLog.user.id.countDistinct()) - .from(missionLog) - .where(missionLog.missionType.eq(MissionType.USER_LOGIN) - .and(missionLog.createdAt.between(startOfDay, endOfDay)) - ) - .fetchOne(); - - result.put(date, count != null ? count : 0L); + result.put(date, countByDate.getOrDefault(date, 0L)); } return result; diff --git a/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java b/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java index f4654928..c8429880 100644 --- a/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java +++ b/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java @@ -11,7 +11,12 @@ import com.aisip.OnO.backend.practicenote.repository.PracticeNoteRepository; import com.aisip.OnO.backend.user.entity.User; import com.aisip.OnO.backend.user.repository.UserRepository; +import java.sql.Date; +import java.sql.Timestamp; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -194,4 +199,43 @@ public Page findAdminPracticeLogs(int page, int siz return new PageImpl<>(content, pageRequest, total); } + + @Transactional(readOnly = true) + public long countNotePracticeLogs() { + return missionLogRepository.countByMissionType(MissionType.NOTE_PRACTICE); + } + + @Transactional(readOnly = true) + public Map getDailyNotePracticeLogsCount(LocalDate startDate, LocalDate endDate) { + Map result = new LinkedHashMap<>(); + missionLogRepository.countDailyByMissionType( + MissionType.NOTE_PRACTICE, + startDate.atStartOfDay(), + endDate.atTime(LocalTime.MAX) + ) + .forEach(row -> result.put(toLocalDate(row[0]), (Long) row[1])); + + Map orderedResult = new LinkedHashMap<>(); + for (LocalDate date = endDate; !date.isBefore(startDate); date = date.minusDays(1)) { + orderedResult.put(date, result.getOrDefault(date, 0L)); + } + + return orderedResult; + } + + private LocalDate toLocalDate(Object value) { + if (value instanceof LocalDate localDate) { + return localDate; + } + if (value instanceof LocalDateTime localDateTime) { + return localDateTime.toLocalDate(); + } + if (value instanceof Date date) { + return date.toLocalDate(); + } + if (value instanceof Timestamp timestamp) { + return timestamp.toLocalDateTime().toLocalDate(); + } + return LocalDate.parse(value.toString()); + } } diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/repository/PracticeNoteRepository.java b/src/main/java/com/aisip/OnO/backend/practicenote/repository/PracticeNoteRepository.java index da8b8479..05732cc1 100644 --- a/src/main/java/com/aisip/OnO/backend/practicenote/repository/PracticeNoteRepository.java +++ b/src/main/java/com/aisip/OnO/backend/practicenote/repository/PracticeNoteRepository.java @@ -5,12 +5,26 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.time.LocalDateTime; import java.util.List; public interface PracticeNoteRepository extends JpaRepository, PracticeNoteRepositoryCustom { List findAllByUserId(Long userId); + long countByCreatedAtBetween(LocalDateTime startDateTime, LocalDateTime endDateTime); + + @Query(""" + SELECT FUNCTION('DATE', p.createdAt), COUNT(p) + FROM PracticeNote p + WHERE p.createdAt BETWEEN :startDateTime AND :endDateTime + GROUP BY FUNCTION('DATE', p.createdAt) + """) + List countDailyPracticeNotes( + @Param("startDateTime") LocalDateTime startDateTime, + @Param("endDateTime") LocalDateTime endDateTime + ); + @Query("SELECT p.id FROM PracticeNote p WHERE p.userId = :userId") List findAllPracticeIdsByUserId(@Param("userId") Long userId); } diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java b/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java index cb940ef4..ca3f5f6f 100644 --- a/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java +++ b/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java @@ -25,7 +25,14 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.sql.Date; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -251,4 +258,39 @@ public Page findAdminPracticeNotes(int page, int s return new PageImpl<>(content, pageRequest, practiceNotePage.getTotalElements()); } + + @Transactional(readOnly = true) + public long countAllPracticeNotes() { + return practiceNoteRepository.count(); + } + + @Transactional(readOnly = true) + public Map getDailyPracticeNotesCount(LocalDate startDate, LocalDate endDate) { + Map result = new LinkedHashMap<>(); + practiceNoteRepository.countDailyPracticeNotes(startDate.atStartOfDay(), endDate.atTime(LocalTime.MAX)) + .forEach(row -> result.put(toLocalDate(row[0]), (Long) row[1])); + + Map orderedResult = new LinkedHashMap<>(); + for (LocalDate date = endDate; !date.isBefore(startDate); date = date.minusDays(1)) { + orderedResult.put(date, result.getOrDefault(date, 0L)); + } + + return orderedResult; + } + + private LocalDate toLocalDate(Object value) { + if (value instanceof LocalDate localDate) { + return localDate; + } + if (value instanceof LocalDateTime localDateTime) { + return localDateTime.toLocalDate(); + } + if (value instanceof Date date) { + return date.toLocalDate(); + } + if (value instanceof Timestamp timestamp) { + return timestamp.toLocalDateTime().toLocalDate(); + } + return LocalDate.parse(value.toString()); + } } diff --git a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java index d7ed5ee9..267f03b2 100644 --- a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java +++ b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java @@ -126,6 +126,11 @@ public List findAllProblems() { .collect(Collectors.toList()); } + @Transactional(readOnly = true) + public long countAllProblems() { + return problemRepository.count(); + } + @Transactional(readOnly = true) public Long findProblemCountByUser(Long userId) { log.info("userId: {} find problem count", userId); diff --git a/src/main/java/com/aisip/OnO/backend/user/repository/UserRepository.java b/src/main/java/com/aisip/OnO/backend/user/repository/UserRepository.java index e3476947..f30042f1 100644 --- a/src/main/java/com/aisip/OnO/backend/user/repository/UserRepository.java +++ b/src/main/java/com/aisip/OnO/backend/user/repository/UserRepository.java @@ -4,7 +4,11 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; public interface UserRepository extends JpaRepository { @@ -15,4 +19,17 @@ public interface UserRepository extends JpaRepository { Optional findByName(String name); Page findAll(Pageable pageable); + + List findAllByCreatedAtBetweenOrderByCreatedAtDesc(LocalDateTime startDateTime, LocalDateTime endDateTime); + + @Query(""" + SELECT FUNCTION('DATE', u.createdAt), COUNT(u) + FROM User u + WHERE u.createdAt BETWEEN :startDateTime AND :endDateTime + GROUP BY FUNCTION('DATE', u.createdAt) + """) + List countDailyNewUsers( + @Param("startDateTime") LocalDateTime startDateTime, + @Param("endDateTime") LocalDateTime endDateTime + ); } diff --git a/src/main/java/com/aisip/OnO/backend/user/service/UserService.java b/src/main/java/com/aisip/OnO/backend/user/service/UserService.java index 3c940d0a..8915b7dc 100644 --- a/src/main/java/com/aisip/OnO/backend/user/service/UserService.java +++ b/src/main/java/com/aisip/OnO/backend/user/service/UserService.java @@ -17,6 +17,8 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.sql.Date; +import java.sql.Timestamp; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -103,6 +105,11 @@ public List findAllUsers() { .collect(Collectors.toList()); } + @Transactional(readOnly = true) + public long countAllUsers() { + return userRepository.count(); + } + @Transactional public void updateUser(Long userId, UserRegisterDto userRegisterDto) { @@ -163,26 +170,15 @@ public Map getDailyNewUsersCount(int days) { @Transactional(readOnly = true) public Map getDailyNewUsersCount(LocalDate startDate, LocalDate endDate) { Map result = new LinkedHashMap<>(); - List allUsers = userRepository.findAll(); + userRepository.countDailyNewUsers(startDate.atStartOfDay(), endDate.atTime(LocalTime.MAX)) + .forEach(row -> result.put(toLocalDate(row[0]), (Long) row[1])); - // 최근 날짜가 위로 오도록 역순으로 조회 + Map orderedResult = new LinkedHashMap<>(); for (LocalDate date = endDate; !date.isBefore(startDate); date = date.minusDays(1)) { - LocalDateTime startOfDay = date.atStartOfDay(); - LocalDateTime endOfDay = date.atTime(LocalTime.MAX); - - long count = allUsers.stream() - .filter(user -> { - LocalDateTime createdAt = user.getCreatedAt(); - return createdAt != null && - !createdAt.isBefore(startOfDay) && - !createdAt.isAfter(endOfDay); - }) - .count(); - - result.put(date, count); + orderedResult.put(date, result.getOrDefault(date, 0L)); } - return result; + return orderedResult; } @Transactional(readOnly = true) @@ -190,15 +186,24 @@ public List getUsersByDate(LocalDate date) { LocalDateTime startOfDay = date.atStartOfDay(); LocalDateTime endOfDay = date.atTime(LocalTime.MAX); - return userRepository.findAll().stream() - .filter(user -> { - LocalDateTime createdAt = user.getCreatedAt(); - return createdAt != null && - !createdAt.isBefore(startOfDay) && - !createdAt.isAfter(endOfDay); - }) - .sorted((u1, u2) -> u2.getCreatedAt().compareTo(u1.getCreatedAt())) + return userRepository.findAllByCreatedAtBetweenOrderByCreatedAtDesc(startOfDay, endOfDay).stream() .map(UserResponseDto::from) .collect(Collectors.toList()); } + + private LocalDate toLocalDate(Object value) { + if (value instanceof LocalDate localDate) { + return localDate; + } + if (value instanceof LocalDateTime localDateTime) { + return localDateTime.toLocalDate(); + } + if (value instanceof Date date) { + return date.toLocalDate(); + } + if (value instanceof Timestamp timestamp) { + return timestamp.toLocalDateTime().toLocalDate(); + } + return LocalDate.parse(value.toString()); + } } diff --git a/src/main/resources/templates/analysis.html b/src/main/resources/templates/analysis.html index 7b10391f..057d0eff 100644 --- a/src/main/resources/templates/analysis.html +++ b/src/main/resources/templates/analysis.html @@ -151,20 +151,37 @@

시스템 통계

+
+
+
+

총 복습노트 수

+

0

+
+
+ + + +
+
+ +
+
-

시스템 상태

-

정상

+

총 복습 기록 수

+

0

- +
-

모든 시스템 작동 중

+

복습노트 사용 미션 기준

@@ -175,7 +192,7 @@

시스템 통계

상세 통계

-
+

@@ -207,6 +224,34 @@

+ + +
+

+ + + + 복습 통계 +

+
+
+ 총 복습노트 수 + 0 +
+
+ 총 복습 기록 수 + 0 +
+
+ 선택 기간 복습노트 생성 + 0 +
+
+ 선택 기간 복습 수행 + 0 +
+
+
@@ -224,6 +269,8 @@

날짜별 출석 유저 수

날짜 출석 유저 수 신규 가입자 수 + 복습노트 생성 수 + 복습 수행 수 @@ -253,9 +300,19 @@

날짜별 출석 유저 수

+ + 0 + + + + 0 + + - + 데이터가 없습니다 From 865d2a8f9a67dc245b85cf06bae1064055108a6f Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 25 Apr 2026 23:05:22 +0900 Subject: [PATCH 06/43] =?UTF-8?q?[feat]=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=B8=94=EB=A1=9D=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/templates/practice-notes.html | 38 ++++++++++-- src/main/resources/templates/problems.html | 61 +++++++++++-------- src/main/resources/templates/users.html | 36 ++++++++--- 3 files changed, 96 insertions(+), 39 deletions(-) diff --git a/src/main/resources/templates/practice-notes.html b/src/main/resources/templates/practice-notes.html index 2a3bdf3a..d5df7768 100644 --- a/src/main/resources/templates/practice-notes.html +++ b/src/main/resources/templates/practice-notes.html @@ -101,13 +101,26 @@

복습노트 목록

-
+
이전 이전 - - 1 / 1 + + << + << + + + 1 + 1 + + >> + >> + 다음 다음 @@ -150,13 +163,26 @@

복습 기록

-
+
이전 이전 - - 1 / 1 + + << + << + + + 1 + 1 + + >> + >> + 다음 다음 diff --git a/src/main/resources/templates/problems.html b/src/main/resources/templates/problems.html index 716e7148..6c0acd1f 100644 --- a/src/main/resources/templates/problems.html +++ b/src/main/resources/templates/problems.html @@ -60,7 +60,6 @@

문제 관리

폴더 ID 메모 출처 - 이미지 분석 푼 날짜 작성일 @@ -80,37 +79,27 @@

문제 관리

- -
- - - 문제 이미지 - - - 이미지 없음 -
- - - 분석 완료 - 분석중 - 분석 대기 - 분석 실패 - + 이미지 없음 + + 미등록 @@ -122,7 +111,7 @@

문제 관리

2024-01-01 00:00 - + 문제가 없습니다 @@ -134,12 +123,12 @@

문제 관리

- 1 - 부터 20 + 1 + 부터 20 까지 (총 100개)
-
+
문제 관리 이전 + + << + + + << + + - - 0 + 0 2024-01-01 00:00 @@ -136,12 +136,12 @@

유저 관리

- 1 - 부터 20 + 1 + 부터 20 까지 (총 100개)
-
+
유저 관리 이전 + + + << + + + << + + -
- + + + + >> + + + >> + + Date: Sat, 25 Apr 2026 23:05:49 +0900 Subject: [PATCH 07/43] =?UTF-8?q?[fix]=20=ED=86=B5=EA=B3=84=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=EB=B3=84=20=EC=A1=B0=ED=9A=8C=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminAnalysisController.java | 4 ++ .../repository/MissionLogRepositoryImpl.java | 20 +++++++- src/main/resources/templates/analysis.html | 50 ++++++++++++------- 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminAnalysisController.java b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminAnalysisController.java index 17ca66a9..402885d3 100644 --- a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminAnalysisController.java +++ b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminAnalysisController.java @@ -91,6 +91,10 @@ public String getAllAnalysis( model.addAttribute("averageDailyVisitors", averageDailyVisitors); model.addAttribute("startDate", selectedStartDate); model.addAttribute("endDate", selectedEndDate); + model.addAttribute("quickStart7Days", today.minusDays(6)); + model.addAttribute("quickStart30Days", today.minusDays(29)); + model.addAttribute("quickStart90Days", today.minusDays(89)); + model.addAttribute("today", today); return "analysis"; } diff --git a/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryImpl.java b/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryImpl.java index 903b86e4..f48c0450 100644 --- a/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryImpl.java +++ b/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepositoryImpl.java @@ -9,6 +9,8 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; +import java.sql.Date; +import java.sql.Timestamp; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -116,7 +118,7 @@ public Map getDailyActiveUsersCount(LocalDate startDate, LocalD Map countByDate = new LinkedHashMap<>(); for (Tuple row : dailyCounts) { - LocalDate date = row.get(createdDate); + LocalDate date = toLocalDate(row.get(createdDate)); Long count = row.get(activeUserCount); if (date != null) { countByDate.put(date, count != null ? count : 0L); @@ -152,4 +154,20 @@ private LocalDateTime getStartOfToday() { private LocalDateTime getEndOfToday() { return LocalDate.now().atTime(LocalTime.MAX); } + + private LocalDate toLocalDate(Object value) { + if (value instanceof LocalDate localDate) { + return localDate; + } + if (value instanceof LocalDateTime localDateTime) { + return localDateTime.toLocalDate(); + } + if (value instanceof Date date) { + return date.toLocalDate(); + } + if (value instanceof Timestamp timestamp) { + return timestamp.toLocalDateTime().toLocalDate(); + } + return value != null ? LocalDate.parse(value.toString()) : null; + } } diff --git a/src/main/resources/templates/analysis.html b/src/main/resources/templates/analysis.html index 057d0eff..7b0043a1 100644 --- a/src/main/resources/templates/analysis.html +++ b/src/main/resources/templates/analysis.html @@ -50,29 +50,45 @@

시스템 통계

OnO 애플리케이션 통계 개요

-
-
+ + +
+
+ +
+ +
- - + +
+ +
-
-

- 현재 기간: - 2024-01-01 - 부터 - 2024-01-30 - 까지 -

From 41226c65764aefe508ee51946312e4f5795989b9 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 25 Apr 2026 23:06:05 +0900 Subject: [PATCH 08/43] =?UTF-8?q?[feat]=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=95=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminPracticeNoteController.java | 18 ++++- .../controller/AdminProblemController.java | 35 +++++---- .../admin/controller/AdminUserController.java | 73 +++++-------------- .../admin/dto/AdminProblemResponseDto.java | 14 ++++ .../admin/dto/AdminUserResponseDto.java | 27 +++++++ .../repository/MissionLogRepository.java | 3 + .../mission/service/MissionLogService.java | 21 ++++-- .../repository/PracticeNoteRepository.java | 8 ++ .../service/PracticeNoteService.java | 25 ++++++- .../repository/ProblemRepositoryCustom.java | 5 ++ .../repository/ProblemRepositoryImpl.java | 35 +++++++++ .../problem/service/ProblemService.java | 8 ++ .../backend/user/repository/UserAdminRow.java | 9 +++ .../user/repository/UserRepository.java | 2 +- .../user/repository/UserRepositoryCustom.java | 8 ++ .../user/repository/UserRepositoryImpl.java | 65 +++++++++++++++++ .../OnO/backend/user/service/UserService.java | 16 +++- 17 files changed, 286 insertions(+), 86 deletions(-) create mode 100644 src/main/java/com/aisip/OnO/backend/admin/dto/AdminProblemResponseDto.java create mode 100644 src/main/java/com/aisip/OnO/backend/admin/dto/AdminUserResponseDto.java create mode 100644 src/main/java/com/aisip/OnO/backend/user/repository/UserAdminRow.java create mode 100644 src/main/java/com/aisip/OnO/backend/user/repository/UserRepositoryCustom.java create mode 100644 src/main/java/com/aisip/OnO/backend/user/repository/UserRepositoryImpl.java diff --git a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminPracticeNoteController.java b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminPracticeNoteController.java index 750deaee..788203c6 100644 --- a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminPracticeNoteController.java +++ b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminPracticeNoteController.java @@ -35,13 +35,27 @@ public String getPracticeNotes( Page practiceNotes = practiceNoteService.findAdminPracticeNotes(selectedNotePage, selectedSize); Page practiceLogs = missionLogService.findAdminPracticeLogs(selectedLogPage, selectedSize); + int noteTotalPages = practiceNotes.getTotalPages(); + int logTotalPages = practiceLogs.getTotalPages(); + int notePageBlockStart = (selectedNotePage / 10) * 10; + int logPageBlockStart = (selectedLogPage / 10) * 10; + int notePageBlockEnd = Math.min(notePageBlockStart + 9, Math.max(noteTotalPages - 1, 0)); + int logPageBlockEnd = Math.min(logPageBlockStart + 9, Math.max(logTotalPages - 1, 0)); model.addAttribute("practiceNotes", practiceNotes.getContent()); model.addAttribute("practiceLogs", practiceLogs.getContent()); model.addAttribute("notePage", selectedNotePage); model.addAttribute("logPage", selectedLogPage); - model.addAttribute("noteTotalPages", practiceNotes.getTotalPages()); - model.addAttribute("logTotalPages", practiceLogs.getTotalPages()); + model.addAttribute("noteTotalPages", noteTotalPages); + model.addAttribute("logTotalPages", logTotalPages); + model.addAttribute("notePageBlockStart", notePageBlockStart); + model.addAttribute("notePageBlockEnd", notePageBlockEnd); + model.addAttribute("logPageBlockStart", logPageBlockStart); + model.addAttribute("logPageBlockEnd", logPageBlockEnd); + model.addAttribute("hasPreviousNoteBlock", notePageBlockStart > 0); + model.addAttribute("hasNextNoteBlock", notePageBlockEnd < noteTotalPages - 1); + model.addAttribute("hasPreviousLogBlock", logPageBlockStart > 0); + model.addAttribute("hasNextLogBlock", logPageBlockEnd < logTotalPages - 1); model.addAttribute("totalPracticeNotes", practiceNotes.getTotalElements()); model.addAttribute("totalPracticeLogs", practiceLogs.getTotalElements()); model.addAttribute("size", selectedSize); diff --git a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminProblemController.java b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminProblemController.java index f2d60dc3..fe711425 100644 --- a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminProblemController.java +++ b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminProblemController.java @@ -1,5 +1,6 @@ package com.aisip.OnO.backend.admin.controller; +import com.aisip.OnO.backend.admin.dto.AdminProblemResponseDto; import com.aisip.OnO.backend.folder.dto.FolderResponseDto; import com.aisip.OnO.backend.folder.entity.Folder; import com.aisip.OnO.backend.folder.service.FolderService; @@ -9,6 +10,7 @@ import com.aisip.OnO.backend.user.service.UserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -16,8 +18,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; -import java.util.List; - @Slf4j @RequiredArgsConstructor @Controller @@ -34,21 +34,24 @@ public String getAllProblems( @RequestParam(defaultValue = "20", name = "size") int size, Model model ) { - List allProblems = problemService.findAllProblems(); - - // 페이징 계산 - int totalProblems = allProblems.size(); - int totalPages = (int) Math.ceil((double) totalProblems / size); - int startIndex = page * size; - int endIndex = Math.min(startIndex + size, totalProblems); - - List pagedProblems = allProblems.subList(startIndex, endIndex); + int selectedPage = Math.max(page, 0); + int selectedSize = Math.max(size, 1); + Page problemPage = problemService.findAdminProblems(selectedPage, selectedSize); + int totalPages = problemPage.getTotalPages(); + int pageBlockStart = (selectedPage / 10) * 10; + int pageBlockEnd = Math.min(pageBlockStart + 9, Math.max(totalPages - 1, 0)); - model.addAttribute("problems", pagedProblems); - model.addAttribute("currentPage", page); + model.addAttribute("problems", problemPage.getContent()); + model.addAttribute("currentPage", selectedPage); model.addAttribute("totalPages", totalPages); - model.addAttribute("totalProblems", totalProblems); - model.addAttribute("size", size); + model.addAttribute("totalProblems", problemPage.getTotalElements()); + model.addAttribute("size", selectedSize); + model.addAttribute("pageStartItem", problemPage.isEmpty() ? 0 : selectedPage * selectedSize + 1); + model.addAttribute("pageEndItem", selectedPage * selectedSize + problemPage.getNumberOfElements()); + model.addAttribute("pageBlockStart", pageBlockStart); + model.addAttribute("pageBlockEnd", pageBlockEnd); + model.addAttribute("hasPreviousBlock", pageBlockStart > 0); + model.addAttribute("hasNextBlock", pageBlockEnd < totalPages - 1); return "problems"; } @@ -66,4 +69,4 @@ public String getProblemDetail(@PathVariable(name = "problemId") Long problemId, return "problem"; } -} \ No newline at end of file +} diff --git a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminUserController.java b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminUserController.java index 17def2a1..b2eebcb0 100644 --- a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminUserController.java +++ b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminUserController.java @@ -1,5 +1,6 @@ package com.aisip.OnO.backend.admin.controller; +import com.aisip.OnO.backend.admin.dto.AdminUserResponseDto; import com.aisip.OnO.backend.folder.dto.FolderResponseDto; import com.aisip.OnO.backend.folder.service.FolderService; import com.aisip.OnO.backend.mission.entity.MissionLog; @@ -14,15 +15,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; -import java.util.Comparator; import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; @Slf4j @RequiredArgsConstructor @@ -44,63 +42,30 @@ public String getAllUsers( @RequestParam(defaultValue = "desc", name = "direction") String direction, Model model ) { - List allUsers = userService.findAllUsers(); - Map problemCountByUserId = allUsers.stream() - .collect(Collectors.toMap( - UserResponseDto::userId, - user -> problemService.findProblemCountByUser(user.userId()) - )); - - Comparator comparator = getUserComparator(sortBy, problemCountByUserId); - if ("desc".equalsIgnoreCase(direction)) { - comparator = comparator.reversed(); - } - - List sortedUsers = allUsers.stream() - .sorted(comparator.thenComparing(UserResponseDto::userId, Comparator.reverseOrder())) - .toList(); - - // 페이징 계산 - int totalUsers = sortedUsers.size(); - int totalPages = (int) Math.ceil((double) totalUsers / size); - int startIndex = page * size; - int endIndex = Math.min(startIndex + size, totalUsers); - - List pagedUsers = startIndex >= totalUsers - ? List.of() - : sortedUsers.subList(startIndex, endIndex); - - // 각 유저의 문제 개수 계산 - List problemCounts = pagedUsers.stream() - .map(user -> problemCountByUserId.get(user.userId())) - .toList(); - - model.addAttribute("users", pagedUsers); - model.addAttribute("problemCounts", problemCounts); - model.addAttribute("currentPage", page); + int selectedPage = Math.max(page, 0); + int selectedSize = Math.max(size, 1); + Page userPage = userService.findAdminUsers(selectedPage, selectedSize, sortBy, direction); + int totalPages = userPage.getTotalPages(); + int pageBlockStart = (selectedPage / 10) * 10; + int pageBlockEnd = Math.min(pageBlockStart + 9, Math.max(totalPages - 1, 0)); + + model.addAttribute("users", userPage.getContent()); + model.addAttribute("currentPage", selectedPage); model.addAttribute("totalPages", totalPages); - model.addAttribute("totalUsers", totalUsers); - model.addAttribute("size", size); + model.addAttribute("totalUsers", userPage.getTotalElements()); + model.addAttribute("size", selectedSize); + model.addAttribute("pageStartItem", userPage.isEmpty() ? 0 : selectedPage * selectedSize + 1); + model.addAttribute("pageEndItem", selectedPage * selectedSize + userPage.getNumberOfElements()); model.addAttribute("sortBy", sortBy); model.addAttribute("direction", direction); + model.addAttribute("pageBlockStart", pageBlockStart); + model.addAttribute("pageBlockEnd", pageBlockEnd); + model.addAttribute("hasPreviousBlock", pageBlockStart > 0); + model.addAttribute("hasNextBlock", pageBlockEnd < totalPages - 1); return "users"; } - private Comparator getUserComparator(String sortBy, Map problemCountByUserId) { - Function sortKey = switch (sortBy) { - case "level" -> UserResponseDto::totalStudyLevel; - case "problemCount" -> user -> problemCountByUserId.getOrDefault(user.userId(), 0L); - default -> null; - }; - - if (sortKey == null) { - return Comparator.comparing(UserResponseDto::createdAt); - } - - return Comparator.comparing(sortKey); - } - @GetMapping("/user/{userId}") public String getUserDetailsById(@PathVariable(name = "userId") Long userId, Model model) { UserResponseDto user = userService.findUser(userId); diff --git a/src/main/java/com/aisip/OnO/backend/admin/dto/AdminProblemResponseDto.java b/src/main/java/com/aisip/OnO/backend/admin/dto/AdminProblemResponseDto.java new file mode 100644 index 00000000..d29d7459 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/admin/dto/AdminProblemResponseDto.java @@ -0,0 +1,14 @@ +package com.aisip.OnO.backend.admin.dto; + +import java.time.LocalDateTime; + +public record AdminProblemResponseDto( + Long problemId, + Long folderId, + String memo, + String reference, + String analysisStatus, + LocalDateTime solvedAt, + LocalDateTime createdAt +) { +} diff --git a/src/main/java/com/aisip/OnO/backend/admin/dto/AdminUserResponseDto.java b/src/main/java/com/aisip/OnO/backend/admin/dto/AdminUserResponseDto.java new file mode 100644 index 00000000..41a92f85 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/admin/dto/AdminUserResponseDto.java @@ -0,0 +1,27 @@ +package com.aisip.OnO.backend.admin.dto; + +import com.aisip.OnO.backend.user.entity.User; + +import java.time.LocalDateTime; + +public record AdminUserResponseDto( + Long userId, + String name, + String email, + Long totalStudyLevel, + Long totalStudyCurrentPoint, + Long problemCount, + LocalDateTime createdAt +) { + public static AdminUserResponseDto from(User user, Long problemCount) { + return new AdminUserResponseDto( + user.getId(), + user.getName(), + user.getEmail(), + user.getUserMissionStatus().getTotalStudyLevel(), + user.getUserMissionStatus().getTotalStudyPoint(), + problemCount != null ? problemCount : 0L, + user.getCreatedAt() + ); + } +} diff --git a/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepository.java b/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepository.java index 05c0866e..e9318f54 100644 --- a/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepository.java +++ b/src/main/java/com/aisip/OnO/backend/mission/repository/MissionLogRepository.java @@ -14,6 +14,9 @@ public interface MissionLogRepository extends JpaRepository, M List findAllByMissionType(MissionType missionType, Pageable pageable); + @Query("SELECT m FROM MissionLog m JOIN FETCH m.user WHERE m.missionType = :missionType") + List findAllByMissionTypeWithUser(@Param("missionType") MissionType missionType, Pageable pageable); + long countByMissionType(MissionType missionType); long countByMissionTypeAndCreatedAtBetween( diff --git a/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java b/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java index c8429880..0b170042 100644 --- a/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java +++ b/src/main/java/com/aisip/OnO/backend/mission/service/MissionLogService.java @@ -19,6 +19,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.springframework.data.domain.Page; @@ -185,16 +187,21 @@ public List getActiveUsersByDate(LocalDa @Transactional(readOnly = true) public Page findAdminPracticeLogs(int page, int size) { PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); - List missionLogs = missionLogRepository.findAllByMissionType(MissionType.NOTE_PRACTICE, pageRequest); + List missionLogs = missionLogRepository.findAllByMissionTypeWithUser(MissionType.NOTE_PRACTICE, pageRequest); long total = missionLogRepository.countByMissionType(MissionType.NOTE_PRACTICE); + List practiceNoteIds = missionLogs.stream() + .map(MissionLog::getReferenceId) + .filter(java.util.Objects::nonNull) + .distinct() + .toList(); + Map practiceNotesById = practiceNoteRepository.findAllById(practiceNoteIds).stream() + .collect(Collectors.toMap(PracticeNote::getId, Function.identity())); List content = missionLogs.stream() - .map(missionLog -> { - PracticeNote practiceNote = missionLog.getReferenceId() != null - ? practiceNoteRepository.findById(missionLog.getReferenceId()).orElse(null) - : null; - return AdminPracticeLogResponseDto.from(missionLog, practiceNote); - }) + .map(missionLog -> AdminPracticeLogResponseDto.from( + missionLog, + practiceNotesById.get(missionLog.getReferenceId()) + )) .toList(); return new PageImpl<>(content, pageRequest, total); diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/repository/PracticeNoteRepository.java b/src/main/java/com/aisip/OnO/backend/practicenote/repository/PracticeNoteRepository.java index 05732cc1..1ee22960 100644 --- a/src/main/java/com/aisip/OnO/backend/practicenote/repository/PracticeNoteRepository.java +++ b/src/main/java/com/aisip/OnO/backend/practicenote/repository/PracticeNoteRepository.java @@ -25,6 +25,14 @@ List countDailyPracticeNotes( @Param("endDateTime") LocalDateTime endDateTime ); + @Query(""" + SELECT m.practiceNote.id, COUNT(m.problem.id) + FROM ProblemPracticeNoteMapping m + WHERE m.practiceNote.id IN :practiceNoteIds + GROUP BY m.practiceNote.id + """) + List countProblemsByPracticeNoteIds(@Param("practiceNoteIds") List practiceNoteIds); + @Query("SELECT p.id FROM PracticeNote p WHERE p.userId = :userId") List findAllPracticeIdsByUserId(@Param("userId") Long userId); } diff --git a/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java b/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java index ca3f5f6f..62683f5c 100644 --- a/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java +++ b/src/main/java/com/aisip/OnO/backend/practicenote/service/PracticeNoteService.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Function; import java.util.stream.Collectors; @Slf4j @@ -247,11 +248,29 @@ public CursorPageResponse findPracticeThumbnai public Page findAdminPracticeNotes(int page, int size) { PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); Page practiceNotePage = practiceNoteRepository.findAll(pageRequest); + List practiceNotes = practiceNotePage.getContent(); + List userIds = practiceNotes.stream() + .map(PracticeNote::getUserId) + .distinct() + .toList(); + List practiceNoteIds = practiceNotes.stream() + .map(PracticeNote::getId) + .toList(); - List content = practiceNotePage.getContent().stream() + Map usersById = userRepository.findAllById(userIds).stream() + .collect(Collectors.toMap(User::getId, Function.identity())); + Map problemCountsByPracticeNoteId = practiceNoteIds.isEmpty() + ? Map.of() + : practiceNoteRepository.countProblemsByPracticeNoteIds(practiceNoteIds).stream() + .collect(Collectors.toMap( + row -> (Long) row[0], + row -> (Long) row[1] + )); + + List content = practiceNotes.stream() .map(practiceNote -> { - User user = userRepository.findById(practiceNote.getUserId()).orElse(null); - Long problemCount = (long) practiceNoteRepository.findProblemIdListByPracticeNoteId(practiceNote.getId()).size(); + User user = usersById.get(practiceNote.getUserId()); + Long problemCount = problemCountsByPracticeNoteId.getOrDefault(practiceNote.getId(), 0L); return AdminPracticeNoteResponseDto.from(practiceNote, user, problemCount); }) .toList(); diff --git a/src/main/java/com/aisip/OnO/backend/problem/repository/ProblemRepositoryCustom.java b/src/main/java/com/aisip/OnO/backend/problem/repository/ProblemRepositoryCustom.java index 661b383f..1dde7b32 100644 --- a/src/main/java/com/aisip/OnO/backend/problem/repository/ProblemRepositoryCustom.java +++ b/src/main/java/com/aisip/OnO/backend/problem/repository/ProblemRepositoryCustom.java @@ -1,6 +1,9 @@ package com.aisip.OnO.backend.problem.repository; +import com.aisip.OnO.backend.admin.dto.AdminProblemResponseDto; import com.aisip.OnO.backend.problem.entity.Problem; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import java.util.List; import java.util.Optional; @@ -15,6 +18,8 @@ public interface ProblemRepositoryCustom { List findAll(); + Page findAdminProblems(Pageable pageable); + List findAllProblemsByPracticeId(Long practiceId); /** diff --git a/src/main/java/com/aisip/OnO/backend/problem/repository/ProblemRepositoryImpl.java b/src/main/java/com/aisip/OnO/backend/problem/repository/ProblemRepositoryImpl.java index d814244a..3ea48ae8 100644 --- a/src/main/java/com/aisip/OnO/backend/problem/repository/ProblemRepositoryImpl.java +++ b/src/main/java/com/aisip/OnO/backend/problem/repository/ProblemRepositoryImpl.java @@ -1,9 +1,14 @@ package com.aisip.OnO.backend.problem.repository; +import com.aisip.OnO.backend.admin.dto.AdminProblemResponseDto; import com.aisip.OnO.backend.problem.entity.Problem; import com.aisip.OnO.backend.problem.entity.QProblem; +import com.querydsl.core.types.Projections; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import java.util.List; import java.util.Optional; @@ -11,6 +16,7 @@ import static com.aisip.OnO.backend.practicenote.entity.QPracticeNote.practiceNote; import static com.aisip.OnO.backend.practicenote.entity.QProblemPracticeNoteMapping.problemPracticeNoteMapping; import static com.aisip.OnO.backend.problem.entity.QProblem.problem; +import static com.aisip.OnO.backend.problem.entity.QProblemAnalysis.problemAnalysis; import static com.aisip.OnO.backend.problem.entity.QProblemImageData.problemImageData; import static com.aisip.OnO.backend.tag.entity.QProblemTagMapping.problemTagMapping; @@ -66,6 +72,35 @@ public List findAll() { .fetch(); } + @Override + public Page findAdminProblems(Pageable pageable) { + List content = queryFactory + .select(Projections.constructor( + AdminProblemResponseDto.class, + problem.id, + problem.folder.id, + problem.memo, + problem.reference, + problemAnalysis.status.stringValue(), + problem.solvedAt, + problem.createdAt + )) + .from(problem) + .leftJoin(problem.folder) + .leftJoin(problem.problemAnalysis, problemAnalysis) + .orderBy(problem.createdAt.desc(), problem.id.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = queryFactory + .select(problem.count()) + .from(problem) + .fetchOne(); + + return new PageImpl<>(content, pageable, total != null ? total : 0L); + } + @Override public List findAllProblemsByPracticeId(Long practiceId) { return queryFactory diff --git a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java index 267f03b2..179d3a7d 100644 --- a/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java +++ b/src/main/java/com/aisip/OnO/backend/problem/service/ProblemService.java @@ -1,5 +1,6 @@ package com.aisip.OnO.backend.problem.service; +import com.aisip.OnO.backend.admin.dto.AdminProblemResponseDto; import com.aisip.OnO.backend.common.exception.ApplicationException; import com.aisip.OnO.backend.common.response.CursorPageResponse; import com.aisip.OnO.backend.config.rabbitmq.producer.S3DeleteProducer; @@ -32,6 +33,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import java.util.ArrayList; import java.util.Collection; @@ -131,6 +134,11 @@ public long countAllProblems() { return problemRepository.count(); } + @Transactional(readOnly = true) + public Page findAdminProblems(int page, int size) { + return problemRepository.findAdminProblems(PageRequest.of(page, size)); + } + @Transactional(readOnly = true) public Long findProblemCountByUser(Long userId) { log.info("userId: {} find problem count", userId); diff --git a/src/main/java/com/aisip/OnO/backend/user/repository/UserAdminRow.java b/src/main/java/com/aisip/OnO/backend/user/repository/UserAdminRow.java new file mode 100644 index 00000000..52ff0e42 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/user/repository/UserAdminRow.java @@ -0,0 +1,9 @@ +package com.aisip.OnO.backend.user.repository; + +import com.aisip.OnO.backend.user.entity.User; + +public record UserAdminRow( + User user, + Long problemCount +) { +} diff --git a/src/main/java/com/aisip/OnO/backend/user/repository/UserRepository.java b/src/main/java/com/aisip/OnO/backend/user/repository/UserRepository.java index f30042f1..9401fdbc 100644 --- a/src/main/java/com/aisip/OnO/backend/user/repository/UserRepository.java +++ b/src/main/java/com/aisip/OnO/backend/user/repository/UserRepository.java @@ -11,7 +11,7 @@ import java.util.List; import java.util.Optional; -public interface UserRepository extends JpaRepository { +public interface UserRepository extends JpaRepository, UserRepositoryCustom { Optional findByEmail(String email); Optional findByIdentifier(String identifier); diff --git a/src/main/java/com/aisip/OnO/backend/user/repository/UserRepositoryCustom.java b/src/main/java/com/aisip/OnO/backend/user/repository/UserRepositoryCustom.java new file mode 100644 index 00000000..01598b84 --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/user/repository/UserRepositoryCustom.java @@ -0,0 +1,8 @@ +package com.aisip.OnO.backend.user.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface UserRepositoryCustom { + Page findAdminUsers(Pageable pageable, String sortBy, String direction); +} diff --git a/src/main/java/com/aisip/OnO/backend/user/repository/UserRepositoryImpl.java b/src/main/java/com/aisip/OnO/backend/user/repository/UserRepositoryImpl.java new file mode 100644 index 00000000..dc2cef3a --- /dev/null +++ b/src/main/java/com/aisip/OnO/backend/user/repository/UserRepositoryImpl.java @@ -0,0 +1,65 @@ +package com.aisip.OnO.backend.user.repository; + +import com.querydsl.core.Tuple; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +import static com.aisip.OnO.backend.problem.entity.QProblem.problem; +import static com.aisip.OnO.backend.user.entity.QUser.user; + +public class UserRepositoryImpl implements UserRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + public UserRepositoryImpl(EntityManager entityManager) { + this.queryFactory = new JPAQueryFactory(entityManager); + } + + @Override + public Page findAdminUsers(Pageable pageable, String sortBy, String direction) { + NumberExpression problemCount = problem.id.count(); + + List rows = queryFactory + .select(user, problemCount) + .from(user) + .leftJoin(problem).on(problem.userId.eq(user.id)) + .groupBy(user.id) + .orderBy(getOrderSpecifier(sortBy, direction, problemCount), user.id.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + List content = rows.stream() + .map(row -> new UserAdminRow(row.get(user), row.get(problemCount))) + .toList(); + + Long total = queryFactory + .select(user.count()) + .from(user) + .fetchOne(); + + return new PageImpl<>(content, pageable, total != null ? total : 0L); + } + + private OrderSpecifier getOrderSpecifier( + String sortBy, + String direction, + NumberExpression problemCount + ) { + Order order = "asc".equalsIgnoreCase(direction) ? Order.ASC : Order.DESC; + + return switch (sortBy) { + case "level" -> new OrderSpecifier<>(order, user.userMissionStatus.totalStudyLevel); + case "problemCount" -> new OrderSpecifier<>(order, problemCount); + default -> new OrderSpecifier<>(order, user.createdAt); + }; + } +} diff --git a/src/main/java/com/aisip/OnO/backend/user/service/UserService.java b/src/main/java/com/aisip/OnO/backend/user/service/UserService.java index 8915b7dc..fce6363d 100644 --- a/src/main/java/com/aisip/OnO/backend/user/service/UserService.java +++ b/src/main/java/com/aisip/OnO/backend/user/service/UserService.java @@ -1,5 +1,6 @@ package com.aisip.OnO.backend.user.service; +import com.aisip.OnO.backend.admin.dto.AdminUserResponseDto; import com.aisip.OnO.backend.folder.service.FolderService; import com.aisip.OnO.backend.practicenote.service.PracticeNoteService; import com.aisip.OnO.backend.problem.service.ProblemService; @@ -12,19 +13,21 @@ import com.aisip.OnO.backend.util.webhook.DiscordWebhookNotificationService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.sql.Date; +import java.sql.Timestamp; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; -import java.sql.Date; -import java.sql.Timestamp; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; -import org.springframework.transaction.annotation.Transactional; @Slf4j @Service @@ -105,6 +108,13 @@ public List findAllUsers() { .collect(Collectors.toList()); } + @Transactional(readOnly = true) + public Page findAdminUsers(int page, int size, String sortBy, String direction) { + PageRequest pageRequest = PageRequest.of(page, size); + return userRepository.findAdminUsers(pageRequest, sortBy, direction) + .map(row -> AdminUserResponseDto.from(row.user(), row.problemCount())); + } + @Transactional(readOnly = true) public long countAllUsers() { return userRepository.count(); From 6df9c9f4dc7f8acee613899d080e33540a42cae5 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Sat, 25 Apr 2026 23:16:09 +0900 Subject: [PATCH 09/43] =?UTF-8?q?[feat]=20=EB=B3=B5=EC=8A=B5=20=EB=85=B8?= =?UTF-8?q?=ED=8A=B8,=20=EB=B3=B5=EC=8A=B5=20=EA=B8=B0=EB=A1=9D=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminPracticeNoteController.java | 39 ++++-- src/main/resources/templates/admin.html | 8 +- .../resources/templates/practice-logs.html | 119 ++++++++++++++++++ .../resources/templates/practice-notes.html | 83 ++---------- 4 files changed, 159 insertions(+), 90 deletions(-) create mode 100644 src/main/resources/templates/practice-logs.html diff --git a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminPracticeNoteController.java b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminPracticeNoteController.java index 788203c6..87fbed99 100644 --- a/src/main/java/com/aisip/OnO/backend/admin/controller/AdminPracticeNoteController.java +++ b/src/main/java/com/aisip/OnO/backend/admin/controller/AdminPracticeNoteController.java @@ -25,41 +25,54 @@ public class AdminPracticeNoteController { @GetMapping("/practice-notes") public String getPracticeNotes( @RequestParam(defaultValue = "0", name = "notePage") int notePage, - @RequestParam(defaultValue = "0", name = "logPage") int logPage, @RequestParam(defaultValue = "20", name = "size") int size, Model model ) { int selectedNotePage = Math.max(notePage, 0); - int selectedLogPage = Math.max(logPage, 0); int selectedSize = Math.max(size, 1); Page practiceNotes = practiceNoteService.findAdminPracticeNotes(selectedNotePage, selectedSize); - Page practiceLogs = missionLogService.findAdminPracticeLogs(selectedLogPage, selectedSize); int noteTotalPages = practiceNotes.getTotalPages(); - int logTotalPages = practiceLogs.getTotalPages(); int notePageBlockStart = (selectedNotePage / 10) * 10; - int logPageBlockStart = (selectedLogPage / 10) * 10; int notePageBlockEnd = Math.min(notePageBlockStart + 9, Math.max(noteTotalPages - 1, 0)); - int logPageBlockEnd = Math.min(logPageBlockStart + 9, Math.max(logTotalPages - 1, 0)); model.addAttribute("practiceNotes", practiceNotes.getContent()); - model.addAttribute("practiceLogs", practiceLogs.getContent()); model.addAttribute("notePage", selectedNotePage); - model.addAttribute("logPage", selectedLogPage); model.addAttribute("noteTotalPages", noteTotalPages); - model.addAttribute("logTotalPages", logTotalPages); model.addAttribute("notePageBlockStart", notePageBlockStart); model.addAttribute("notePageBlockEnd", notePageBlockEnd); - model.addAttribute("logPageBlockStart", logPageBlockStart); - model.addAttribute("logPageBlockEnd", logPageBlockEnd); model.addAttribute("hasPreviousNoteBlock", notePageBlockStart > 0); model.addAttribute("hasNextNoteBlock", notePageBlockEnd < noteTotalPages - 1); + model.addAttribute("totalPracticeNotes", practiceNotes.getTotalElements()); + model.addAttribute("size", selectedSize); + + return "practice-notes"; + } + + @GetMapping("/practice-logs") + public String getPracticeLogs( + @RequestParam(defaultValue = "0", name = "logPage") int logPage, + @RequestParam(defaultValue = "20", name = "size") int size, + Model model + ) { + int selectedLogPage = Math.max(logPage, 0); + int selectedSize = Math.max(size, 1); + + Page practiceLogs = missionLogService.findAdminPracticeLogs(selectedLogPage, selectedSize); + int logTotalPages = practiceLogs.getTotalPages(); + int logPageBlockStart = (selectedLogPage / 10) * 10; + int logPageBlockEnd = Math.min(logPageBlockStart + 9, Math.max(logTotalPages - 1, 0)); + + model.addAttribute("practiceLogs", practiceLogs.getContent()); + model.addAttribute("logPage", selectedLogPage); + model.addAttribute("logTotalPages", logTotalPages); + model.addAttribute("logPageBlockStart", logPageBlockStart); + model.addAttribute("logPageBlockEnd", logPageBlockEnd); model.addAttribute("hasPreviousLogBlock", logPageBlockStart > 0); model.addAttribute("hasNextLogBlock", logPageBlockEnd < logTotalPages - 1); - model.addAttribute("totalPracticeNotes", practiceNotes.getTotalElements()); model.addAttribute("totalPracticeLogs", practiceLogs.getTotalElements()); model.addAttribute("size", selectedSize); - return "practice-notes"; + return "practice-logs"; } } diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html index ee8307c6..d9381100 100644 --- a/src/main/resources/templates/admin.html +++ b/src/main/resources/templates/admin.html @@ -90,8 +90,8 @@

문제 관리

-

복습 관리

-

복습노트 및 복습 기록 조회

+

복습노트 관리

+

복습노트 목록 조회

@@ -124,8 +124,8 @@

빠른 이동

최신 문제 목록 보기

- -

복습 목록 보기

+
+

복습 기록 보기

통계 보기

diff --git a/src/main/resources/templates/practice-logs.html b/src/main/resources/templates/practice-logs.html new file mode 100644 index 00000000..17b049cd --- /dev/null +++ b/src/main/resources/templates/practice-logs.html @@ -0,0 +1,119 @@ + + + + + + OnO 관리자 - 복습 기록 + + + +
+ +
+
+
+

복습 기록

+

+ 총 0개 +

+
+ + 복습노트 목록 + +
+ +
+
+

복습 기록 목록

+
+ + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
기록 ID유저복습노트포인트완료일
1 + 유저 +

user@example.com

+
+ 복습노트 + #1 + 15pt2024-01-01 00:00
복습 기록이 없습니다
+
+ +
+ 이전 + 이전 + + << + << + + + 1 + 1 + + + >> + >> + + 다음 + 다음 +
+
+
+ + diff --git a/src/main/resources/templates/practice-notes.html b/src/main/resources/templates/practice-notes.html index d5df7768..cc3e8a59 100644 --- a/src/main/resources/templates/practice-notes.html +++ b/src/main/resources/templates/practice-notes.html @@ -35,8 +35,7 @@

OnO 관리자

복습 관리

- 복습노트 0개, - 복습 기록 0개 + 복습노트 0

@@ -45,10 +44,10 @@

복습 관리

총 복습노트 수

0

-
-

총 복습 기록 수

-

0

-
+ +

복습 기록

+

복습 기록 페이지로 이동

+
@@ -56,7 +55,6 @@

복습 관리

복습노트 목록

-