Skip to content

Commit

Permalink
feat: 스터디 통계 조회 API 추가 (#809)
Browse files Browse the repository at this point in the history
* 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 로 메서드 명 개선
  • Loading branch information
kckc0608 authored Nov 1, 2024
1 parent adf7c95 commit 482acaf
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.gdschongik.gdsc.domain.study.dto.response.AssignmentResponse;
import com.gdschongik.gdsc.domain.study.dto.response.StudyCurriculumResponse;
import com.gdschongik.gdsc.domain.study.dto.response.StudyMentorAttendanceResponse;
import com.gdschongik.gdsc.domain.study.dto.response.StudyStatisticsResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
Expand Down Expand Up @@ -80,4 +81,11 @@ public ResponseEntity<List<StudyMentorAttendanceResponse>> getAttendanceNumbers(
List<StudyMentorAttendanceResponse> response = mentorStudyDetailService.getAttendanceNumbers(studyId);
return ResponseEntity.ok(response);
}

@Operation(summary = "스터디 통계 조회", description = "멘토가 자신의 스터디 출석률, 과제 제출률, 수료율에 대한 통계를 조회합니다. 휴강 주차는 계산에서 제외합니다.")
@GetMapping("/statistics")
public ResponseEntity<StudyStatisticsResponse> getStudyStatistics(@RequestParam(name = "studyId") Long studyId) {
StudyStatisticsResponse response = mentorStudyDetailService.getStudyStatistics(studyId);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
package com.gdschongik.gdsc.domain.study.application;

import static com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus.SUCCESS;
import static com.gdschongik.gdsc.global.exception.ErrorCode.*;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.study.dao.AssignmentHistoryRepository;
import com.gdschongik.gdsc.domain.study.dao.AttendanceRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyDetailRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyRepository;
import com.gdschongik.gdsc.domain.study.domain.Study;
import com.gdschongik.gdsc.domain.study.domain.StudyDetail;
import com.gdschongik.gdsc.domain.study.domain.StudyDetailValidator;
import com.gdschongik.gdsc.domain.study.domain.StudyHistory;
import com.gdschongik.gdsc.domain.study.domain.StudyValidator;
import com.gdschongik.gdsc.domain.study.dto.request.AssignmentCreateUpdateRequest;
import com.gdschongik.gdsc.domain.study.dto.response.AssignmentResponse;
import com.gdschongik.gdsc.domain.study.dto.response.StudyCurriculumResponse;
import com.gdschongik.gdsc.domain.study.dto.response.StudyMentorAttendanceResponse;
import com.gdschongik.gdsc.domain.study.dto.response.StudyStatisticsResponse;
import com.gdschongik.gdsc.domain.study.dto.response.StudyWeekStatisticsResponse;
import com.gdschongik.gdsc.global.exception.CustomException;
import com.gdschongik.gdsc.global.util.MemberUtil;
import java.time.LocalDate;
Expand All @@ -27,6 +37,11 @@ public class MentorStudyDetailService {
private final MemberUtil memberUtil;
private final StudyDetailRepository studyDetailRepository;
private final StudyDetailValidator studyDetailValidator;
private final StudyHistoryRepository studyHistoryRepository;
private final AttendanceRepository attendanceRepository;
private final AssignmentHistoryRepository assignmentHistoryRepository;
private final StudyValidator studyValidator;
private final StudyRepository studyRepository;

@Transactional(readOnly = true)
public List<AssignmentResponse> getWeeklyAssignments(Long studyId) {
Expand Down Expand Up @@ -108,4 +123,83 @@ public List<StudyMentorAttendanceResponse> getAttendanceNumbers(Long studyId) {
.limit(2)
.toList();
}

@Transactional(readOnly = true)
public StudyStatisticsResponse getStudyStatistics(Long studyId) {
Member currentMember = memberUtil.getCurrentMember();
Study study = studyRepository.findById(studyId).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND));
List<StudyHistory> studyHistories = studyHistoryRepository.findAllByStudyId(studyId);
List<StudyDetail> studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId);
studyValidator.validateStudyMentor(currentMember, study);

long totalStudentCount = studyHistories.size();
long studyCompletedStudentCount =
studyHistories.stream().filter(StudyHistory::isCompleted).count();

List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses = studyDetails.stream()
.map((studyDetail -> calculateWeekStatistics(studyDetail, totalStudentCount)))
.toList();

long averageAttendanceRate = calculateAverageWeekAttendanceRate(studyWeekStatisticsResponses);
long averageAssignmentSubmissionRate =
calculateAverageWeekAssignmentSubmissionRate(studyWeekStatisticsResponses);

return StudyStatisticsResponse.of(
totalStudentCount,
studyCompletedStudentCount,
averageAttendanceRate,
averageAssignmentSubmissionRate,
studyWeekStatisticsResponses);
}

private StudyWeekStatisticsResponse calculateWeekStatistics(StudyDetail studyDetail, Long totalStudentCount) {
boolean isNotOpenedCurriculum = !studyDetail.getCurriculum().isOpen();
boolean isNotOpenedAssignment = !studyDetail.getAssignment().isOpen() || isNotOpenedCurriculum;

if (totalStudentCount == 0) {
return StudyWeekStatisticsResponse.empty(
studyDetail.getWeek(), isNotOpenedAssignment, isNotOpenedCurriculum);
}

if (isNotOpenedCurriculum) {
return StudyWeekStatisticsResponse.canceledWeek(studyDetail.getWeek());
}

long attendanceCount = attendanceRepository.countByStudyDetailId(studyDetail.getId());
long attendanceRate = Math.round(attendanceCount / (double) totalStudentCount * 100);

if (isNotOpenedAssignment) {
return StudyWeekStatisticsResponse.assignmentCanceled(studyDetail.getWeek(), attendanceRate);
}

long successfullySubmittedAssignmentCount =
assignmentHistoryRepository.countByStudyDetailIdAndSubmissionStatusEquals(studyDetail.getId(), SUCCESS);
long assignmentSubmissionRate =
Math.round(successfullySubmittedAssignmentCount / (double) totalStudentCount * 100);

return StudyWeekStatisticsResponse.opened(studyDetail.getWeek(), attendanceRate, assignmentSubmissionRate);
}

private long calculateAverageWeekAttendanceRate(List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses) {

double averageAttendanceRate = studyWeekStatisticsResponses.stream()
.filter(weekStatisticsResponse -> !weekStatisticsResponse.isCurriculumCanceled())
.mapToLong(StudyWeekStatisticsResponse::attendanceRate)
.average()
.orElse(0);

return Math.round(averageAttendanceRate);
}

private long calculateAverageWeekAssignmentSubmissionRate(
List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses) {

double averageAssignmentSubmissionRate = studyWeekStatisticsResponses.stream()
.filter(studyWeekStatistics -> !studyWeekStatistics.isAssignmentCanceled())
.mapToLong(StudyWeekStatisticsResponse::assignmentSubmissionRate)
.average()
.orElse(0);

return Math.round(averageAssignmentSubmissionRate);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory;
import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus;
import com.gdschongik.gdsc.domain.study.domain.StudyDetail;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AssignmentHistoryRepository
extends JpaRepository<AssignmentHistory, Long>, AssignmentHistoryCustomRepository {
Optional<AssignmentHistory> findByMemberAndStudyDetail(Member member, StudyDetail studyDetail);

long countByStudyDetailIdAndSubmissionStatusEquals(Long studyDetailId, AssignmentSubmissionStatus status);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@

public interface AttendanceRepository extends JpaRepository<Attendance, Long>, AttendanceCustomRepository {
boolean existsByStudentIdAndStudyDetailId(Long studentId, Long studyDetailId);

long countByStudyDetailId(Long studyDetailId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,8 @@ public void complete() {
public boolean isWithinApplicationAndCourse() {
return study.isWithinApplicationAndCourse();
}

public boolean isCompleted() {
return studyHistoryStatus == COMPLETED;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.gdschongik.gdsc.domain.study.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;

public record StudyStatisticsResponse(
@Schema(description = "스터디 전체 수강생 수") Long totalStudentCount,
@Schema(description = "스터디 수료 수강생 수") Long completeStudentCount,
@Schema(description = "평균 출석률") Long averageAttendanceRate,
@Schema(description = "평균 과제 제출률") Long averageAssignmentSubmissionRate,
@Schema(description = "스터디 수료율") Long studyCompleteRate,
@Schema(description = "주차별 출석률 및 과제 제출률") List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses) {

public static StudyStatisticsResponse of(
Long totalStudentCount,
Long completeStudentCount,
Long averageAttendanceRate,
Long averageAssignmentSubmissionRate,
List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses) {
return new StudyStatisticsResponse(
totalStudentCount,
completeStudentCount,
averageAttendanceRate,
averageAssignmentSubmissionRate,
totalStudentCount == 0 ? 0 : Math.round(completeStudentCount / (double) totalStudentCount * 100),
studyWeekStatisticsResponses);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.gdschongik.gdsc.domain.study.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;

public record StudyWeekStatisticsResponse(
@Schema(description = "스터디 주차") Long week,
@Schema(description = "출석률") Long attendanceRate,
@Schema(description = "과제 제출률") Long assignmentSubmissionRate,
@Schema(description = "과제 휴강 여부") boolean isAssignmentCanceled,
@Schema(description = "수업 휴강 여부") boolean isCurriculumCanceled) {

public static StudyWeekStatisticsResponse opened(Long week, Long attendanceRate, Long assignmentSubmissionRate) {
return new StudyWeekStatisticsResponse(week, attendanceRate, assignmentSubmissionRate, false, false);
}

public static StudyWeekStatisticsResponse empty(
Long week, boolean isAssignmentCanceled, boolean isCurriculumCanceled) {
return new StudyWeekStatisticsResponse(week, 0L, 0L, isAssignmentCanceled, isCurriculumCanceled);
}

public static StudyWeekStatisticsResponse canceledWeek(Long week) {
return StudyWeekStatisticsResponse.empty(week, true, true);
}

public static StudyWeekStatisticsResponse assignmentCanceled(Long week, Long attendanceRate) {
return new StudyWeekStatisticsResponse(week, attendanceRate, 0L, true, false);
}
}

0 comments on commit 482acaf

Please sign in to comment.