Skip to content

Commit

Permalink
✨ [STMT-290] 단일 스터디 멤버 조회 API 구현 (#151)
Browse files Browse the repository at this point in the history
* ✨ [STMT-290] 단일 스터디 멤버 조회 쿼리 구현

* ✨ [STMT-290] 멤버의 활동 참여 목록 쿼리 구현

* ✨ [STMT-290] 활동 상태와 활동 참여자 도메인에 성취 판단 여부를 반환하는 메서드 구현

* ✨ [STMT-290] 멤버의 성취도를 계산하는 유스케이스 구현

* ✨ [STMT-290] 스터디 멤버 상세 조회 유스케이스 구현

* ✨ [STMT-290] 스터디 멤버 상세 조회 API 구현

* 🐛 [STMT-290] 포도알 발송 여부를 포도알 발송 가능 여부로 수정

* ✅ [STMT-290] 스터디 멤버 상세 조회 성공 테스트 케이스 작성

* ✅ [STMT-290] 스터디 멤버 상세 조회 실패 테스트 케이스 작성

* 📝 [STMT-290] 스터디 멤버 목록/상세 조회 API 테스트 문서에 header 정보 추가

* 📝 [STMT-290] 스터디 멤버 상세 조회 API 명세서 작성
  • Loading branch information
05AM authored Sep 10, 2024
1 parent 35f5dce commit e3f05d4
Show file tree
Hide file tree
Showing 21 changed files with 346 additions and 5 deletions.
28 changes: 27 additions & 1 deletion src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,33 @@ include::{snippets}/update-study/fail/invalid-study-meeting-schedule/response-fi

== 스터디 멤버 관리

=== 스터디 멤버 조회
=== 스터디 멤버 단일 상세 조회

전달받은 스터디 ID의 단일 멤버 정보를 상세 조회하는 API입니다.

==== GET /v1/api/studies/{studyId}/members/{memberId}

===== 요청
include::{snippets}/get-study-member-detail/success/http-request.adoc[]

====== 헤더
include::{snippets}/get-study-member-detail/success/request-headers.adoc[]

====== 경로 변수
include::{snippets}/get-study-member-detail/success/path-parameters.adoc[]

===== 응답 실패 (403)
.요청자가 스터디의 멤버가 아닌 경우
include::{snippets}/get-study-member-detail/fail/is-not-study-member/response-body.adoc[]
include::{snippets}/get-study-member-detail/fail/is-not-study-member/response-fields.adoc[]

===== 응답 실패 (404)
.요청한 스터디가 존재하지 않는 경우
include::{snippets}/get-study-member-detail/fail/study-not-found/response-body.adoc[]
include::{snippets}/get-study-member-detail/fail/study-not-found/response-fields.adoc[]


=== 스터디 멤버 목록 조회

전달받은 스터디 ID의 속한 멤버들을 조회하는 API입니다.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ public List<ActivityParticipant> findAllByActivityId(Long activityId) {
.toList();
}

@Override
public List<ActivityParticipant> findMemberParticipation(Long studyId, Long memberId) {
return jpaActivityParticipantRepository.findAllByActivityStudyIdAndMemberId(studyId, memberId).stream()
.map(activityParticipantPersistenceMapper::toDomain)
.toList();
}

@Override
public void update(ActivityParticipant participant) {
jpaActivityParticipantRepository.save(activityParticipantPersistenceMapper.toEntity(participant));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ public interface JpaActivityParticipantRepository extends JpaRepository<Activity

List<ActivityParticipantJpaEntity> findAllByActivityId(Long activityId);

List<ActivityParticipantJpaEntity> findAllByActivityStudyIdAndMemberId(Long studyId, Long memberId);

void deleteAllByActivityId(Long activityId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.stumeet.server.activity.application.port.in;

public interface EvaluateMemberAchievementUseCase {

Integer getMemberAchievement(Long studyId, Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public interface ActivityParticipantQueryPort {
ActivityParticipant findByActivityIdAndMemberIdAndId(Long activityId, Long memberId, Long id);

List<ActivityParticipant> findAllByActivityId(Long activityId);

List<ActivityParticipant> findMemberParticipation(Long studyId, Long memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.stumeet.server.activity.application.service;

import java.util.List;

import org.springframework.transaction.annotation.Transactional;

import com.stumeet.server.activity.application.port.in.EvaluateMemberAchievementUseCase;
import com.stumeet.server.activity.application.port.out.ActivityParticipantQueryPort;
import com.stumeet.server.activity.domain.model.ActivityCategory;
import com.stumeet.server.activity.domain.model.ActivityParticipant;
import com.stumeet.server.activity.domain.model.CommonStatus;
import com.stumeet.server.common.annotation.UseCase;

import lombok.RequiredArgsConstructor;

@UseCase
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class EvaluateMemberMemberParticipationService implements EvaluateMemberAchievementUseCase {
private static final int PERCENTAGE_MULTIPLIER = 100;

private final ActivityParticipantQueryPort activityParticipantQueryPort;

@Override
public Integer getMemberAchievement(Long studyId, Long memberId) {
List<ActivityParticipant> activityParticipants =
activityParticipantQueryPort.findMemberParticipation(studyId, memberId);

return calculateParticipationPercentage(activityParticipants);
}

private Integer calculateParticipationPercentage(List<ActivityParticipant> participants) {
List<ActivityParticipant> joinedActivities = getValidJoinedActivities(participants);
long totalActivities = joinedActivities.size();

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

long achievedActivities = joinedActivities.stream()
.filter(ActivityParticipant::isAchieved)
.count();

double participation = (double)achievedActivities / totalActivities * PERCENTAGE_MULTIPLIER;
return (int)participation;
}

private List<ActivityParticipant> getValidJoinedActivities(List<ActivityParticipant> participants) {
return participants.stream()
.filter(participant -> isValidCategory(participant) && hasJoined(participant))
.toList();
}

private boolean isValidCategory(ActivityParticipant participant) {
return participant.getActivity().getCategory().equals(ActivityCategory.ASSIGNMENT)
|| participant.getActivity().getCategory().equals(ActivityCategory.MEET);
}

private boolean hasJoined(ActivityParticipant participant) {
return !participant.getStatus().equals(CommonStatus.NOT_JOINED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@ public ActivityParticipant update(ActivityStatus status) {
.status(status)
.build();
}

public boolean isAchieved() {
return this.status.isSuccessfulStatus();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ static ActivityStatus findByStatus(String status) {
.findAny()
.orElseThrow(() -> new NotExistsActivityStatusException(status));
}

boolean isSuccessfulStatus();
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,9 @@ public String getStatus() {
public String getDescription() {
return this.description;
}

@Override
public boolean isSuccessfulStatus() {
return this.equals(PERFORMED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ public String getStatus() {
public String getDescription() {
return this.description;
}

@Override
public boolean isSuccessfulStatus() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ public String getStatus() {
public String getDescription() {
return this.description;
}

@Override
public boolean isSuccessfulStatus() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ public String getStatus() {
public String getDescription() {
return this.description;
}

@Override
public boolean isSuccessfulStatus() {
return this.equals(ATTENDANCE) || this.equals(ACKNOWLEDGED_ABSENCE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import com.stumeet.server.common.auth.model.LoginMember;
import com.stumeet.server.common.model.ApiResponse;
import com.stumeet.server.common.response.SuccessCode;
import com.stumeet.server.studymember.application.port.in.response.StudyMemberDetailResponse;
import com.stumeet.server.studymember.application.port.in.response.StudyMemberResponses;
import com.stumeet.server.studymember.application.port.in.StudyMemberQueryUseCase;
import lombok.RequiredArgsConstructor;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand All @@ -32,4 +34,17 @@ public ResponseEntity<ApiResponse<StudyMemberResponses>> getStudyMembers(
HttpStatus.OK
);
}

@GetMapping("/studies/{studyId}/members/{memberId}")
public ResponseEntity<ApiResponse<StudyMemberDetailResponse>> getStudyMemberDetail(
@AuthenticationPrincipal LoginMember member,
@PathVariable Long studyId,
@PathVariable Long memberId
) {
StudyMemberDetailResponse response = studyMemberQueryUseCase.getStudyMemberDetail(studyId, memberId, member.getId());

return ResponseEntity.ok(
ApiResponse.success(SuccessCode.GET_SUCCESS, response)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
import java.util.List;

public interface JpaStudyMemberRepositoryCustom {

StudyMemberJpaEntity findStudyMemberByStudyIdAndMemberId(Long studyId, Long memberId);

List<SimpleStudyMemberResponse> findStudyMembersByStudyId(Long studyId);

boolean isStudyJoinMember(Long studyId, Long memberId);

boolean isAdmin(Long studyId, Long adminId);

boolean isSentGrape(Long studyId, Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.stumeet.server.studymember.application.port.in.response.QSimpleStudyMemberResponse;
import com.stumeet.server.studymember.application.port.in.response.SimpleStudyMemberResponse;

import lombok.RequiredArgsConstructor;

import java.util.List;
Expand All @@ -14,6 +15,18 @@ public class JpaStudyMemberRepositoryCustomImpl implements JpaStudyMemberReposit

private final JPAQueryFactory query;

@Override
public StudyMemberJpaEntity findStudyMemberByStudyIdAndMemberId(Long studyId, Long memberId) {
return query
.select(studyMemberJpaEntity)
.from(studyMemberJpaEntity)
.innerJoin(studyMemberJpaEntity.member)
.where(
studyMemberJpaEntity.study.id.eq(studyId),
studyMemberJpaEntity.member.id.eq(memberId)
)
.fetchOne();
}

@Override
public List<SimpleStudyMemberResponse> findStudyMembersByStudyId(Long studyId) {
Expand Down Expand Up @@ -56,4 +69,17 @@ public boolean isAdmin(Long studyId, Long adminId) {
).fetchOne()
);
}

@Override
public boolean isSentGrape(Long studyId, Long memberId) {
return Boolean.TRUE.equals(
query
.select(studyMemberJpaEntity.isSentGrape)
.from(studyMemberJpaEntity)
.where(
studyMemberJpaEntity.study.id.eq(studyId),
studyMemberJpaEntity.member.id.eq(memberId)
).fetchOne()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ public List<SimpleStudyMemberResponse> findStudyMembers(Long studyId) {
return jpaStudyMemberRepository.findStudyMembersByStudyId(studyId);
}

@Override
public StudyMember findStudyMember(Long studyId, Long memberId) {
StudyMemberJpaEntity entity = jpaStudyMemberRepository.findStudyMemberByStudyIdAndMemberId(studyId, memberId);

return studyMemberPersistenceMapper.toDomain(entity);
}

@Override
public boolean isSentGrape(Long studyId, Long memberId) {
return jpaStudyMemberRepository.isSentGrape(studyId, memberId);
}

@Override
public boolean isNotStudyJoinMember(Long studyId, Long memberId) {
return !jpaStudyMemberRepository.isStudyJoinMember(studyId, memberId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.stumeet.server.studymember.application.port.in;

import com.stumeet.server.studymember.application.port.in.response.StudyMemberDetailResponse;
import com.stumeet.server.studymember.application.port.in.response.StudyMemberResponses;

public interface StudyMemberQueryUseCase {
StudyMemberResponses getStudyMembers(Long studyId, Long memberId);
StudyMemberResponses getStudyMembers(Long studyId, Long requesterId);

StudyMemberDetailResponse getStudyMemberDetail(Long studyId, Long targetMemberId, Long requesterId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.stumeet.server.studymember.application.port.in.response;

public record StudyMemberDetailResponse(
Long id,
String name,
String image,
String region,
String profession,
boolean canSendGrape,
Integer achievement
) {
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.stumeet.server.studymember.application.port.out;

import com.stumeet.server.studymember.application.port.in.response.SimpleStudyMemberResponse;
import com.stumeet.server.studymember.domain.StudyMember;

import java.util.List;

public interface StudyMemberQueryPort {
List<SimpleStudyMemberResponse> findStudyMembers(Long studyId);

StudyMember findStudyMember(Long studyId, Long memberId);

boolean isSentGrape(Long studyId, Long memberId);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package com.stumeet.server.studymember.application.service;

import com.stumeet.server.activity.application.port.in.EvaluateMemberAchievementUseCase;
import com.stumeet.server.common.annotation.UseCase;
import com.stumeet.server.study.application.port.in.StudyValidationUseCase;
import com.stumeet.server.studymember.application.port.in.response.SimpleStudyMemberResponse;
import com.stumeet.server.studymember.application.port.in.response.StudyMemberDetailResponse;
import com.stumeet.server.studymember.application.port.in.response.StudyMemberResponses;
import com.stumeet.server.studymember.application.port.in.StudyMemberQueryUseCase;
import com.stumeet.server.studymember.application.port.in.StudyMemberValidationUseCase;
import com.stumeet.server.studymember.application.port.out.StudyMemberQueryPort;
import com.stumeet.server.studymember.domain.StudyMember;

import lombok.RequiredArgsConstructor;

import org.springframework.transaction.annotation.Transactional;

import java.util.List;
Expand All @@ -19,14 +24,36 @@ public class StudyMemberQueryService implements StudyMemberQueryUseCase {

private final StudyValidationUseCase studyValidationUseCase;
private final StudyMemberValidationUseCase studyMemberValidationUseCase;
private final EvaluateMemberAchievementUseCase evaluateMemberAchievementUseCase;

private final StudyMemberQueryPort studyMemberQueryPort;

@Override
public StudyMemberResponses getStudyMembers(Long studyId, Long memberId) {
public StudyMemberResponses getStudyMembers(Long studyId, Long requesterId) {
studyValidationUseCase.checkById(studyId);
studyMemberValidationUseCase.checkStudyJoinMember(studyId, memberId);
studyMemberValidationUseCase.checkStudyJoinMember(studyId, requesterId);
List<SimpleStudyMemberResponse> response = studyMemberQueryPort.findStudyMembers(studyId);

return new StudyMemberResponses(response);
}

@Override
public StudyMemberDetailResponse getStudyMemberDetail(Long studyId, Long targetMemberId, Long requesterId) {
studyValidationUseCase.checkById(studyId);
studyMemberValidationUseCase.checkStudyJoinMember(studyId, requesterId);

StudyMember studyMember = studyMemberQueryPort.findStudyMember(studyId, targetMemberId);
boolean canSendGrape = studyMemberQueryPort.isSentGrape(studyId, requesterId);
int achievement = evaluateMemberAchievementUseCase.getMemberAchievement(studyId, targetMemberId);

return new StudyMemberDetailResponse(
studyMember.getMember().getId(),
studyMember.getMember().getName(),
studyMember.getMember().getImage(),
studyMember.getMember().getRegion(),
studyMember.getMember().getProfession().getName(),
canSendGrape,
achievement
);
}
}
Loading

0 comments on commit e3f05d4

Please sign in to comment.