Skip to content

Commit 482acaf

Browse files
authored
feat: 스터디 통계 조회 API 추가 (#809)
* feat: response dto 추가 * feat: 레포지토리, 도메인 메서드 추가 * feat: 통계 조회 로직 작성 * feat: 통계 조회 컨트롤러 * fix: 평균 출석률, 과제 제출률 계산 로직 개선 * chore: spotless 적용 * chore: import 축약 수정 * chore: dto 설명 문구 수정 * chore: 변수명 total로 통일 * chore: 정적 팩토리 메서드명 컨벤션에 맞게 수정 * refactor: 과제 휴강 고려한 로직 수정 * chore: StudyWeekResponse static import 제거 * refactor: StudyStatisticsReponse 에서 0으로 나누는 케이스 고려 * fix: 연산자 오타 수정 * refactor: DTO 정적 메서드 추가 및 로직 메서드 분리 * chore: 변수명 및 메서드명 수정 * chore: soptless 적용 * chore: 변수명 개선 * chore: dto 에서 변수명 통일 * chore: isCompleted 로 메서드 명 개선
1 parent adf7c95 commit 482acaf

7 files changed

+167
-0
lines changed

src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyDetailController.java

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.gdschongik.gdsc.domain.study.dto.response.AssignmentResponse;
66
import com.gdschongik.gdsc.domain.study.dto.response.StudyCurriculumResponse;
77
import com.gdschongik.gdsc.domain.study.dto.response.StudyMentorAttendanceResponse;
8+
import com.gdschongik.gdsc.domain.study.dto.response.StudyStatisticsResponse;
89
import io.swagger.v3.oas.annotations.Operation;
910
import io.swagger.v3.oas.annotations.tags.Tag;
1011
import jakarta.validation.Valid;
@@ -80,4 +81,11 @@ public ResponseEntity<List<StudyMentorAttendanceResponse>> getAttendanceNumbers(
8081
List<StudyMentorAttendanceResponse> response = mentorStudyDetailService.getAttendanceNumbers(studyId);
8182
return ResponseEntity.ok(response);
8283
}
84+
85+
@Operation(summary = "스터디 통계 조회", description = "멘토가 자신의 스터디 출석률, 과제 제출률, 수료율에 대한 통계를 조회합니다. 휴강 주차는 계산에서 제외합니다.")
86+
@GetMapping("/statistics")
87+
public ResponseEntity<StudyStatisticsResponse> getStudyStatistics(@RequestParam(name = "studyId") Long studyId) {
88+
StudyStatisticsResponse response = mentorStudyDetailService.getStudyStatistics(studyId);
89+
return ResponseEntity.ok(response);
90+
}
8391
}

src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java

+94
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
package com.gdschongik.gdsc.domain.study.application;
22

3+
import static com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus.SUCCESS;
34
import static com.gdschongik.gdsc.global.exception.ErrorCode.*;
45

