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

Conversation

kckc0608
Copy link
Member

@kckc0608 kckc0608 commented Oct 15, 2024

🌱 관련 이슈

📌 작업 내용 및 특이사항

  • 스터디 통계 조회 API 작성

📝 참고사항

  • 수료율의 경우, 스터디 수료 여부 필드를 이용하여 계산하였습니다.

  • 스터디 평균 출석률과 과제 제출률은 각 주차별 출석률, 과제 제출률의 계산 결과를 평균하였습니다.

  • 각 주차별 과제 제출률은 제출에 성공한 과제만 평균냈습니다.

  • 평균을 낼 때 휴강주차는 평균 계산에서 제외하였습니다.

  • 피그마에 화면 기획을 보면 '수료 스터디 인원 수' 와 '수료 가능한 스터디 인원 수' 정보가 필요한데, 혁님이 '수료 가능한 스터디 인원 수'는 스터디 전체 인원으로 고려하면 된다고 하셔서 response에 별도 필드를 만들지 않았습니다.

📚 기타

Summary by CodeRabbit

  • 새로운 기능
    • 연구 통계를 조회할 수 있는 새로운 API 엔드포인트 추가.
    • 연구 세션에 대한 통계 정보를 포함하는 StudyStatisticsResponseStudyWeekStatisticsResponse 데이터 구조 도입.
    • 주간 통계 계산을 위한 새로운 메소드 추가.
    • 출석 및 과제 제출 상태를 기반으로 한 통계 계산 기능 추가.
    • 특정 연구 세부사항에 대한 출석 및 과제 기록 수를 계산하는 새로운 메소드 추가.
    • 연구 이력의 완료 상태를 확인하는 isCompleted() 메소드 추가.
  • 버그 수정
    • 연구 이력의 완료 상태를 확인하는 isCompleted() 메소드 추가.

@kckc0608 kckc0608 added ✨ feature 새로운 기능 추가 및 수정 ⚓ Domain: Study 스터디 관련 작업 labels Oct 15, 2024
@kckc0608 kckc0608 self-assigned this Oct 15, 2024
@kckc0608 kckc0608 requested a review from a team as a code owner October 15, 2024 12:26
Copy link

coderabbitai bot commented Oct 15, 2024

Walkthrough

이 PR에서는 MentorStudyDetailService 클래스에 스터디 통계를 조회할 수 있는 기능을 추가했습니다. getStudyStatistics(Long studyId) 메서드는 멘토의 역할을 검증하고, 총 학생 수 및 완료된 학생 수를 세며, 평균 출석률과 과제 제출률 등의 통계를 계산합니다. 여러 레포지토리와 DTO 클래스가 수정되어 통계 계산을 위한 새로운 메서드들이 추가되었습니다.

Changes

파일 경로 변경 요약
src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java getStudyStatistics 메서드 추가 및 통계 계산을 위한 여러 메서드 추가
src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryRepository.java countByStudyDetailIdAndSubmissionStatusEquals 메서드 추가
src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java countByStudyDetailId 메서드 추가
src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java isCompleted 메서드 추가
src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStatisticsResponse.java 새로운 DTO 클래스 StudyStatisticsResponse 추가
src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyWeekStatisticsResponse.java 새로운 DTO 클래스 StudyWeekStatisticsResponse 추가

Assessment against linked issues

