Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 스터디 통계 조회 API 추가 #809

Merged
merged 20 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,26 @@
package com.gdschongik.gdsc.domain.study.application;

import static com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus.SUCCESS;
import static com.gdschongik.gdsc.domain.study.dto.response.StudyWeekStatisticsResponse.*;
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 +38,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 +124,89 @@ 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));
studyValidator.validateStudyMentor(currentMember, study);

List<StudyHistory> studyHistories = studyHistoryRepository.findAllByStudyId(studyId);
long totalStudentCount = studyHistories.size();
long studyCompleteStudentCount =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
long studyCompleteStudentCount =
long studyCompletedStudentCount =

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 아직 반영 안 된 것 같은데 resolve되어 있어서 unresolve 해둘게요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 코드 상에 반영해두고 resolve 한 뒤 커밋을 안 했었네요. 앞으론 커밋하고 resolve 할게요! 감사합니당

studyHistories.stream().filter(StudyHistory::isComplete).count();

List<StudyDetail> studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId);
long openedWeekCount = studyDetails.stream()
.filter(studyDetail -> studyDetail.getCurriculum().isOpen())
.count();
kckc0608 marked this conversation as resolved.
Show resolved Hide resolved

List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses =
calculateStudyWeekStatistics(studyDetails, totalStudentCount);

long averageAttendanceRate = calculateAverageWeekAttendanceRate(studyWeekStatisticsResponses, openedWeekCount);
long averageAssignmentSubmitRate =
calculateAverageWeekAssignmentSubmitRate(studyWeekStatisticsResponses, openedWeekCount);

return StudyStatisticsResponse.of(
totalStudentCount,
studyCompleteStudentCount,
averageAttendanceRate,
averageAssignmentSubmitRate,
studyWeekStatisticsResponses);
}

private List<StudyWeekStatisticsResponse> calculateStudyWeekStatistics(
List<StudyDetail> studyDetails, Long totalStudentCount) {

return studyDetails.stream()
.map((studyDetail -> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map 내부 람다식이 복잡한데, 별도 메서드로 추출하면 좋을듯 합니다

Copy link
Member Author

@kckc0608 kckc0608 Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선 DTO 정적 팩토리 메서드 종류를 나눠서 케이스마다 반환하도록 했는데, 지금 생각하기에는 내부 로직에서 메서드로 더 분리할 만한 부분은 평균 출석률 계산, 평균 과제 제출률 계산 로직 정도가 있을 것 같습니다.

개인적으로는 각 계산 로직의 길이가 2줄로 길지 않고, 변수명을 통해 코드의 의도를 이해할 수 있을 것 같아서 메서드로 분리하지 않고 놔두었는데, 의도하신 리뷰에 맞는지 확인 부탁드립니다!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 람다식을 extract해서, 스트림 내부를 method reference로 처리하면 더 깔끔할 것 같다는 이야기었어요. 제가 설명을 제대로 못했네요

++ 후속 리뷰 필요한 경우 resolve 처리하지 마시고 열어두시면 제가 리뷰 확인할 때 더 편할 것 같아요~ 이번엔 제가 unresolve 처리해두었습니다

if (!studyDetail.getCurriculum().isOpen()) {
return canceledWeekStatisticsFrom(studyDetail.getWeek());
kckc0608 marked this conversation as resolved.
Show resolved Hide resolved
}

if (totalStudentCount == 0) {
return openedWeekStatisticsOf(studyDetail.getWeek(), 0L, 0L);
}

long attendanceCount = attendanceRepository.countByStudyDetailId(studyDetail.getId());
long assignmentCount = assignmentHistoryRepository.countByStudyDetailIdAndSubmissionStatusEquals(
studyDetail.getId(), SUCCESS);

return openedWeekStatisticsOf(
studyDetail.getWeek(),
Math.round(attendanceCount / (double) totalStudentCount * 100),
Math.round(assignmentCount / (double) totalStudentCount * 100));
}))
.toList();
}

private long calculateAverageWeekAttendanceRate(
List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses, long openedWeekCount) {

if (openedWeekCount == 0) {
return 0;
}

long attendanceRateSum = studyWeekStatisticsResponses.stream()
.mapToLong(weekStatistics -> weekStatistics.isCanceledWeek() ? 0 : weekStatistics.attendanceRate())
.sum();

return Math.round(attendanceRateSum / (double) openedWeekCount);
}

private long calculateAverageWeekAssignmentSubmitRate(
List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses, long openedWeekCount) {

if (openedWeekCount == 0) {
return 0;
}

long assignmentSubmitRateSum = studyWeekStatisticsResponses.stream()
.mapToLong(
weekStatistics -> weekStatistics.isCanceledWeek() ? 0 : weekStatistics.assignmentSubmitRate())
.sum();

return Math.round(assignmentSubmitRateSum / (double) openedWeekCount);
}
kckc0608 marked this conversation as resolved.
Show resolved Hide resolved
}
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 isComplete() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public boolean isComplete() {
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 averageAssignmentSubmitRate,
@Schema(description = "스터디 수료율") Long studyCompleteRate,
@Schema(description = "주차별 출석률 및 과제 제출률") List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses) {

public static StudyStatisticsResponse of(
Long totalStudentCount,
Long completeStudentCount,
Long averageAttendanceRate,
Long averageAssignmentSubmitRate,
List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses) {
return new StudyStatisticsResponse(
totalStudentCount,
completeStudentCount,
averageAttendanceRate,
averageAssignmentSubmitRate,
Math.round(completeStudentCount / (double) totalStudentCount * 100),
studyWeekStatisticsResponses);
}
kckc0608 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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 assignmentSubmitRate,
@Schema(description = "휴강 여부") boolean isCanceledWeek) {

public static StudyWeekStatisticsResponse openedWeekStatisticsOf(
Long studyWeek, Long attendanceRate, Long assignmentSubmitRate) {
return new StudyWeekStatisticsResponse(studyWeek, attendanceRate, assignmentSubmitRate, false);
}

public static StudyWeekStatisticsResponse canceledWeekStatisticsFrom(Long studyWeek) {
return new StudyWeekStatisticsResponse(studyWeek, 0L, 0L, true);
}
kckc0608 marked this conversation as resolved.
Show resolved Hide resolved
}