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

Merged
merged 14 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -7,4 +7,15 @@

public interface AttendanceQueryRepository {
List<Attendance> findAttendancesByAvatarIdAndEnrollmentStatuses(Long avatarId, List<EnrollmentStatus> statuses);

List<Attendance> findAttendancesByStudyIdInEnrollmentStatuses(
Long studyId,
List<EnrollmentStatus> statuses
);

List<Attendance> findAttendancesByAvatarIdAndStudyIdInEnrollmentStatuses(
Long avatarId,
Long studyId,
List<EnrollmentStatus> statuses
);
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package com.join.core.attendance.repository;

import com.join.core.attendance.domain.Attendance;
import com.join.core.attendance.domain.QAttendance;
import com.join.core.enrollment.constant.EnrollmentStatus;
import com.join.core.enrollment.domain.QEnrollment;
import com.join.core.meeting.domain.QMeeting;
import com.join.core.study.domain.QStudy;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.List;

import static com.join.core.attendance.domain.QAttendance.*;
import static com.join.core.avatar.domain.QAvatar.*;
import static com.join.core.enrollment.domain.QEnrollment.*;
import static com.join.core.meeting.domain.QMeeting.*;
import static com.join.core.study.domain.QStudy.*;

@RequiredArgsConstructor
@Component
public class AttendanceQueryRepositoryImpl implements AttendanceQueryRepository {
Expand All @@ -21,26 +23,56 @@ public class AttendanceQueryRepositoryImpl implements AttendanceQueryRepository

@Override
public List<Attendance> findAttendancesByAvatarIdAndEnrollmentStatuses(Long avatarId, List<EnrollmentStatus> statuses) {
QAttendance attendance = QAttendance.attendance;
QMeeting meeting = QMeeting.meeting;
QStudy study = QStudy.study;
QEnrollment enrollment = QEnrollment.enrollment;

return queryFactory
.selectFrom(attendance)
.distinct()
.join(attendance.meeting, meeting)
.join(meeting.study, study)
.where(
study.id.in(
JPAExpressions
.select(enrollment.study.id)
.from(enrollment)
.where(
enrollment.avatar.id.eq(avatarId)
.and(enrollment.status.in(statuses))
)
)
JPAExpressions
.selectOne()
.from(enrollment)
.where(
enrollment.study.id.eq(study.id)
.and(enrollment.avatar.id.eq(avatarId))
.and(enrollment.status.in(statuses))
)
.exists(),
attendance.avatar.id.eq(avatarId)
)
.fetch();
}

@Override
public List<Attendance> findAttendancesByStudyIdInEnrollmentStatuses(Long studyId, List<EnrollmentStatus> statuses) {
return queryFactory
.selectFrom(attendance)
.join(attendance.meeting, meeting)
.join(attendance.avatar, avatar)
.join(meeting.study, study).on(study.id.eq(studyId))
.leftJoin(enrollment).on(
enrollment.study.eq(study)
.and(enrollment.avatar.eq(avatar))
)
.where(
enrollment.status.in(statuses)
)
.fetch();
}

@Override
public List<Attendance> findAttendancesByAvatarIdAndStudyIdInEnrollmentStatuses(Long avatarId, Long studyId, List<EnrollmentStatus> statuses) {
return queryFactory
.selectFrom(attendance)
.join(attendance.meeting, meeting)
.join(attendance.avatar, avatar).on(avatar.id.eq(avatarId))
.join(meeting.study, study).on(study.id.eq(studyId))
.leftJoin(enrollment).on(
enrollment.study.eq(study)
.and(enrollment.avatar.eq(avatar))
)
.where(
enrollment.status.in(statuses)
)
.fetch();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,23 @@ public List<Attendance> findAttendanceForLeftStudy(Long avatarId) {
return attendanceQueryRepository.findAttendancesByAvatarIdAndEnrollmentStatuses(avatarId, List.of(EnrollmentStatus.LEFT));
}

@Override
public List<Attendance> findByStudyIdForJoinedStudy(Long studyId) {
return attendanceQueryRepository.findAttendancesByStudyIdInEnrollmentStatuses(studyId, List.of(EnrollmentStatus.JOINED, EnrollmentStatus.REQUEST_LEAVE));
}

@Override
public List<Attendance> findByStudyIdForLeftStudy(Long studyId) {
return attendanceQueryRepository.findAttendancesByStudyIdInEnrollmentStatuses(studyId, List.of(EnrollmentStatus.LEFT));
}

@Override
public List<Attendance> findByAvatarIdAndStudyIdForJoinedStudy(Long avatarId, Long studyId) {
return attendanceQueryRepository.findAttendancesByAvatarIdAndStudyIdInEnrollmentStatuses(avatarId, studyId, List.of(EnrollmentStatus.JOINED, EnrollmentStatus.REQUEST_LEAVE));
}

@Override
public List<Attendance> findByAvatarIdAndStudyIdForLeftStudy(Long avatarId, Long studyId) {
return attendanceQueryRepository.findAttendancesByAvatarIdAndStudyIdInEnrollmentStatuses(avatarId, studyId, List.of(EnrollmentStatus.LEFT));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.join.core.attendance.service;

import com.join.core.attendance.domain.Attendance;
import com.join.core.avatar.domain.AvatarReader;
import com.join.core.common.util.NumberUtil;
import com.join.core.enrollment.constant.EnrollmentStatus;
import com.join.core.meeting.domain.MeetingReader;
import lombok.RequiredArgsConstructor;
Expand All @@ -14,11 +16,12 @@
@RequiredArgsConstructor
public class AttendanceRateService {

private final AvatarReader avatarReader;
private final AttendanceReader attendanceReader;
private final MeetingReader meetingReader;

@Transactional(readOnly = true)
public double calculateAttendanceRate(Long avatarId) {
public double calculateIndividualAttendanceRate(Long avatarId) {
List<Attendance> attendancesForJoinedStudies = attendanceReader.findAttendanceForJoinedStudy(avatarId);
List<Attendance> attendancesForLeftStudies = attendanceReader.findAttendanceForLeftStudy(avatarId);

Expand All @@ -41,6 +44,62 @@ public double calculateAttendanceRate(Long avatarId) {

if (totalAttendance == 0) return 0.0;

return totalAttendance / totalMeetings * 100;
return NumberUtil.round(2, totalAttendance / totalMeetings * 100);
}

@Transactional(readOnly = true)
public double calculateTeamAttendanceRateForStudy(Long studyId) {
List<Attendance> attendancesForJoinedStudies = attendanceReader.findByStudyIdForJoinedStudy(studyId);
List<Attendance> attendancesForLeftStudies = attendanceReader.findByStudyIdForLeftStudy(studyId);

long countStudyMembers = avatarReader.findAvatarsExceptPendingByStudyId(studyId).size();
long countAllMeetings = meetingReader.findMeetingsByStudyId(studyId).size();
long totalMeetings = countAllMeetings * countStudyMembers;

double totalAttendance = Stream.concat(
attendancesForJoinedStudies.stream(),
attendancesForLeftStudies.stream()
)
.mapToDouble(attendance -> {
double reflectionRate = attendance.getStatus().getReflectionRate();

if (attendancesForLeftStudies.contains(attendance)) {
reflectionRate *= EnrollmentStatus.LEFT.getReflectionRate();
}

return reflectionRate;
})
.sum();

if (totalAttendance == 0) return 0.0;

return NumberUtil.round(2, totalAttendance / totalMeetings * 100);
}

@Transactional(readOnly = true)
public double calculateMemberAttendanceRateForStudy(Long avatarId, Long studyId) {
List<Attendance> attendancesForJoinedStudies = attendanceReader.findByAvatarIdAndStudyIdForJoinedStudy(avatarId, studyId);
List<Attendance> attendancesForLeftStudies = attendanceReader.findByAvatarIdAndStudyIdForLeftStudy(avatarId, studyId);

long totalMeetings = meetingReader.findMeetingsByStudyId(studyId).size();

double totalAttendance = Stream.concat(
attendancesForJoinedStudies.stream(),
attendancesForLeftStudies.stream()
)
.mapToDouble(attendance -> {
double reflectionRate = attendance.getStatus().getReflectionRate();

if (attendancesForLeftStudies.contains(attendance)) {
reflectionRate *= EnrollmentStatus.LEFT.getReflectionRate();
}

return reflectionRate;
})
.sum();

if (totalAttendance == 0) return 0.0;

return NumberUtil.round(2, totalAttendance / totalMeetings * 100);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,12 @@ public interface AttendanceReader {
List<Attendance> findAttendanceForJoinedStudy(Long avatarId);

List<Attendance> findAttendanceForLeftStudy(Long avatarId);

List<Attendance> findByStudyIdForJoinedStudy(Long studyId);

List<Attendance> findByStudyIdForLeftStudy(Long studyId);

List<Attendance> findByAvatarIdAndStudyIdForJoinedStudy(Long avatarId, Long studyId);

List<Attendance> findByAvatarIdAndStudyIdForLeftStudy(Long avatarId, Long studyId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.join.core.auth.domain.UserPrincipal;
import com.join.core.avatar.controller.specification.MyPageControllerSpecification;
import com.join.core.avatar.domain.MyPageService;
import com.join.core.avatar.dto.response.MyManagedStudyInfoResponse;
import com.join.core.avatar.dto.response.MyPageInfoResponse;
import com.join.core.common.response.ApiResponse;
import lombok.RequiredArgsConstructor;
Expand All @@ -12,6 +13,8 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RequiredArgsConstructor
@RestController
@RequestMapping("${api.prefix}/my-page")
Expand All @@ -25,4 +28,11 @@ public ApiResponse<MyPageInfoResponse> getMyPageInfo(
@AuthenticationPrincipal UserPrincipal principal) {
return ApiResponse.ok(myPageService.getMyPageInfo(principal.getAvatarId()));
}

@PreAuthorize("isAuthenticated()")
@GetMapping("/manage-study")
public ApiResponse<List<MyManagedStudyInfoResponse>> getMyManagedStudies(
@AuthenticationPrincipal UserPrincipal principal) {
return ApiResponse.ok(myPageService.getMyManagedStudies(principal.getAvatarId()));
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
package com.join.core.avatar.controller.specification;

import com.join.core.auth.domain.UserPrincipal;
import com.join.core.avatar.dto.response.MyManagedStudyInfoResponse;
import com.join.core.avatar.dto.response.MyPageInfoResponse;
import com.join.core.common.response.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.core.annotation.AuthenticationPrincipal;

import java.util.List;

public interface MyPageControllerSpecification {

@Tag(name = "${swagger.tag.user}")
@Tag(name = "${swagger.tag.my-page}")
@Operation(summary = "마이페이지 조회 - 인증 필수",
description = "마이페이지 조회",
security = {@SecurityRequirement(name = "session-token")})
ApiResponse<MyPageInfoResponse> getMyPageInfo(
@AuthenticationPrincipal UserPrincipal principal);

@Tag(name = "${swagger.tag.my-page}")
@Operation(summary = "마이페이지 운영중인 스터디 목록 조회 - 인증 필수",
description = "마이페이지 운영중인 스터디 목록을 조회해옵니다.",
security = {@SecurityRequirement(name = "session-token")})
ApiResponse<List<MyManagedStudyInfoResponse>> getMyManagedStudies(
@AuthenticationPrincipal UserPrincipal principal);
}
3 changes: 3 additions & 0 deletions src/main/java/com/join/core/avatar/domain/AvatarReader.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.join.core.avatar.domain;

import java.util.List;

public interface AvatarReader {
Avatar getAvatarById(Long avatarId);

Expand All @@ -11,4 +13,5 @@ public interface AvatarReader {

Avatar getAvatarByAvatarToken(String avatarToken);

List<Avatar> findAvatarsExceptPendingByStudyId(Long studyId);
}
4 changes: 4 additions & 0 deletions src/main/java/com/join/core/avatar/domain/MyPageService.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.join.core.avatar.domain;

import com.join.core.avatar.dto.response.MyManagedStudyInfoResponse;
import com.join.core.avatar.dto.response.MyPageInfoResponse;

import java.util.List;

public interface MyPageService {

MyPageInfoResponse getMyPageInfo(Long avatarId);
List<MyManagedStudyInfoResponse> getMyManagedStudies(Long avatarId);
}
39 changes: 37 additions & 2 deletions src/main/java/com/join/core/avatar/domain/MyPageServiceImpl.java
Original file line number Diff line number Diff line change
@@ -1,30 +1,65 @@
package com.join.core.avatar.domain;

import com.join.core.attendance.service.AttendanceRateService;
import com.join.core.avatar.dto.response.MyManagedStudyInfoResponse;
import com.join.core.avatar.dto.response.MyPageInfoResponse;
import com.join.core.proof.service.ProofRateService;
import com.join.core.proof.service.ProofReader;
import com.join.core.study.domain.Study;
import com.join.core.study.service.StudyReader;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class MyPageServiceImpl implements MyPageService {

private final AvatarReader avatarReader;
private final StudyReader studyReader;
private final AttendanceRateService attendanceRateService;
private final ProofReader proofReader;
private final ProofRateService proofRateService;

@Transactional(readOnly = true)
@Override
public MyPageInfoResponse getMyPageInfo(Long avatarId) {
Avatar avatar = avatarReader.getAvatarById(avatarId);

double averageAttendanceRate = attendanceRateService.calculateAttendanceRate(avatarId);
double averageAttendanceRate = attendanceRateService.calculateIndividualAttendanceRate(avatarId);

double averageProofRate = proofRateService.calculateProofRate(avatarId);
double averageProofRate = proofRateService.calculateIndividualProofRate(avatarId);

return MyPageInfoResponse.of(avatar, averageAttendanceRate, averageProofRate);
}

@Transactional(readOnly = true)
@Override
public List<MyManagedStudyInfoResponse> getMyManagedStudies(Long avatarId) {
List<Study> managedStudies = studyReader.getStudiesByLeaderAvatarId(avatarId);

return managedStudies.stream().map(
study -> {
double teamAttendanceRateForStudy = attendanceRateService.calculateTeamAttendanceRateForStudy(study.getId());
double teamProofRateForStudy = proofRateService.calculateTeamAttendanceRateForStudy(study.getId());
List<MyManagedStudyInfoResponse.StudyMemberAchievementDto> achievementDtos = getMembersRatesAndApprovedStatus(study);

return MyManagedStudyInfoResponse.of(study, teamAttendanceRateForStudy, teamProofRateForStudy, achievementDtos);
}
).toList();
}

private List<MyManagedStudyInfoResponse.StudyMemberAchievementDto> getMembersRatesAndApprovedStatus(Study study) {
List<Avatar> avatars = avatarReader.findAvatarsExceptPendingByStudyId(study.getId());
return avatars.stream()
.map(avatar -> {
double attendanceRate = attendanceRateService.calculateMemberAttendanceRateForStudy(avatar.getId(), study.getId());
double proofRate = proofRateService.calculateMembersAttendanceRateForStudy(avatar.getId(), study.getId());
boolean isFullyApproved = proofReader.isFullyApproved(avatar.getId(), study.getId());
return MyManagedStudyInfoResponse.StudyMemberAchievementDto.of(avatar, attendanceRate, proofRate, isFullyApproved);
}).toList();
}
}
Loading