Objective Addressed Explanation
스터디 통계 조회 API 추가 (#786)

Possibly related PRs

Suggested reviewers

  • Sangwook02
  • AlmondBreez3
  • uwoobeat

🐇 통계가 쏙쏙,
새로 생긴 길로,
스터디의 진실,
숫자로 가득해,
모두 함께 나누자,
Rabbit의 기쁨이야! 🌟


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link
Member

@Sangwook02 Sangwook02 left a comment

Choose a reason for hiding this comment

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

확인했습니다~

Copy link

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (7)
src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java (1)

9-9: 새로운 메서드가 올바르게 추가되었습니다.

새로 추가된 countByStudyDetailId 메서드는 Spring Data JPA 명명 규칙을 잘 따르고 있으며, PR의 목적인 스터디 통계 조회 기능 구현에 적절히 기여할 것으로 보입니다.

메서드의 목적을 명확히 하기 위해 다음과 같은 주석을 추가하는 것이 좋을 것 같습니다:

/**
 * 특정 스터디 세부 정보에 대한 출석 기록 수를 조회합니다.
 * @param studyDetailId 조회할 스터디 세부 정보의 ID
 * @return 해당 스터디 세부 정보에 대한 출석 기록 수
 */
long countByStudyDetailId(Long studyDetailId);
src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryRepository.java (1)

14-14: 새로운 메서드가 PR 목표에 부합하게 추가되었습니다.

countByStudyDetailIdAndSubmissionStatusEquals 메서드는 Spring Data JPA 쿼리 메서드 명명 규칙을 따르고 있으며, 스터디 통계 조회 API의 요구사항을 충족시키는 것으로 보입니다. 이 메서드를 통해 특정 스터디의 과제 제출 상태에 따른 통계를 계산할 수 있을 것입니다.

다만, 메서드 이름이 다소 길어 가독성이 떨어질 수 있습니다. 필요하다면 @Query 어노테이션을 사용하여 메서드 이름을 간소화하는 것을 고려해 보시기 바랍니다.

예를 들어, 다음과 같이 변경할 수 있습니다:

@Query("SELECT COUNT(ah) FROM AssignmentHistory ah WHERE ah.studyDetail.id = :studyDetailId AND ah.submissionStatus = :status")
long countAssignmentsByStudyAndStatus(@Param("studyDetailId") Long studyDetailId, @Param("status") AssignmentSubmissionStatus status);

이렇게 하면 메서드 이름이 더 간결해지고 쿼리의 의도가 명확해집니다.

src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyWeekStatisticsResponse.java (1)

11-14: 메서드 이름을 'of'로 변경하는 것이 어떨까요?

이전 리뷰 코멘트와 프로젝트의 네이밍 컨벤션을 고려할 때, openedWeekStatisticsOf 메서드의 이름을 of로 변경하는 것이 좋을 것 같습니다. 이렇게 하면 다른 DTO들과 일관성을 유지할 수 있습니다.

다음과 같이 변경을 제안합니다:

-    public static StudyWeekStatisticsResponse openedWeekStatisticsOf(
+    public static StudyWeekStatisticsResponse of(
         Long studyWeek, Long attendanceRate, Long assignmentSubmitRate) {
     return new StudyWeekStatisticsResponse(studyWeek, attendanceRate, assignmentSubmitRate, false);
 }
src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStatisticsResponse.java (1)

6-12: 레코드 선언이 잘 구성되어 있습니다.

레코드 사용과 Swagger 어노테이션 적용이 적절합니다. 필드 이름과 타입도 목적에 맞게 잘 선택되었습니다.

studyCompleteRate 필드는 팩토리 메서드에서 계산되므로, 이를 명확히 하기 위해 주석을 추가하는 것이 좋을 것 같습니다. 예를 들어:

@Schema(description = "스터디 수료율 (팩토리 메서드에서 계산됨)") Long studyCompleteRate,
src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java (1)

85-88: LGTM! 메서드 구현이 적절합니다.

isComplete() 메서드의 구현이 간결하고 명확합니다. 기존의 complete() 메서드와 잘 연계되어 있습니다.

가독성을 더욱 높이기 위해 다음과 같이 간단한 JavaDoc 주석을 추가하는 것을 고려해 보세요:

/**
 * 스터디 이력의 완료 상태를 반환합니다.
 * @return 스터디가 완료되었으면 true, 그렇지 않으면 false
 */
public boolean isComplete() {
    return studyHistoryStatus == COMPLETED;
}
src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyDetailController.java (1)

85-90: LGTM: 새로운 메서드가 잘 구현되었습니다. 작은 개선 사항이 있습니다.

새로운 getStudyStatistics 메서드가 잘 구현되었습니다. Swagger 문서화를 위한 @Operation 어노테이션, HTTP GET 요청을 위한 @GetMapping 어노테이션, 그리고 적절한 메서드 시그니처를 사용하고 있습니다. 서비스 계층에 작업을 위임하는 것도 좋은 방식입니다.

작은 개선 사항:

  • 일관성을 위해 다른 메서드들과 마찬가지로 ResponseEntity.ok().body(response)를 사용하는 것이 좋습니다.

다음과 같이 수정하는 것을 고려해보세요:

@GetMapping("/statistics")
public ResponseEntity<StudyStatisticsResponse> getStudyStatistics(@RequestParam(name = "studyId") Long studyId) {
    StudyStatisticsResponse response = mentorStudyDetailService.getStudyStatistics(studyId);
-   return ResponseEntity.ok(response);
+   return ResponseEntity.ok().body(response);
}
src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java (1)

129-157: 단위 테스트 추가 권장

새로 추가된 getStudyStatistics 메서드 및 관련 계산 메서드들에 대한 단위 테스트를 작성하여 기능의 정확성을 검증하는 것이 좋습니다. 이는 이후 유지보수와 기능 변경 시 오류를 사전에 방지하는 데 도움이 됩니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 014cf8c and 21909c7.

📒 Files selected for processing (7)
  • src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyDetailController.java (2 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java (3 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryRepository.java (1 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java (1 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java (1 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStatisticsResponse.java (1 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyWeekStatisticsResponse.java (1 hunks)
🧰 Additional context used
🔇 Additional comments (4)
src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryRepository.java (1)

5-5: 새로운 import 문이 올바르게 추가되었습니다.

AssignmentSubmissionStatus를 import하는 것은 새로 추가된 메서드에 필요하며, 적절한 위치에 배치되었습니다.

src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyWeekStatisticsResponse.java (1)

5-9: LGTM: 레코드 선언과 필드가 적절합니다.

레코드 선언과 필드 구성이 잘 되어 있습니다. @Schema 어노테이션을 사용하여 API 문서화를 위한 설명을 추가한 것이 좋습니다. 필드 이름과 타입도 목적에 맞게 적절히 선택되었습니다.

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

8-8: LGTM: 새로운 import 문이 올바르게 추가되었습니다.

새로운 StudyStatisticsResponse 클래스의 import 문이 적절하게 추가되었습니다. 이는 새로 추가된 메서드의 반환 타입과 일치하며, 파일의 기존 import 스타일을 따르고 있습니다.

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

135-142: [기존 리뷰 반영 확인]

이미 이전 리뷰에서 studyHistories 조회 로직을 코드 상단으로 이동하는 것에 대한 의견이 있었고, 이에 대해 고민하신 것으로 보입니다. 해당 부분에 대한 수정 여부를 한 번 더 검토해 주시기 바랍니다.

Copy link

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

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 처리해두었습니다

Copy link

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between c16cc3d and 5d665a0.

📒 Files selected for processing (3)
  • src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java (3 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStatisticsResponse.java (1 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyWeekStatisticsResponse.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStatisticsResponse.java
🧰 Additional context used
🔇 Additional comments (5)
src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyWeekStatisticsResponse.java (3)

5-10: 레코드 선언이 잘 구성되어 있습니다.

레코드 구조가 적절하며 각 필드에 대한 @Schema 어노테이션을 통해 API 문서화가 잘 되어 있습니다. 필드 이름과 타입이 목적에 맞게 잘 선택되었습니다.


12-14: openedOf 메서드가 적절히 구현되었습니다.

이 메서드는 열린 스터디 주차에 대한 StudyWeekStatisticsResponse를 생성하며, 로직이 명확하고 간결합니다. 메서드 이름이 이전 리뷰 코멘트에서 제안된 컨벤션을 따르고 있습니다.


16-18: emptyOf 메서드가 잘 구현되었습니다.

이 메서드는 출석률과 과제 제출률을 0으로 설정하면서 과제 휴강과 스터디 휴강을 별도로 지정할 수 있어 유연성이 높습니다. 메서드 이름이 이전 리뷰 코멘트에서 제안된 컨벤션을 따르고 있습니다.

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

3-3: 새로운 의존성 추가가 적절해 보입니다.

새로운 기능을 위해 필요한 리포지토리와 도메인 클래스들이 잘 추가되었습니다. 이러한 변경은 스터디 통계 기능 구현에 필요한 것으로 보입니다.

Also applies to: 7-8, 10-12, 15-16, 21-22, 40-44


180-189: 평균 출석률 계산 로직이 적절해 보입니다.

calculateAverageWeekAttendanceRate 메서드의 구현이 잘 되어 있습니다. 취소된 주를 제외하고 평균을 계산하는 로직이 명확하고 효율적으로 구현되어 있습니다. Java 8의 스트림 API를 잘 활용하고 있어 코드가 간결하고 읽기 쉽습니다.

@kckc0608
Copy link
Member Author

kckc0608 commented Oct 17, 2024

정적 팩토리 메서드 이름으로 과제 휴강도 아니고 스터디 휴강도 아닌 경우 openedOf, 과제 휴강의 경우 canceledAssignmentOf 로 지었는데, 둘 모두 스터디는 휴강이 아님에도 open, cancel 이 나뉘어져서 다르다고 인식할 가능성이 있을 것 같다는 생각도 듭니다.

혹시 다른 좋은 네이밍 아이디어가 있으시면 추천 부탁드립니다!

@uwoobeat
Copy link
Member

uwoobeat commented Oct 17, 2024

정적 팩토리 메서드 이름으로 과제 휴강도 아니고 스터디 휴강도 아닌 경우 openedOf, 과제 휴강의 경우 canceledAssignmentOf 로 지었는데, 둘 모두 스터디는 휴강이 아님에도 open, cancel 이 나뉘어져서 다르다고 인식할 가능성이 있을 것 같다는 생각도 듭니다.

혹시 다른 좋은 네이밍 아이디어가 있으시면 추천 부탁드립니다!

네이밍의 경우 opened, canceled 로 지어도 괜찮을 것 같네요
of 의 경우 기존 컨벤션대로 별도 접두/접미사 붙이지 않고 사용하는게 좋을듯 합니다

studyValidator.validateStudyMentor(currentMember, study);

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 할게요! 감사합니당

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.

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

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

}

private StudyWeekStatisticsResponse calculateWeekStatistics(StudyDetail studyDetail, Long totalStudentCount) {
boolean isCanceledWeek = !studyDetail.getCurriculum().isOpen();
Copy link
Member

Choose a reason for hiding this comment

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

그리고 개인적인 생각인데 '스터다 주차' 라고 하면 수업과 과제 모두를 포괄하는 느낌이라서, 뭔가 좀 어색한 것 같네요. '해당 주차가 취소되었는지' 보다는 '해당 주차 수업 / 과제가 취소되었는지' 로 나타내는게 더 정확하지 않나 싶네요 (cc @Sangwook02 )

Copy link
Member Author

Choose a reason for hiding this comment

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

week 가 포괄적인 느낌이라는데 공감합니다. 변수명 고민을 좀 더 해봐야겠네요..

Copy link
Member

@uwoobeat uwoobeat left a comment

Choose a reason for hiding this comment

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

요렇게 하면 어떨까 싶네요

return StudyWeekStatisticsResponse.emptyOf(week, true, true);
}

public static StudyWeekStatisticsResponse canceledAssignmentOf(Long week, Long attendanceRate) {
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 static StudyWeekStatisticsResponse canceledAssignmentOf(Long week, Long attendanceRate) {
public static StudyWeekStatisticsResponse assignmentCanceled(Long week, Long attendanceRate) {

Copy link
Member Author

Choose a reason for hiding this comment

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

of, from 컨벤션을 지키면서 케이스가 나뉘는 경우 어떻게 처리해야 하나 궁금했는데, 자세하게 알려주셔서 감사합니다~!

Copy link
Member

@uwoobeat uwoobeat Oct 17, 2024

Choose a reason for hiding this comment

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

이전 컨벤션은 create 접두사를 붙이는 쪽이었는데, 이번에 DTO / VO 쪽에서는 접두사 없이 가면서 조금 어색한 감이 있을 수는 있어요.
일단 쭉 써보면서 계속 논의해봅시다~

Copy link
Member

@Sangwook02 Sangwook02 left a comment

Choose a reason for hiding this comment

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

확인했습니다~

}

private StudyWeekStatisticsResponse calculateWeekStatistics(StudyDetail studyDetail, Long totalStudentCount) {
boolean isCanceledWeek = !studyDetail.getCurriculum().isOpen();
Copy link
Member

Choose a reason for hiding this comment

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

isCanceled 메서드가 있는데 isOpen을 쓰신 이유가 있나요?

Copy link
Member Author

Choose a reason for hiding this comment

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

저도 그걸로 고민을 했었는데, 지금 존재하는 상태가 open, cancel, none 이렇게 3가지가 있어서, isCanceled 를 사용하면 none 으로 설정된 studyDetail 도 열린 주차로 인식하는 문제가 발생할 것 같았어요. 그래서 cancel, none 타입인 경우 모두 canceled week 로 간주하도록 했습니다.

Copy link
Member

Choose a reason for hiding this comment

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

아 open이 아닌지만 확인하면 되는거였군요
그럼 변수명을 수정하면 좋을 것 같아요!

return StudyWeekStatisticsResponse.canceledAssignmentOf(studyDetail.getWeek(), attendanceRate);
}

long assignmentCount =
Copy link
Member

Choose a reason for hiding this comment

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

제출 성공인 과제의 수를 count하는 것 같은데 변수명을 보고는 알기 어려운 것 같습니다!

Copy link

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Outside diff range and nitpick comments (1)
src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStatisticsResponse.java (1)

6-12: Schema 문서화가 잘 되어있으나, 필드 값의 범위에 대한 설명이 필요합니다.

각 필드에 대한 기본적인 설명은 잘 되어있습니다. 하지만 통계 데이터의 특성상 다음과 같은 추가 정보가 있으면 좋을 것 같습니다:

  • 비율 필드들(averageAttendanceRate, averageAssignmentSubmissionRate, studyCompleteRate)의 값 범위 (0-100)
  • 각 카운트 필드들의 제약 조건 (0 이상의 정수)

다음과 같이 Schema 설명을 보완하는 것을 제안드립니다:

-@Schema(description = "평균 출석률") Long averageAttendanceRate,
+@Schema(description = "평균 출석률 (0-100 범위의 백분율)") Long averageAttendanceRate,
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 5d665a0 and f9051e4.

📒 Files selected for processing (3)
  • src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java (3 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStatisticsResponse.java (1 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyWeekStatisticsResponse.java (1 hunks)
🔇 Additional comments (3)
src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyWeekStatisticsResponse.java (1)

5-10: 레코드 구조가 명확하고 잘 설계되었습니다!

각 필드의 역할이 명확하게 구분되어 있으며, @Schema 어노테이션을 통한 문서화가 잘 되어있습니다. 특히 과제 휴강과 스터디 휴강을 분리한 것이 좋은 설계 결정이었습니다.

src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStatisticsResponse.java (1)

1-28: 🛠️ Refactor suggestion

불변성 보장을 위한 방어적 복사 검토가 필요합니다.

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);
+            List.copyOf(studyWeekStatisticsResponses));
 }
src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java (1)

128-153: getStudyStatistics 메서드의 구현이 훌륭합니다

getStudyStatistics 메서드는 필요한 통계를 효과적으로 계산하고 있습니다. 전체적인 로직과 데이터 조회 방법이 명확하고 읽기 쉽습니다.

Comment on lines 12 to 26
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 isCanceledAssignment, boolean isCanceledWeek) {
return new StudyWeekStatisticsResponse(week, 0L, 0L, isCanceledAssignment, isCanceledWeek);
}

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);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

매개변수 유효성 검증 추가 제안

각 정적 팩토리 메서드에 대해 매개변수 유효성 검증을 추가하면 좋을 것 같습니다:

  1. week가 null이거나 음수인 경우
  2. attendanceRateassignmentSubmissionRate가 0-100 범위를 벗어나는 경우

다음과 같은 변경을 제안합니다:

 public static StudyWeekStatisticsResponse opened(Long week, Long attendanceRate, Long assignmentSubmissionRate) {
+    validateWeek(week);
+    validateRate(attendanceRate, "출석률");
+    validateRate(assignmentSubmissionRate, "과제 제출률");
     return new StudyWeekStatisticsResponse(week, attendanceRate, assignmentSubmissionRate, false, false);
 }

+private static void validateWeek(Long week) {
+    if (week == null || week < 1) {
+        throw new IllegalArgumentException("주차는 1 이상의 값이어야 합니다.");
+    }
+}
+
+private static void validateRate(Long rate, String fieldName) {
+    if (rate == null || rate < 0 || rate > 100) {
+        throw new IllegalArgumentException(fieldName + "은(는) 0에서 100 사이의 값이어야 합니다.");
+    }
+}

Committable suggestion was skipped due to low confidence.

Comment on lines +14 to +27
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);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

팩토리 메서드의 안전성 개선이 필요합니다.

현재 구현에서 다음과 같은 잠재적인 문제가 있습니다:

  1. completeStudentCount가 null일 경우 NPE 발생 가능
  2. completeStudentCounttotalStudentCount보다 큰 경우에 대한 검증 부재

다음과 같은 개선을 제안드립니다:

 public static StudyStatisticsResponse of(
         Long totalStudentCount,
         Long completeStudentCount,
         Long averageAttendanceRate,
         Long averageAssignmentSubmissionRate,
         List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses) {
+    long safeTotal = totalStudentCount != null ? totalStudentCount : 0L;
+    long safeComplete = completeStudentCount != null ? completeStudentCount : 0L;
+    
+    if (safeComplete > safeTotal) {
+        throw new IllegalArgumentException("완료 학생 수는 전체 학생 수를 초과할 수 없습니다.");
+    }
+
     return new StudyStatisticsResponse(
-            totalStudentCount,
-            completeStudentCount,
+            safeTotal,
+            safeComplete,
             averageAttendanceRate,
             averageAssignmentSubmissionRate,
-            totalStudentCount == 0 ? 0 : Math.round(completeStudentCount / (double) totalStudentCount * 100),
+            safeTotal == 0 ? 0 : Math.round(safeComplete / (double) safeTotal * 100),
             studyWeekStatisticsResponses);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
}
public static StudyStatisticsResponse of(
Long totalStudentCount,
Long completeStudentCount,
Long averageAttendanceRate,
Long averageAssignmentSubmissionRate,
List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses) {
long safeTotal = totalStudentCount != null ? totalStudentCount : 0L;
long safeComplete = completeStudentCount != null ? completeStudentCount : 0L;
if (safeComplete > safeTotal) {
throw new IllegalArgumentException("완료 학생 수는 전체 학생 수를 초과할 수 없습니다.");
}
return new StudyStatisticsResponse(
safeTotal,
safeComplete,
averageAttendanceRate,
averageAssignmentSubmissionRate,
safeTotal == 0 ? 0 : Math.round(safeComplete / (double) safeTotal * 100),
studyWeekStatisticsResponses);
}

Comment on lines 155 to 179
private StudyWeekStatisticsResponse calculateWeekStatistics(StudyDetail studyDetail, Long totalStudentCount) {
boolean isCanceledWeek = !studyDetail.getCurriculum().isOpen();
boolean isCanceledAssignment = !studyDetail.getAssignment().isOpen() || isCanceledWeek;

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

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

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

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

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

return StudyWeekStatisticsResponse.opened(studyDetail.getWeek(), attendanceRate, assignmentRate);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

calculateWeekStatistics 메서드의 가독성 향상을 위한 리팩토링 제안

현재 calculateWeekStatistics 메서드에서 여러 return 문이 존재하여 흐름을 따라가기 어려울 수 있습니다. 가독성을 높이기 위해 조건 분기를 구조화하거나, 메서드를 분리하여 코드의 명확성을 높이는 것을 고려해보세요.

Comment on lines 156 to 157
boolean isCanceledWeek = !studyDetail.getCurriculum().isOpen();
boolean isCanceledAssignment = !studyDetail.getAssignment().isOpen() || isCanceledWeek;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

NullPointerException 방지를 위한 Null 체크 추가 필요

studyDetail.getCurriculum() 또는 studyDetail.getAssignment()null인 경우 NullPointerException이 발생할 수 있습니다. 이를 방지하기 위해 null 체크를 추가하는 것이 좋습니다.

적용할 코드 변경 사항:

- boolean isCanceledWeek = !studyDetail.getCurriculum().isOpen();
+ boolean isCanceledWeek = studyDetail.getCurriculum() == null || !studyDetail.getCurriculum().isOpen();

- boolean isCanceledAssignment = !studyDetail.getAssignment().isOpen() || isCanceledWeek;
+ boolean isCanceledAssignment = studyDetail.getAssignment() == null || !studyDetail.getAssignment().isOpen() || isCanceledWeek;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
boolean isCanceledWeek = !studyDetail.getCurriculum().isOpen();
boolean isCanceledAssignment = !studyDetail.getAssignment().isOpen() || isCanceledWeek;
boolean isCanceledWeek = studyDetail.getCurriculum() == null || !studyDetail.getCurriculum().isOpen();
boolean isCanceledAssignment = studyDetail.getAssignment() == null || !studyDetail.getAssignment().isOpen() || isCanceledWeek;

Copy link

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between f9051e4 and dc9c54f.

📒 Files selected for processing (1)
  • src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java (3 hunks)

Comment on lines 128 to 153
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::isComplete).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);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

성능 최적화 및 방어 로직 추가 제안

다음과 같은 개선사항을 제안드립니다:

  1. studyHistories가 null일 경우를 대비한 방어 로직이 필요합니다.
  2. 데이터베이스 쿼리 최적화를 위해 studyHistoriesstudyDetails를 한 번의 쿼리로 조회하는 것이 좋습니다.
@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);
    if (studyHistories == null) {
        studyHistories = List.of();
    }

    // TODO: Consider optimizing by fetching studyHistories and studyDetails in a single query
    List<StudyDetail> studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId);
    
    // ... rest of the code
}

Comment on lines 183 to 204
private long calculateAverageWeekAttendanceRate(List<StudyWeekStatisticsResponse> studyWeekStatisticsResponses) {

double averageAttendanceRate = studyWeekStatisticsResponses.stream()
.filter(weekStatisticsResponse -> !weekStatisticsResponse.isCanceledWeek())
.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);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

평균 계산 로직 중복 제거 필요

출석률과 과제 제출률 계산 로직이 매우 유사합니다. 공통 메서드로 추출하여 중복을 제거하는 것이 좋습니다.

private long calculateAverageRate(
        List<StudyWeekStatisticsResponse> responses,
        java.util.function.Predicate<StudyWeekStatisticsResponse> filterCondition,
        java.util.function.ToLongFunction<StudyWeekStatisticsResponse> rateExtractor) {
    
    return Math.round(responses.stream()
            .filter(filterCondition)
            .mapToLong(rateExtractor)
            .average()
            .orElse(0));
}

private long calculateAverageWeekAttendanceRate(List<StudyWeekStatisticsResponse> responses) {
    return calculateAverageRate(
            responses,
            response -> !response.isCanceledWeek(),
            StudyWeekStatisticsResponse::attendanceRate);
}

private long calculateAverageWeekAssignmentSubmissionRate(List<StudyWeekStatisticsResponse> responses) {
    return calculateAverageRate(
            responses,
            response -> !response.isAssignmentCanceled(),
            StudyWeekStatisticsResponse::assignmentSubmissionRate);
}

Comment on lines +155 to +181
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);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Null 체크 추가 및 변수명 개선 필요

  1. getCurriculum()getAssignment()의 null 체크가 필요합니다.
  2. isNotOpenedCurriculumisNotOpenedAssignment 변수명이 부정문을 사용하여 이해하기 어렵습니다.
private StudyWeekStatisticsResponse calculateWeekStatistics(StudyDetail studyDetail, Long totalStudentCount) {
-    boolean isNotOpenedCurriculum = !studyDetail.getCurriculum().isOpen();
-    boolean isNotOpenedAssignment = !studyDetail.getAssignment().isOpen() || isNotOpenedCurriculum;
+    boolean isCurriculumClosed = studyDetail.getCurriculum() == null || !studyDetail.getCurriculum().isOpen();
+    boolean isAssignmentClosed = studyDetail.getAssignment() == null || !studyDetail.getAssignment().isOpen() || isCurriculumClosed;

    if (totalStudentCount == 0) {
        return StudyWeekStatisticsResponse.empty(
-                studyDetail.getWeek(), isNotOpenedAssignment, isNotOpenedCurriculum);
+                studyDetail.getWeek(), isAssignmentClosed, isCurriculumClosed);
    }
    // ... rest of the code with updated variable names
}

Committable suggestion was skipped due to low confidence.

@kckc0608
Copy link
Member Author

kckc0608 commented Oct 31, 2024

canceled week 대신에 사용할 표현으로 class, lecture, course 같은 걸 생각해봤는데, 커리큘럼이 취소된다는 것이 일반적으로 어색하긴 하지만 프로젝트에서 Curriculum 이라는 엔티티를 사용하고 있어서, 여기에 표현에 맞추는 게 가독성에 더 좋을 것 같다고 생각했습니다. 혹시 더 좋은 의견 있으시면 남겨주세요 ㅎㅎ

Copy link

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between dc9c54f and 1fb9722.

📒 Files selected for processing (2)
  • src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java (3 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyWeekStatisticsResponse.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyWeekStatisticsResponse.java
🔇 Additional comments (3)
src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java (3)

Line range hint 3-44: 새로운 의존성 주입이 적절히 구성되었습니다.

필요한 레포지토리들과 validator가 잘 추가되었습니다.


128-153: 🛠️ Refactor suggestion

성능 최적화 및 방어 로직 개선이 필요합니다.

다음과 같은 개선사항을 제안드립니다:

  1. studyHistoriesstudyDetails를 단일 쿼리로 조회하여 데이터베이스 호출을 최적화할 수 있습니다.
  2. studyHistories가 null인 경우를 대비한 방어 로직이 필요합니다.
@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);
+   if (studyHistories == null) {
+       studyHistories = List.of();
+   }

    // TODO: studyHistories와 studyDetails를 단일 쿼리로 조회하는 것을 고려해보세요
    List<StudyDetail> studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId);
    
    // ... 나머지 코드
}

