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 추가 #796

Merged
merged 17 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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 @@ -10,6 +10,8 @@
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

Expand All @@ -35,10 +37,10 @@ public ResponseEntity<List<StudyResponse>> getStudiesInCharge() {
return ResponseEntity.ok(response);
}

@Operation(summary = "스터디 수강생 명단 조회", description = "해당 스터디의 수강생 명단을 조회합니다")
@Operation(summary = "스터디 수강생 관리", description = "해당 스터디의 수강생을 관리합니다")
@GetMapping("/{studyId}/students")
public ResponseEntity<List<StudyStudentResponse>> getStudyStudents(@PathVariable Long studyId) {
List<StudyStudentResponse> response = mentorStudyService.getStudyStudents(studyId);
public ResponseEntity<Page<StudyStudentResponse>> getStudyStudents(@PathVariable Long studyId, Pageable pageable) {
Page<StudyStudentResponse> response = mentorStudyService.getStudyStudents(studyId, pageable);
return ResponseEntity.ok(response);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.gdschongik.gdsc.domain.study.application;

import static com.gdschongik.gdsc.global.exception.ErrorCode.STUDY_NOT_FOUND;
import static com.gdschongik.gdsc.global.exception.ErrorCode.*;
import static java.util.stream.Collectors.*;

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.StudyAchievementRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyAnnouncementRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyDetailRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository;
Expand All @@ -17,14 +21,19 @@
import com.gdschongik.gdsc.domain.study.dto.request.StudyUpdateRequest;
import com.gdschongik.gdsc.domain.study.dto.response.StudyResponse;
import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentResponse;
import com.gdschongik.gdsc.domain.study.dto.response.StudyTodoResponse;
import com.gdschongik.gdsc.global.exception.CustomException;
import com.gdschongik.gdsc.global.exception.ErrorCode;
import com.gdschongik.gdsc.global.util.MemberUtil;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -34,12 +43,15 @@
public class MentorStudyService {

private final MemberUtil memberUtil;
private final StudyValidator studyValidator;
private final StudyDetailValidator studyDetailValidator;
private final StudyRepository studyRepository;
private final StudyAnnouncementRepository studyAnnouncementRepository;
private final StudyHistoryRepository studyHistoryRepository;
private final StudyValidator studyValidator;
private final StudyDetailRepository studyDetailRepository;
private final StudyDetailValidator studyDetailValidator;
private final StudyAchievementRepository studyAchievementRepository;
private final AttendanceRepository attendanceRepository;
private final AssignmentHistoryRepository assignmentHistoryRepository;

@Transactional(readOnly = true)
public List<StudyResponse> getStudiesInCharge() {
Expand All @@ -49,15 +61,67 @@ public List<StudyResponse> getStudiesInCharge() {
}

@Transactional(readOnly = true)
public List<StudyStudentResponse> getStudyStudents(Long studyId) {
public Page<StudyStudentResponse> getStudyStudents(Long studyId, Pageable pageable) {
Member currentMember = memberUtil.getCurrentMember();
Study study =
studyRepository.findById(studyId).orElseThrow(() -> new CustomException(ErrorCode.STUDY_NOT_FOUND));

Study study = studyRepository.findById(studyId).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND));
studyValidator.validateStudyMentor(currentMember, study);
List<StudyHistory> studyHistories = studyHistoryRepository.findByStudyId(studyId);

return studyHistories.stream().map(StudyStudentResponse::from).toList();
List<StudyDetail> studyDetails = studyDetailRepository.findAllByStudyId(studyId);
Page<StudyHistory> studyHistories = studyHistoryRepository.findByStudyId(studyId, pageable);
List<Long> studentIds = studyHistories.getContent().stream()
.map(studyHistory -> studyHistory.getStudent().getId())
.toList();
List<StudyAchievement> studyAchievements =
studyAchievementRepository.findByStudyIdAndMemberIds(studyId, studentIds);
List<Attendance> attendances = attendanceRepository.findByStudyIdAndMemberIds(studyId, studentIds);
List<AssignmentHistory> assignmentHistories =
assignmentHistoryRepository.findByStudyIdAndMemberIds(studyId, studentIds);

// StudyAchievement, Attendance, AssignmentHistory에 대해 Member의 id를 key로 하는 Map 생성
Map<Long, List<StudyAchievement>> studyAchievementMap = studyAchievements.stream()
.collect(groupingBy(
studyAchievement -> studyAchievement.getStudent().getId()));
Map<Long, List<Attendance>> attendanceMap = attendances.stream()
.collect(groupingBy(attendance -> attendance.getStudent().getId()));
Map<Long, List<AssignmentHistory>> assignmentHistoryMap = assignmentHistories.stream()
.collect(groupingBy(
assignmentHistory -> assignmentHistory.getMember().getId()));

List<StudyStudentResponse> response = new ArrayList<>();
studyHistories.getContent().forEach(studyHistory -> {
List<StudyAchievement> currentStudyAchievements =
studyAchievementMap.getOrDefault(studyHistory.getStudent().getId(), new ArrayList<>());
List<Attendance> currentAttendances =
attendanceMap.getOrDefault(studyHistory.getStudent().getId(), new ArrayList<>());
List<AssignmentHistory> currentAssignmentHistories =
assignmentHistoryMap.getOrDefault(studyHistory.getStudent().getId(), new ArrayList<>());

List<StudyTodoResponse> studyTodos = new ArrayList<>();
studyDetails.forEach(studyDetail -> {
studyTodos.add(StudyTodoResponse.createAttendanceType(
studyDetail, LocalDate.now(), isAttended(currentAttendances, studyDetail)));
studyTodos.add(StudyTodoResponse.createAssignmentType(
studyDetail, getSubmittedAssignment(currentAssignmentHistories, studyDetail)));
});

response.add(StudyStudentResponse.of(studyHistory, currentStudyAchievements, studyTodos));
});

return new PageImpl<>(response, pageable, studyHistories.getTotalElements());
}

private boolean isAttended(List<Attendance> attendances, StudyDetail studyDetail) {
return attendances.stream()
.anyMatch(attendance -> attendance.getStudyDetail().getId().equals(studyDetail.getId()));
}

private AssignmentHistory getSubmittedAssignment(
List<AssignmentHistory> assignmentHistories, StudyDetail studyDetail) {
return assignmentHistories.stream()
.filter(assignmentHistory ->
assignmentHistory.getStudyDetail().getId().equals(studyDetail.getId()))
.findFirst()
.orElse(null);
Comment on lines +118 to +124
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

getSubmittedAssignment 메서드 개선 제안

getSubmittedAssignment 메서드의 구조는 좋지만, null 체크가 부족하여 NullPointerException이 발생할 수 있습니다.

다음과 같이 null 체크를 추가하고 최적화하는 것을 제안합니다:

private AssignmentHistory getSubmittedAssignment(
        List<AssignmentHistory> assignmentHistories, StudyDetail studyDetail) {
    return assignmentHistories != null && studyDetail != null ? assignmentHistories.stream()
            .filter(assignmentHistory ->
                    assignmentHistory.getStudyDetail() != null &&
                    assignmentHistory.getStudyDetail().getId().equals(studyDetail.getId()))
            .findFirst()
            .orElse(null)
            : null;
}

이렇게 하면 NullPointerException을 방지하고 메서드의 안정성을 높일 수 있습니다.

}

@Transactional
Expand Down Expand Up @@ -109,12 +173,12 @@ public void updateStudy(Long studyId, StudyUpdateRequest request) {

List<StudyDetail> studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId);
// StudyDetail ID를 추출하여 Set으로 저장
Set<Long> studyDetailIds = studyDetails.stream().map(StudyDetail::getId).collect(Collectors.toSet());
Set<Long> studyDetailIds = studyDetails.stream().map(StudyDetail::getId).collect(toSet());

// 요청된 StudyCurriculumCreateRequest의 StudyDetail ID를 추출하여 Set으로 저장
Set<Long> requestIds = request.studyCurriculums().stream()
.map(StudyCurriculumCreateRequest::studyDetailId)
.collect(Collectors.toSet());
.collect(toSet());

studyDetailValidator.validateUpdateStudyDetail(studyDetailIds, requestIds);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ public interface AssignmentHistoryCustomRepository {

List<AssignmentHistory> findAssignmentHistoriesByStudentAndStudyId(Member member, Long studyId);

List<AssignmentHistory> findByStudyIdAndMemberIds(Long studyId, List<Long> memberIds);

void deleteByStudyIdAndMemberId(Long studyId, Long memberId);
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
package com.gdschongik.gdsc.domain.study.dao;

import static com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus.*;
import static com.gdschongik.gdsc.domain.study.domain.QAssignmentHistory.*;
import static com.gdschongik.gdsc.domain.study.domain.QStudyDetail.studyDetail;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory;
import com.gdschongik.gdsc.domain.study.domain.Study;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class AssignmentHistoryCustomRepositoryImpl implements AssignmentHistoryCustomRepository {
public class AssignmentHistoryCustomRepositoryImpl
implements AssignmentHistoryCustomRepository, AssignmentHistoryQueryMethod {

private final JPAQueryFactory queryFactory;

Expand All @@ -28,18 +27,6 @@ public boolean existsSubmittedAssignmentByMemberAndStudy(Member member, Study st
return fetchOne != null;
}

private BooleanExpression eqMember(Member member) {
return member == null ? null : assignmentHistory.member.eq(member);
}

private BooleanExpression eqStudy(Study study) {
return study == null ? null : assignmentHistory.studyDetail.study.eq(study);
}

private BooleanExpression isSubmitted() {
return assignmentHistory.submissionStatus.in(FAILURE, SUCCESS);
}

@Override
public List<AssignmentHistory> findAssignmentHistoriesByStudentAndStudyId(Member currentMember, Long studyId) {
return queryFactory
Expand All @@ -50,10 +37,6 @@ public List<AssignmentHistory> findAssignmentHistoriesByStudentAndStudyId(Member
.fetch();
}

private BooleanExpression eqStudyId(Long studyId) {
return studyId != null ? studyDetail.study.id.eq(studyId) : null;
}

@Override
public void deleteByStudyIdAndMemberId(Long studyId, Long memberId) {
queryFactory
Expand All @@ -62,7 +45,13 @@ public void deleteByStudyIdAndMemberId(Long studyId, Long memberId) {
.execute();
}

private BooleanExpression eqMemberId(Long memberId) {
return memberId != null ? assignmentHistory.member.id.eq(memberId) : null;
@Override
public List<AssignmentHistory> findByStudyIdAndMemberIds(Long studyId, List<Long> memberIds) {
return queryFactory
.selectFrom(assignmentHistory)
.innerJoin(assignmentHistory.studyDetail, studyDetail)
.fetchJoin()
.where(assignmentHistory.member.id.in(memberIds), eqStudyId(studyId))
.fetch();
Comment on lines +48 to +55
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

memberIds에 대한 null 또는 빈 리스트 처리 필요

현재 메서드 findByStudyIdAndMemberIds에서 memberIdsnull이거나 빈 리스트일 경우, 쿼리에서 예기치 않은 결과가 발생할 수 있습니다. memberIds에 대한 null 및 빈 리스트 체크를 추가하여 예외를 방지하고 적절한 결과를 반환하도록 수정하는 것이 좋습니다.

다음과 같이 수정할 수 있습니다:

if (memberIds == null || memberIds.isEmpty()) {
    return Collections.emptyList();
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.gdschongik.gdsc.domain.study.dao;

import static com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus.*;
import static com.gdschongik.gdsc.domain.study.domain.QAssignmentHistory.*;
import static com.gdschongik.gdsc.domain.study.domain.QStudyDetail.*;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.study.domain.Study;
import com.querydsl.core.types.dsl.BooleanExpression;

public interface AssignmentHistoryQueryMethod {
default BooleanExpression eqMember(Member member) {
return member == null ? null : assignmentHistory.member.eq(member);
}

default BooleanExpression eqStudy(Study study) {
return study == null ? null : assignmentHistory.studyDetail.study.eq(study);
}

default BooleanExpression isSubmitted() {
return assignmentHistory.submissionStatus.in(FAILURE, SUCCESS);
}

default BooleanExpression eqStudyId(Long studyId) {
return studyId != null ? studyDetail.study.id.eq(studyId) : null;
}
Sangwook02 marked this conversation as resolved.
Show resolved Hide resolved

default BooleanExpression eqMemberId(Long memberId) {
return memberId != null ? assignmentHistory.member.id.eq(memberId) : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@

public interface AssignmentHistoryRepository
extends JpaRepository<AssignmentHistory, Long>, AssignmentHistoryCustomRepository {
// todo: public 제거
public Optional<AssignmentHistory> findByMemberAndStudyDetail(Member member, StudyDetail studyDetail);
Comment on lines +11 to 12
Copy link
Member

@kckc0608 kckc0608 Oct 6, 2024

Choose a reason for hiding this comment

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

로직에 영향을 주는 변경점이 아니라면 개발하면서 고쳐도 괜찮을 것 같다고 생각했는데, 이런 부분은 보통 다른 이슈에서 처리하나요?

Copy link
Member Author

Choose a reason for hiding this comment

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

그냥 한 이슈에서 처리해도 되는데 public 일부러 붙여두신건가 싶어서 별도 이슈로 분리했어요.
한 이슈에서 하면 pr에서 묻힐수도 있을 것 같아서

Copy link
Member

Choose a reason for hiding this comment

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

제거해야합니다 굿

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
public interface AttendanceCustomRepository {
List<Attendance> findByMemberAndStudyId(Member member, Long studyId);

List<Attendance> findByStudyIdAndMemberIds(Long studyId, List<Long> memberIds);

void deleteByStudyIdAndMemberId(Long studyId, Long memberId);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.gdschongik.gdsc.domain.study.dao;

import static com.gdschongik.gdsc.domain.member.domain.QMember.member;
import static com.gdschongik.gdsc.domain.study.domain.QAttendance.attendance;
import static com.gdschongik.gdsc.domain.study.domain.QStudyDetail.studyDetail;

Expand All @@ -26,6 +25,16 @@ public List<Attendance> findByMemberAndStudyId(Member member, Long studyId) {
.fetch();
}

@Override
public List<Attendance> findByStudyIdAndMemberIds(Long studyId, List<Long> memberIds) {
return queryFactory
.selectFrom(attendance)
.innerJoin(attendance.studyDetail, studyDetail)
.fetchJoin()
.where(attendance.student.id.in(memberIds), eqStudyId(studyId))
.fetch();
}

private BooleanExpression eqMemberId(Long memberId) {
return memberId != null ? attendance.student.id.eq(memberId) : null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.gdschongik.gdsc.domain.study.dao;

import com.gdschongik.gdsc.domain.study.domain.StudyAchievement;
import java.util.List;

public interface StudyAchievementCustomRepository {
List<StudyAchievement> findByStudyIdAndMemberIds(Long studyId, List<Long> memberIds);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.gdschongik.gdsc.domain.study.dao;

import static com.gdschongik.gdsc.domain.study.domain.QStudyAchievement.*;

import com.gdschongik.gdsc.domain.study.domain.StudyAchievement;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class StudyAchievementCustomRepositoryImpl implements StudyAchievementCustomRepository {

private final JPAQueryFactory queryFactory;

@Override
public List<StudyAchievement> findByStudyIdAndMemberIds(Long studyId, List<Long> memberIds) {
return queryFactory
.selectFrom(studyAchievement)
.where(studyAchievement.student.id.in(memberIds), eqStudyId(studyId))
Copy link
Member

Choose a reason for hiding this comment

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

사소하긴 한데 studyId가 where절 앞에 오는 편이 더 효율적일듯 합니다

.fetch();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

쿼리 메서드가 효율적으로 구현되었습니다만, 개선의 여지가 있습니다.

findByStudyIdAndMemberIds 메서드가 QueryDSL을 사용하여 효율적으로 구현되었습니다. in 절을 사용하여 여러 회원 ID를 한 번에 처리하는 것이 좋습니다.

하지만 memberIds 파라미터에 대한 null 체크가 없습니다. 빈 리스트가 전달될 경우 문제가 발생할 수 있습니다.

다음과 같이 memberIds에 대한 null 체크를 추가하는 것이 좋겠습니다:

 @Override
 public List<StudyAchievement> findByStudyIdAndMemberIds(Long studyId, List<Long> memberIds) {
+    if (memberIds == null || memberIds.isEmpty()) {
+        return List.of();
+    }
     return queryFactory
             .selectFrom(studyAchievement)
             .where(studyAchievement.student.id.in(memberIds), eqStudyId(studyId))
             .fetch();
 }
📝 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
@Override
public List<StudyAchievement> findByStudyIdAndMemberIds(Long studyId, List<Long> memberIds) {
return queryFactory
.selectFrom(studyAchievement)
.where(studyAchievement.student.id.in(memberIds), eqStudyId(studyId))
.fetch();
}
@Override
public List<StudyAchievement> findByStudyIdAndMemberIds(Long studyId, List<Long> memberIds) {
if (memberIds == null || memberIds.isEmpty()) {
return List.of();
}
return queryFactory
.selectFrom(studyAchievement)
.where(studyAchievement.student.id.in(memberIds), eqStudyId(studyId))
.fetch();
}


private BooleanExpression eqStudyId(Long studyId) {
return studyId != null ? studyAchievement.study.id.eq(studyId) : null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.gdschongik.gdsc.domain.study.dao;

import com.gdschongik.gdsc.domain.study.domain.StudyAchievement;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StudyAchievementRepository
extends JpaRepository<StudyAchievement, Long>, StudyAchievementCustomRepository {}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@
import com.gdschongik.gdsc.domain.study.domain.StudyHistory;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StudyHistoryRepository extends JpaRepository<StudyHistory, Long> {

List<StudyHistory> findByStudyId(Long studyId);

List<StudyHistory> findAllByStudent(Member member);

Optional<StudyHistory> findByStudentAndStudy(Member member, Study study);

boolean existsByStudentAndStudy(Member member, Study study);

Optional<StudyHistory> findByStudentAndStudyId(Member member, Long studyId);

Page<StudyHistory> findByStudyId(Long studyId, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,8 @@ public static Curriculum generateCurriculum(
public boolean isOpen() {
return status == StudyStatus.OPEN;
}

public boolean isCancelled() {
return status == StudyStatus.CANCELLED;
}
}
Loading