Skip to content

Commit 27dfbc3

Browse files
authored
feat: 풀이 현황 테이블 조회 API 추가 (#379)
1 parent a1889da commit 27dfbc3

File tree

8 files changed

+247
-23
lines changed

8 files changed

+247
-23
lines changed

src/main/java/com/gamzabat/algohub/feature/problem/repository/querydsl/CustomProblemRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.gamzabat.algohub.feature.problem.repository.querydsl;
22

3+
import java.util.List;
4+
35
import org.springframework.data.domain.Page;
46
import org.springframework.data.domain.Pageable;
57

@@ -10,6 +12,8 @@
1012
public interface CustomProblemRepository {
1113
Page<Problem> findAllInProgressProblem(User user, StudyGroup group, Boolean unsolvedOnly, Pageable pageable);
1214

15+
List<Problem> findAllInProgressProblem(StudyGroup group);
16+
1317
Page<Problem> findAllExpiredProblem(StudyGroup group, Pageable pageable);
1418

1519
Page<Problem> findAllQueuedProblem(StudyGroup group, Pageable pageable);

src/main/java/com/gamzabat/algohub/feature/problem/repository/querydsl/CustomProblemRepositoryImpl.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static com.gamzabat.algohub.feature.solution.domain.QSolution.*;
55

66
import java.time.LocalDate;
7+
import java.util.List;
78

89
import org.springframework.data.domain.Page;
910
import org.springframework.data.domain.Pageable;
@@ -41,6 +42,16 @@ public Page<Problem> findAllInProgressProblem(User user, StudyGroup group, Boole
4142
return findAllProblemByCondition(condition, pageable);
4243
}
4344

45+
@Override
46+
public List<Problem> findAllInProgressProblem(StudyGroup group) {
47+
JPAQuery<Problem> query = queryFactory.selectFrom(problem)
48+
.where(problem.studyGroup.eq(group)
49+
.and(problem.deletedAt.isNull())
50+
.and(problem.startDate.loe(LocalDate.now()))
51+
.and(problem.endDate.goe(LocalDate.now())));
52+
return query.fetch();
53+
}
54+
4455
@Override
4556
public Page<Problem> findAllExpiredProblem(StudyGroup group, Pageable pageable) {
4657
BooleanExpression condition = problem.studyGroup.eq(group)

src/main/java/com/gamzabat/algohub/feature/solution/controller/SolutionController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.gamzabat.algohub.feature.solution.controller;
22

3+
import java.util.List;
4+
35
import org.springframework.data.domain.Page;
46
import org.springframework.data.domain.PageRequest;
57
import org.springframework.data.domain.Pageable;
@@ -17,6 +19,7 @@
1719
import com.gamzabat.algohub.common.annotation.AuthedUser;
1820
import com.gamzabat.algohub.exception.RequestException;
1921
import com.gamzabat.algohub.feature.solution.dto.CreateSolutionRequest;
22+
import com.gamzabat.algohub.feature.solution.dto.GetCurrentSolvingStatusResponse;
2023
import com.gamzabat.algohub.feature.solution.dto.GetSolutionResponse;
2124
import com.gamzabat.algohub.feature.solution.dto.GetSolutionWithGroupIdResponse;
2225
import com.gamzabat.algohub.feature.solution.service.SolutionService;
@@ -123,4 +126,12 @@ public ResponseEntity<Page<GetSolutionWithGroupIdResponse>> getMySolutions(@Auth
123126
problemNumber, language, result, pageable);
124127
return ResponseEntity.ok().body(response);
125128
}
129+
130+
@GetMapping("/groups/{groupId}/solutions/current-status")
131+
@Operation(summary = "풀이 현황 테이블 조회 API", description = "진행 중인 문제들에 대해 풀이 현황 테이블을 조회하는 API")
132+
public ResponseEntity<List<GetCurrentSolvingStatusResponse>> getCurrentSolvingStatus(@AuthedUser User user,
133+
@PathVariable Long groupId) {
134+
List<GetCurrentSolvingStatusResponse> response = solutionService.getCurrentSolvingStatuses(user, groupId);
135+
return ResponseEntity.ok().body(response);
136+
}
126137
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.gamzabat.algohub.feature.solution.dto;
2+
3+
import java.util.List;
4+
5+
public record GetCurrentSolvingStatusResponse(Integer rank,
6+
String nickname,
7+
Integer totalSubmissionCount,
8+
String totalPassedTime,
9+
List<GetSolvingStatusPerProblemResponse> problems) {
10+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.gamzabat.algohub.feature.solution.dto;
2+
3+
public record GetSolvingStatusPerProblemResponse(Long problemId,
4+
Long firstCorrectSolutionId,
5+
int submissionCount,
6+
String firstCorrectDuration,
7+
boolean solved) {
8+
}

src/main/java/com/gamzabat/algohub/feature/solution/repository/SolutionRepository.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.gamzabat.algohub.feature.solution.repository;
22

3+
import java.util.List;
34
import java.util.Optional;
45

56
import org.springframework.data.jpa.repository.JpaRepository;
@@ -59,4 +60,11 @@ Long countDistinctCorrectSolutionsByUserAndGroup(User user, StudyGroup group,
5960
+ "FROM Problem p "
6061
+ "WHERE p.studyGroup = :studyGroup)")
6162
void deleteAllByStudyGroupAndUser(StudyGroup studyGroup, User user);
63+
64+
@Query("SELECT s "
65+
+ "FROM Solution s "
66+
+ "WHERE s.deletedAt IS NULL "
67+
+ "AND s.user = :user "
68+
+ "AND s.problem = :problem")
69+
List<Solution> findAllByUserAndProblem(User user, Problem problem);
6270
}

src/main/java/com/gamzabat/algohub/feature/solution/service/SolutionService.java

Lines changed: 137 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
package com.gamzabat.algohub.feature.solution.service;
22

3+
import java.time.Duration;
34
import java.time.LocalDate;
45
import java.time.LocalDateTime;
6+
import java.util.ArrayList;
7+
import java.util.Comparator;
8+
import java.util.LinkedHashMap;
59
import java.util.List;
10+
import java.util.Map;
11+
import java.util.Optional;
612

713
import org.springframework.data.domain.Page;
814
import org.springframework.data.domain.Pageable;
@@ -29,8 +35,10 @@
2935
import com.gamzabat.algohub.feature.solution.domain.Solution;
3036
import com.gamzabat.algohub.feature.solution.domain.SolutionComment;
3137
import com.gamzabat.algohub.feature.solution.dto.CreateSolutionRequest;
38+
import com.gamzabat.algohub.feature.solution.dto.GetCurrentSolvingStatusResponse;
3239
import com.gamzabat.algohub.feature.solution.dto.GetSolutionResponse;
3340
import com.gamzabat.algohub.feature.solution.dto.GetSolutionWithGroupIdResponse;
41+
import com.gamzabat.algohub.feature.solution.dto.GetSolvingStatusPerProblemResponse;
3442
import com.gamzabat.algohub.feature.solution.enums.ProgressCategory;
3543
import com.gamzabat.algohub.feature.solution.exception.CannotFoundSolutionException;
3644
import com.gamzabat.algohub.feature.solution.repository.SolutionCommentRepository;
@@ -94,11 +102,7 @@ public GetSolutionResponse getSolution(User user, Long solutionId) {
94102
public Page<GetSolutionResponse> getMySolutionsInGroupInProgress(User user, Long groupId, Integer problemNumber,
95103
String language,
96104
String result, Pageable pageable) {
97-
StudyGroup group = studyGroupRepository.findById(groupId)
98-
.orElseThrow(() -> new CannotFoundGroupException("존재하지 않는 그룹입니다."));
99-
if (!groupMemberRepository.existsByUserAndStudyGroup(user, group)) {
100-
throw new GroupMemberValidationException(HttpStatus.FORBIDDEN.value(), "참여하지 않은 그룹입니다.");
101-
}
105+
StudyGroup group = validateGroupAndMember(user, groupId);
102106

103107
Page<GetSolutionResponse> inProgressSolutions = solutionRepository.findAllFilteredMySolutionsInGroup(user,
104108
group, problemNumber, language, result, ProgressCategory.IN_PROGRESS, pageable)
@@ -112,11 +116,7 @@ public Page<GetSolutionResponse> getMySolutionsInGroupInProgress(User user, Long
112116
public Page<GetSolutionResponse> getMySolutionsInGroupExpired(User user, Long groupId, Integer problemNumber,
113117
String language,
114118
String result, Pageable pageable) {
115-
StudyGroup group = studyGroupRepository.findById(groupId)
116-
.orElseThrow(() -> new CannotFoundGroupException("존재하지 않는 그룹입니다."));
117-
if (!groupMemberRepository.existsByUserAndStudyGroup(user, group)) {
118-
throw new GroupMemberValidationException(HttpStatus.FORBIDDEN.value(), "참여하지 않은 그룹입니다.");
119-
}
119+
StudyGroup group = validateGroupAndMember(user, groupId);
120120

121121
Page<GetSolutionResponse> expiredSolutions = solutionRepository.findAllFilteredMySolutionsInGroup(user, group,
122122
problemNumber, language, result, ProgressCategory.EXPIRED, pageable)
@@ -153,6 +153,120 @@ public Page<GetSolutionWithGroupIdResponse> getMySolutionsExpired(User user, Int
153153
return expiredSolutions;
154154
}
155155

156+
@Transactional(readOnly = true)
157+
public List<GetCurrentSolvingStatusResponse> getCurrentSolvingStatuses(User user, Long groupId) {
158+
StudyGroup group = validateGroupAndMember(user, groupId);
159+
160+
List<Problem> inProgressProblems = problemRepository.findAllInProgressProblem(group);
161+
List<GroupMember> members = groupMemberRepository.findAllByStudyGroup(group);
162+
163+
Map<GroupMember, SolvedStatusResult> solvedStatuses = calculateMemberStatusRanks(members, inProgressProblems);
164+
165+
return createSolvingStatusResponses(solvedStatuses);
166+
}
167+
168+
private Map<GroupMember, SolvedStatusResult> calculateMemberStatusRanks(List<GroupMember> members,
169+
List<Problem> inProgressProblems) {
170+
Map<GroupMember, SolvedStatusResult> ranks = new LinkedHashMap<>();
171+
172+
for (GroupMember member : members) {
173+
int totalSubmissionCount = 0;
174+
Duration totalPassedTime = Duration.ZERO;
175+
List<GetSolvingStatusPerProblemResponse> statusResponses = new ArrayList<>();
176+
177+
for (Problem problem : inProgressProblems) {
178+
List<Solution> solutions = solutionRepository.findAllByUserAndProblem(member.getUser(), problem);
179+
int submissionCount = solutions.size();
180+
Long firstCorrectSolutionId = null;
181+
String firstCorrectDuration = "--";
182+
boolean solved = false;
183+
184+
Optional<Solution> firstCorrectSolution = solutions.stream()
185+
.filter(solution -> isCorrect(solution.getResult()))
186+
.min(Comparator.comparing(Solution::getSolvedDateTime));
187+
188+
if (firstCorrectSolution.isPresent()) {
189+
Solution solution = firstCorrectSolution.get();
190+
191+
firstCorrectSolutionId = solution.getId();
192+
193+
Duration duration = calculateGap(problem, solution);
194+
totalPassedTime = totalPassedTime.plus(duration);
195+
firstCorrectDuration = convertToSolvedTimeFormat(duration);
196+
solved = true;
197+
}
198+
totalSubmissionCount += submissionCount;
199+
200+
statusResponses.add(new GetSolvingStatusPerProblemResponse(
201+
problem.getId(), firstCorrectSolutionId,
202+
submissionCount, firstCorrectDuration, solved
203+
));
204+
}
205+
206+
float totalScore = calculateTotalScore(totalSubmissionCount, totalPassedTime);
207+
String formattedPassedTime = convertToSolvedTimeFormat(totalPassedTime);
208+
ranks.put(member,
209+
new SolvedStatusResult(totalScore, totalSubmissionCount, formattedPassedTime, statusResponses));
210+
}
211+
return ranks;
212+
}
213+
214+
private Duration calculateGap(Problem problem, Solution solution) {
215+
LocalDateTime startDate = problem.getStartDate().atStartOfDay();
216+
LocalDateTime solvedDateTime = solution.getSolvedDateTime();
217+
return Duration.between(startDate, solvedDateTime);
218+
}
219+
220+
private float calculateTotalScore(int totalSubmissionCount, Duration totalPassedTime) {
221+
float totalScore = 0;
222+
if (!(totalSubmissionCount == 0 || totalPassedTime.isZero())) {
223+
long minutes = totalPassedTime.toMinutes();
224+
totalScore = (float)1 / (totalSubmissionCount * minutes);
225+
}
226+
return totalScore;
227+
}
228+
229+
private List<GetCurrentSolvingStatusResponse> createSolvingStatusResponses(
230+
Map<GroupMember, SolvedStatusResult> ranks) {
231+
List<GroupMember> memberOrders = ranks.keySet().stream()
232+
.sorted((m1, m2) -> Float.compare(ranks.get(m2).totalScore, ranks.get(m1).totalScore))
233+
.toList();
234+
235+
List<GetCurrentSolvingStatusResponse> responses = new ArrayList<>();
236+
for (int i = 0; i < memberOrders.size(); i++) {
237+
GroupMember member = memberOrders.get(i);
238+
responses.add(new GetCurrentSolvingStatusResponse(
239+
i + 1, member.getUser().getNickname(),
240+
ranks.get(member).totalSubmissionCount,
241+
ranks.get(member).totalPassedTime,
242+
ranks.get(member).problems
243+
));
244+
}
245+
return responses;
246+
}
247+
248+
private void sendNewSolutionNotification(StudyGroup group, GroupMember solver, Problem problem) {
249+
List<GroupMember> groupMembers = groupMemberRepository.findAllByStudyGroup(group).stream()
250+
.filter(member -> !member.getId().equals(solver.getId()))
251+
.toList();
252+
253+
notificationService.sendNotificationToMembers(
254+
group,
255+
groupMembers,
256+
problem,
257+
null,
258+
NotificationCategory.NEW_SOLUTION_POSTED,
259+
NotificationCategory.NEW_SOLUTION_POSTED.getMessage(solver.getUser().getNickname())
260+
);
261+
}
262+
263+
private String convertToSolvedTimeFormat(Duration duration) {
264+
long totalMinutes = duration.toMinutes();
265+
long hours = totalMinutes / 60;
266+
long minutes = totalMinutes % 60;
267+
return String.format("%d:%02d", hours, minutes);
268+
}
269+
156270
private GetSolutionWithGroupIdResponse getGetSolutionWithGroupIdResponse(User user, Solution solution) {
157271
Integer correctCount = getCorrectCount(solution);
158272
Integer submitMemberCount = solutionRepository.countDistinctUsersByProblem(solution.getProblem());
@@ -244,19 +358,13 @@ public void createSolution(CreateSolutionRequest request) {
244358

245359
}
246360

247-
private void sendNewSolutionNotification(StudyGroup group, GroupMember solver, Problem problem) {
248-
List<GroupMember> groupMembers = groupMemberRepository.findAllByStudyGroup(group).stream()
249-
.filter(member -> !member.getId().equals(solver.getId()))
250-
.toList();
251-
252-
notificationService.sendNotificationToMembers(
253-
group,
254-
groupMembers,
255-
problem,
256-
null,
257-
NotificationCategory.NEW_SOLUTION_POSTED,
258-
NotificationCategory.NEW_SOLUTION_POSTED.getMessage(solver.getUser().getNickname())
259-
);
361+
private StudyGroup validateGroupAndMember(User user, Long groupId) {
362+
StudyGroup group = studyGroupRepository.findById(groupId)
363+
.orElseThrow(() -> new CannotFoundGroupException("존재하지 않는 그룹입니다."));
364+
if (!groupMemberRepository.existsByUserAndStudyGroup(user, group)) {
365+
throw new GroupMemberValidationException(HttpStatus.FORBIDDEN.value(), "참여하지 않은 그룹입니다.");
366+
}
367+
return group;
260368
}
261369

262370
private boolean isCorrect(String result) {
@@ -291,4 +399,10 @@ private boolean isAllCommentsRead(Solution solution) {
291399

292400
return true;
293401
}
402+
403+
private record SolvedStatusResult(float totalScore,
404+
int totalSubmissionCount,
405+
String totalPassedTime,
406+
List<GetSolvingStatusPerProblemResponse> problems) {
407+
}
294408
}

0 commit comments

Comments
 (0)