Likely invalid or redundant comment.


155-181: ⚠️ Potential issue

NPE 방지 및 변수명 개선이 필요합니다.

  1. getCurriculum()getAssignment()의 null 체크가 필요합니다.
  2. 부정문을 사용한 변수명(isNotOpenedCurriculum, isNotOpenedAssignment)은 이해하기 어렵습니다.
private StudyWeekStatisticsResponse calculateWeekStatistics(StudyDetail studyDetail, Long totalStudentCount) {
-    boolean isNotOpenedCurriculum = !studyDetail.getCurriculum().isOpen();
-    boolean isNotOpenedAssignment = !studyDetail.getAssignment().isOpen() || isNotOpenedCurriculum;
+    boolean isCurriculumClosed = studyDetail.getCurriculum() == null || !studyDetail.getCurriculum().isOpen();
+    boolean isAssignmentClosed = studyDetail.getAssignment() == null || !studyDetail.getAssignment().isOpen() || isCurriculumClosed;

    if (totalStudentCount == 0) {
        return StudyWeekStatisticsResponse.empty(
-               studyDetail.getWeek(), isNotOpenedAssignment, isNotOpenedCurriculum);
+               studyDetail.getWeek(), isAssignmentClosed, isCurriculumClosed);
    }
    // ... 나머지 코드에서도 변수명 업데이트
}