56
import com.gdschongik.gdsc.domain.member.domain.Member;
7+
import com.gdschongik.gdsc.domain.study.dao.AssignmentHistoryRepository;
8+
import com.gdschongik.gdsc.domain.study.dao.AttendanceRepository;
69
import com.gdschongik.gdsc.domain.study.dao.StudyDetailRepository;
10+
import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository;
11+
import com.gdschongik.gdsc.domain.study.dao.StudyRepository;
12+
import com.gdschongik.gdsc.domain.study.domain.Study;
713
import com.gdschongik.gdsc.domain.study.domain.StudyDetail;
814
import com.gdschongik.gdsc.domain.study.domain.StudyDetailValidator;
15+
import com.gdschongik.gdsc.domain.study.domain.StudyHistory;
16+
import com.gdschongik.gdsc.domain.study.domain.StudyValidator;
917
import com.gdschongik.gdsc.domain.study.dto.request.AssignmentCreateUpdateRequest;
1018
import com.gdschongik.gdsc.domain.study.dto.response.AssignmentResponse;
1119
import com.gdschongik.gdsc.domain.study.dto.response.StudyCurriculumResponse;
1220
import com.gdschongik.gdsc.domain.study.dto.response.StudyMentorAttendanceResponse;
21+
import com.gdschongik.gdsc.domain.study.dto.response.StudyStatisticsResponse;
22+
import com.gdschongik.gdsc.domain.study.dto.response.StudyWeekStatisticsResponse;
1323
import com.gdschongik.gdsc.global.exception.CustomException;
1424
import com.gdschongik.gdsc.global.util.MemberUtil;
1525
import java.time.LocalDate;
@@ -27,6 +37,11 @@ public class MentorStudyDetailService {
2737
private final MemberUtil memberUtil;
2838
private final StudyDetailRepository studyDetailRepository;
2939
private final StudyDetailValidator studyDetailValidator;
40+
private final StudyHistoryRepository studyHistoryRepository;
41+
private final AttendanceRepository attendanceRepository;
42+
private final AssignmentHistoryRepository assignmentHistoryRepository;
43+
private final StudyValidator studyValidator;
44+
private final StudyRepository studyRepository;
3045

3146
@Transactional(readOnly = true)
3247
public List<AssignmentResponse> getWeeklyAssignments(Long studyId) {
@@ -108,4 +123,83 @@ public List<StudyMentorAttendanceResponse> getAttendanceNumbers(Long studyId) {
108123
.limit(2)
109124
.toList();
110125
}
126+
127+
@Transactional(readOnly = true)
128+
public StudyStatisticsResponse getStudyStatistics(Long studyId) {
129+
Member currentMember = memberUtil.getCurrentMember();
130+
Study study = studyRepository.findById(studyId).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND));
131+
List<StudyHistory> studyHistories = studyHistoryRepository.findAllByStudyId(studyId);
132+
List<StudyDetail> studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId);
133+
studyValidator.validateStudyMentor(currentMember, study);
134+
135+
long totalStudentCount = studyHistories.size();
136+
long studyCompletedStudentCount =
137+
studyHistories.stream().filter(StudyHistory::isCompleted).count();
138+
139+
List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses = studyDetails.stream()
140+
.map((studyDetail -> calculateWeekStatistics(studyDetail, totalStudentCount)))
141+
.toList();
142+
143+
long averageAttendanceRate = calculateAverageWeekAttendanceRate(studyWeekStatisticsResponses);
144+
long averageAssignmentSubmissionRate =
145+
calculateAverageWeekAssignmentSubmissionRate(studyWeekStatisticsResponses);
146+
147+
return StudyStatisticsResponse.of(
148+
totalStudentCount,
149+
studyCompletedStudentCount,
150+
averageAttendanceRate,
151+
averageAssignmentSubmissionRate,
152+
studyWeekStatisticsResponses);
153+
}
154+
155+
private StudyWeekStatisticsResponse calculateWeekStatistics(StudyDetail studyDetail, Long totalStudentCount) {
156+
boolean isNotOpenedCurriculum = !studyDetail.getCurriculum().isOpen();
157+
boolean isNotOpenedAssignment = !studyDetail.getAssignment().isOpen() || isNotOpenedCurriculum;
158+
159+
if (totalStudentCount == 0) {
160+
return StudyWeekStatisticsResponse.empty(
161+
studyDetail.getWeek(), isNotOpenedAssignment, isNotOpenedCurriculum);
162+
}
163+
164+
if (isNotOpenedCurriculum) {
165+
return StudyWeekStatisticsResponse.canceledWeek(studyDetail.getWeek());
166+
}
167+
168+
long attendanceCount = attendanceRepository.countByStudyDetailId(studyDetail.getId());
169+
long attendanceRate = Math.round(attendanceCount / (double) totalStudentCount * 100);
170+
171+
if (isNotOpenedAssignment) {
172+
return StudyWeekStatisticsResponse.assignmentCanceled(studyDetail.getWeek(), attendanceRate);
173+
}
174+
175+
long successfullySubmittedAssignmentCount =
176+
assignmentHistoryRepository.countByStudyDetailIdAndSubmissionStatusEquals(studyDetail.getId(), SUCCESS);
177+
long assignmentSubmissionRate =
178+
Math.round(successfullySubmittedAssignmentCount / (double) totalStudentCount * 100);
179+
180+
return StudyWeekStatisticsResponse.opened(studyDetail.getWeek(), attendanceRate, assignmentSubmissionRate);
181+
}
182+
183+
private long calculateAverageWeekAttendanceRate(List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses) {
184+
185+
double averageAttendanceRate = studyWeekStatisticsResponses.stream()
186+
.filter(weekStatisticsResponse -> !weekStatisticsResponse.isCurriculumCanceled())
187+
.mapToLong(StudyWeekStatisticsResponse::attendanceRate)
188+
.average()
189+
.orElse(0);
190+
191+
return Math.round(averageAttendanceRate);
192+
}
193+
194+
private long calculateAverageWeekAssignmentSubmissionRate(
195+
List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses) {
196+
197+
double averageAssignmentSubmissionRate = studyWeekStatisticsResponses.stream()
198+
.filter(studyWeekStatistics -> !studyWeekStatistics.isAssignmentCanceled())
199+
.mapToLong(StudyWeekStatisticsResponse::assignmentSubmissionRate)
200+
.average()
201+
.orElse(0);
202+
203+
return Math.round(averageAssignmentSubmissionRate);
204+
}
111205
}