Likely invalid or redundant comment.

Copy link
Member

@uwoobeat uwoobeat left a comment

Choose a reason for hiding this comment

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

lgtm

Copy link
Member

@Sangwook02 Sangwook02 left a comment

Choose a reason for hiding this comment

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

하나만 확인해주세요!
미리 어푸루브 해두겠습니다~

@@ -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() {

Copy link

github-actions bot commented Nov 1, 2024

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 1fb9722 and 8e79492.

📒 Files selected for processing (2)
  • src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java (3 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java (1 hunks)
🔇 Additional comments (3)
src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java (1)

86-88: 구현이 깔끔하고 명확합니다!

isCompleted() 메서드는 스터디 통계 조회 API에서 수료율을 계산하는데 적절하게 활용될 수 있습니다. 메서드 명이 Java 컨벤션을 잘 따르고 있으며, 구현이 간단명료합니다.

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

Line range hint 3-44: 의존성 주입이 적절히 구성되었습니다!

새로운 통계 기능을 위한 레포지토리들이 잘 구성되어 있습니다.


155-181: ⚠️ Potential issue

Null 안전성 확보 및 변수명 개선이 필요합니다.

  1. getCurriculum()getAssignment()의 null 체크가 누락되었습니다.
  2. isNotOpenedCurriculum과 같은 이중 부정 변수명은 코드 이해를 어렵게 만듭니다.

다음과 같이 수정을 제안드립니다:

-    boolean isNotOpenedCurriculum = !studyDetail.getCurriculum().isOpen();
-    boolean isNotOpenedAssignment = !studyDetail.getAssignment().isOpen() || isNotOpenedCurriculum;
+    boolean isCurriculumClosed = studyDetail.getCurriculum() == null || !studyDetail.getCurriculum().isOpen();
+    boolean isAssignmentClosed = studyDetail.getAssignment() == null || !studyDetail.getAssignment().isOpen() || isCurriculumClosed;

Likely invalid or redundant comment.

Comment on lines +183 to +204
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);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

평균 계산 로직의 중복을 제거해주세요.

출석률과 과제 제출률 계산 로직이 매우 유사합니다. 공통 메서드로 추출하여 코드 중복을 제거하는 것이 좋습니다.

다음과 같은 리팩토링을 제안드립니다:

private long calculateAverageRate(
        List<StudyWeekStatisticsResponse> responses,
        Predicate<StudyWeekStatisticsResponse> filterCondition,
        ToLongFunction<StudyWeekStatisticsResponse> rateExtractor) {
    
    return Math.round(responses.stream()
            .filter(filterCondition)
            .mapToLong(rateExtractor)
            .average()
            .orElse(0));
}

private long calculateAverageWeekAttendanceRate(List<StudyWeekStatisticsResponse> responses) {
    return calculateAverageRate(
            responses,
            response -> !response.isCurriculumCanceled(),
            StudyWeekStatisticsResponse::attendanceRate);
}

private long calculateAverageWeekAssignmentSubmissionRate(List<StudyWeekStatisticsResponse> responses) {
    return calculateAverageRate(
            responses,
            response -> !response.isAssignmentCanceled(),
            StudyWeekStatisticsResponse::assignmentSubmissionRate);
}

Comment on lines +127 to +153
@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);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Repository 조회 결과에 대한 Null 처리가 필요합니다.

studyHistoriesstudyDetails 조회 결과가 null일 경우를 대비한 방어 로직이 필요합니다.

다음과 같이 수정하는 것을 제안드립니다:

-        List<StudyHistory> studyHistories = studyHistoryRepository.findAllByStudyId(studyId);
-        List<StudyDetail> studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId);
+        List<StudyHistory> studyHistories = studyHistoryRepository.findAllByStudyId(studyId);
+        if (studyHistories == null) {
+            studyHistories = List.of();
+        }
+        
+        List<StudyDetail> studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId);
+        if (studyDetails == null) {
+            studyDetails = List.of();
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@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);
}
@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);
if (studyHistories == null) {
studyHistories = List.of();
}
List<StudyDetail> studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId);
if (studyDetails == null) {
studyDetails = List.of();
}
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);
}

@kckc0608 kckc0608 merged commit 482acaf into develop Nov 1, 2024
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
⚓ Domain: Study 스터디 관련 작업 ✨ feature 새로운 기능 추가 및 수정
Projects
None yet
Development

Successfully merging this pull request may close these issues.

✨ 스터디 통계 조회 API 추가
3 participants