src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryRepository.java

+3
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
import com.gdschongik.gdsc.domain.member.domain.Member;
44
import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory;
5+
import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus;
56
import com.gdschongik.gdsc.domain.study.domain.StudyDetail;
67
import java.util.Optional;
78
import org.springframework.data.jpa.repository.JpaRepository;
89

910
public interface AssignmentHistoryRepository
1011
extends JpaRepository<AssignmentHistory, Long>, AssignmentHistoryCustomRepository {
1112
Optional<AssignmentHistory> findByMemberAndStudyDetail(Member member, StudyDetail studyDetail);
13+
14+
long countByStudyDetailIdAndSubmissionStatusEquals(Long studyDetailId, AssignmentSubmissionStatus status);
1215
}

src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java

+2
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@
55

66
public interface AttendanceRepository extends JpaRepository<Attendance, Long>, AttendanceCustomRepository {
77
boolean existsByStudentIdAndStudyDetailId(Long studentId, Long studyDetailId);
8+
9+
long countByStudyDetailId(Long studyDetailId);
810
}

src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java

+4
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,8 @@ public void complete() {
8282
public boolean isWithinApplicationAndCourse() {
8383
return study.isWithinApplicationAndCourse();
8484
}
85+
86+
public boolean isCompleted() {
87+
return studyHistoryStatus == COMPLETED;
88+
}
8589
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.gdschongik.gdsc.domain.study.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import java.util.List;
5+
6+
public record StudyStatisticsResponse(
7+
@Schema(description = "스터디 전체 수강생 수") Long totalStudentCount,
8+
@Schema(description = "스터디 수료 수강생 수") Long completeStudentCount,
9+
@Schema(description = "평균 출석률") Long averageAttendanceRate,
10+
@Schema(description = "평균 과제 제출률") Long averageAssignmentSubmissionRate,
11+
@Schema(description = "스터디 수료율") Long studyCompleteRate,
12+
@Schema(description = "주차별 출석률 및 과제 제출률") List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses) {
13+
14+
public static StudyStatisticsResponse of(
15+
Long totalStudentCount,
16+
Long completeStudentCount,
17+
Long averageAttendanceRate,
18+
Long averageAssignmentSubmissionRate,
19+
List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses) {
20+
return new StudyStatisticsResponse(
21+
totalStudentCount,
22+
completeStudentCount,
23+
averageAttendanceRate,
24+
averageAssignmentSubmissionRate,
25+
totalStudentCount == 0 ? 0 : Math.round(completeStudentCount / (double) totalStudentCount * 100),
26+
studyWeekStatisticsResponses);
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.gdschongik.gdsc.domain.study.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
public record StudyWeekStatisticsResponse(
6+
@Schema(description = "스터디 주차") Long week,
7+
@Schema(description = "출석률") Long attendanceRate,
8+
@Schema(description = "과제 제출률") Long assignmentSubmissionRate,
9+
@Schema(description = "과제 휴강 여부") boolean isAssignmentCanceled,
10+
@Schema(description = "수업 휴강 여부") boolean isCurriculumCanceled) {
11+
12+
public static StudyWeekStatisticsResponse opened(Long week, Long attendanceRate, Long assignmentSubmissionRate) {
13+
return new StudyWeekStatisticsResponse(week, attendanceRate, assignmentSubmissionRate, false, false);
14+
}
15+
16+
public static StudyWeekStatisticsResponse empty(
17+
Long week, boolean isAssignmentCanceled, boolean isCurriculumCanceled) {
18+
return new StudyWeekStatisticsResponse(week, 0L, 0L, isAssignmentCanceled, isCurriculumCanceled);
19+
}
20+
21+
public static StudyWeekStatisticsResponse canceledWeek(Long week) {
22+
return StudyWeekStatisticsResponse.empty(week, true, true);
23+
}
24+
25+
public static StudyWeekStatisticsResponse assignmentCanceled(Long week, Long attendanceRate) {
26+
return new StudyWeekStatisticsResponse(week, attendanceRate, 0L, true, false);
27+
}
28+
}

0 commit comments

Comments
 (0)