Skip to content

Commit

Permalink
feat: 우수 스터디원 지정 및 철회 API 구현 (#800)
Browse files Browse the repository at this point in the history
* feat: 우수 스터디원 지정 및 철회 API 구현

* feat: 우수 스터디원 지정 및 철회 로직 추가

* test: 우수 스터디원 지정 및 철회 테스트 추가

* feat: 수강신청 여부 검증 로직 추가

* test: 수강신청 여부 검증 로직 테스트 추가

* refactor: 레포지토리 메서드명 변경

* refactor: getById 메서드를 findById로 대체

* refactor: NPE 방지 코드 추가

* refactor: 타입 캐스팅으로 변경
  • Loading branch information
Sangwook02 authored Oct 11, 2024
1 parent a2e3a40 commit f12092a
Show file tree
Hide file tree
Showing 13 changed files with 300 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.gdschongik.gdsc.domain.study.api;

import com.gdschongik.gdsc.domain.study.application.MentorStudyAchievementService;
import com.gdschongik.gdsc.domain.study.dto.request.OutstandingStudentRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Mentor StudyAchievement", description = "멘토 스터디 우수 스터디원 관리 API입니다.")
@RestController
@RequestMapping("/mentor/study-achievements")
@RequiredArgsConstructor
public class MentorStudyAchievementController {

private final MentorStudyAchievementService mentorStudyAchievementService;

@Operation(summary = "우수 스터디원 지정", description = "우수 스터디원으로 지정합니다.")
@PostMapping
public ResponseEntity<Void> designateOutstandingStudent(
@RequestParam(name = "studyId") Long studyId, @Valid @RequestBody OutstandingStudentRequest request) {
mentorStudyAchievementService.designateOutstandingStudent(studyId, request);
return ResponseEntity.ok().build();
}

@Operation(summary = "우수 스터디원 철회", description = "우수 스터디원 지정을 철회합니다.")
@DeleteMapping
public ResponseEntity<Void> withdrawOutstandingStudent(
@RequestParam(name = "studyId") Long studyId, @Valid @RequestBody OutstandingStudentRequest request) {
mentorStudyAchievementService.withdrawOutstandingStudent(studyId, request);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.gdschongik.gdsc.domain.study.application;

import static com.gdschongik.gdsc.global.exception.ErrorCode.*;

import com.gdschongik.gdsc.domain.member.dao.MemberRepository;
import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.study.dao.StudyAchievementRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyRepository;
import com.gdschongik.gdsc.domain.study.domain.Study;
import com.gdschongik.gdsc.domain.study.domain.StudyAchievement;
import com.gdschongik.gdsc.domain.study.domain.StudyHistoryValidator;
import com.gdschongik.gdsc.domain.study.domain.StudyValidator;
import com.gdschongik.gdsc.domain.study.dto.request.OutstandingStudentRequest;
import com.gdschongik.gdsc.global.exception.CustomException;
import com.gdschongik.gdsc.global.util.MemberUtil;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class MentorStudyAchievementService {

private final MemberUtil memberUtil;
private final StudyValidator studyValidator;
private final StudyHistoryValidator studyHistoryValidator;
private final StudyRepository studyRepository;
private final StudyHistoryRepository studyHistoryRepository;
private final StudyAchievementRepository studyAchievementRepository;
private final MemberRepository memberRepository;

@Transactional
public void designateOutstandingStudent(Long studyId, OutstandingStudentRequest request) {
Member currentMember = memberUtil.getCurrentMember();
Study study = studyRepository.findById(studyId).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND));
Long countByStudyIdAndStudentIds =
studyHistoryRepository.countByStudyIdAndStudentIds(studyId, request.studentIds());

studyValidator.validateStudyMentor(currentMember, study);
studyHistoryValidator.validateAppliedToStudy(
countByStudyIdAndStudentIds, request.studentIds().size());

List<Member> outstandingStudents = memberRepository.findAllById(request.studentIds());
List<StudyAchievement> studyAchievements = outstandingStudents.stream()
.map(member -> StudyAchievement.create(member, study, request.achievementType()))
.toList();
studyAchievementRepository.saveAll(studyAchievements);

log.info(
"[MentorStudyAchievementService] 우수 스터디원 지정: studyId={}, studentIds={}", studyId, request.studentIds());
}

@Transactional
public void withdrawOutstandingStudent(Long studyId, OutstandingStudentRequest request) {
Member currentMember = memberUtil.getCurrentMember();
Study study = studyRepository.findById(studyId).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND));
long countByStudyIdAndStudentIds =
studyHistoryRepository.countByStudyIdAndStudentIds(studyId, request.studentIds());

studyValidator.validateStudyMentor(currentMember, study);
studyHistoryValidator.validateAppliedToStudy(
countByStudyIdAndStudentIds, request.studentIds().size());

studyAchievementRepository.deleteByStudyAndAchievementTypeAndMemberIds(
studyId, request.achievementType(), request.studentIds());

log.info(
"[MentorStudyAchievementService] 우수 스터디원 철회: studyId={}, studentIds={}", studyId, request.studentIds());
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.gdschongik.gdsc.domain.study.dao;

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

public interface StudyAchievementCustomRepository {
List<StudyAchievement> findByStudyIdAndMemberIds(Long studyId, List<Long> memberIds);

void deleteByStudyAndAchievementTypeAndMemberIds(
Long studyId, AchievementType achievementType, List<Long> memberIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

import com.gdschongik.gdsc.domain.study.domain.AchievementType;
import com.gdschongik.gdsc.domain.study.domain.StudyAchievement;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
Expand All @@ -21,6 +22,18 @@ public List<StudyAchievement> findByStudyIdAndMemberIds(Long studyId, List<Long>
.fetch();
}

@Override
public void deleteByStudyAndAchievementTypeAndMemberIds(
Long studyId, AchievementType achievementType, List<Long> memberIds) {
queryFactory
.delete(studyAchievement)
.where(
eqStudyId(studyId),
studyAchievement.achievementType.eq(achievementType),
studyAchievement.student.id.in(memberIds))
.execute();
}

private BooleanExpression eqStudyId(Long studyId) {
return studyId != null ? studyAchievement.study.id.eq(studyId) : 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 java.util.List;

public interface StudyHistoryCustomRepository {

long countByStudyIdAndStudentIds(Long studyId, List<Long> studentIds);
}
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.QStudyHistory.*;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class StudyHistoryCustomRepositoryImpl implements StudyHistoryCustomRepository {

private final JPAQueryFactory queryFactory;

@Override
public long countByStudyIdAndStudentIds(Long studyId, List<Long> studentIds) {
return (long) queryFactory
.select(studyHistory.count())
.from(studyHistory)
.where(eqStudyId(studyId), studyHistory.student.id.in(studentIds))
.fetchOne();
}

private BooleanExpression eqStudyId(Long studyId) {
return studyHistory.study.id.eq(studyId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StudyHistoryRepository extends JpaRepository<StudyHistory, Long> {
public interface StudyHistoryRepository extends JpaRepository<StudyHistory, Long>, StudyHistoryCustomRepository {

List<StudyHistory> findAllByStudent(Member member);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,10 @@ public void validateUpdateRepository(
throw new CustomException(STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_OWNER_MISMATCH);
}
}

public void validateAppliedToStudy(long countStudyHistory, int studentCount) {
if (countStudyHistory != studentCount) {
throw new CustomException(STUDY_HISTORY_NOT_APPLIED_STUDENT_EXISTS);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.gdschongik.gdsc.domain.study.dto.request;

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

public record OutstandingStudentRequest(List<Long> studentIds, AchievementType achievementType) {}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public enum ErrorCode {
STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_ASSIGNMENT_ALREADY_SUBMITTED(
HttpStatus.CONFLICT, "이미 제출한 과제가 있으므로 레포지토리를 수정할 수 없습니다."),
STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_OWNER_MISMATCH(HttpStatus.CONFLICT, "레포지토리 소유자가 현재 멤버와 다릅니다."),
STUDY_HISTORY_NOT_APPLIED_STUDENT_EXISTS(HttpStatus.CONFLICT, "해당 스터디에 신청하지 않은 멤버가 있습니다."),

// StudyAnnouncement
STUDY_ANNOUNCEMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 공지입니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.gdschongik.gdsc.domain.study.application;

import static com.gdschongik.gdsc.domain.study.domain.AchievementType.*;
import static org.assertj.core.api.Assertions.*;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.recruitment.domain.vo.Period;
import com.gdschongik.gdsc.domain.study.domain.Study;
import com.gdschongik.gdsc.domain.study.domain.StudyAchievement;
import com.gdschongik.gdsc.domain.study.dto.request.OutstandingStudentRequest;
import com.gdschongik.gdsc.helper.IntegrationTest;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

public class MentorStudyAchievementServiceTest extends IntegrationTest {

@Autowired
private MentorStudyAchievementService mentorStudyAchievementService;

@Nested
class 우수_스터디원_지정시 {

@Test
void 성공한다() {
// given
LocalDateTime now = LocalDateTime.now();
Member mentor = createMentor();
Study study = createStudy(
mentor,
Period.createPeriod(now.plusDays(5), now.plusDays(10)),
Period.createPeriod(now.minusDays(5), now));

Member student = createRegularMember();
createStudyHistory(student, study);

logoutAndReloginAs(mentor.getId(), mentor.getRole());
OutstandingStudentRequest request =
new OutstandingStudentRequest(List.of(student.getId()), FIRST_ROUND_OUTSTANDING_STUDENT);

// when
mentorStudyAchievementService.designateOutstandingStudent(study.getId(), request);

// then
List<StudyAchievement> studyAchievements =
studyAchievementRepository.findByStudyIdAndMemberIds(study.getId(), request.studentIds());
assertThat(studyAchievements).hasSize(request.studentIds().size());
}
}

@Nested
class 우수_스터디원_철회시 {

@Test
void 성공한다() {
// given
Member student = createRegularMember();
LocalDateTime now = LocalDateTime.now();
Member mentor = createMentor();
Study study = createStudy(
mentor,
Period.createPeriod(now.plusDays(5), now.plusDays(10)),
Period.createPeriod(now.minusDays(5), now));
createStudyHistory(student, study);
createStudyAchievement(student, study, FIRST_ROUND_OUTSTANDING_STUDENT);

logoutAndReloginAs(mentor.getId(), mentor.getRole());
OutstandingStudentRequest request =
new OutstandingStudentRequest(List.of(student.getId()), FIRST_ROUND_OUTSTANDING_STUDENT);

// when
mentorStudyAchievementService.withdrawOutstandingStudent(study.getId(), request);

// then
List<StudyAchievement> studyAchievements =
studyAchievementRepository.findByStudyIdAndMemberIds(study.getId(), request.studentIds());
assertThat(studyAchievements).isEmpty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,21 @@ class 레포지토리_입력시 {
.hasMessage(STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_OWNER_MISMATCH.getMessage());
}
}

@Nested
class 스터디_수강신청_여부_확인시 {

@Test
void 해당_스터디를_신청하지_않은_멤버가_있다면_실패한다() {
// given
Long countStudyHistory = 1L;
int requestStudentCount = 2;

// when & then
assertThatThrownBy(
() -> studyHistoryValidator.validateAppliedToStudy(countStudyHistory, requestStudentCount))
.isInstanceOf(CustomException.class)
.hasMessage(STUDY_HISTORY_NOT_APPLIED_STUDENT_EXISTS.getMessage());
}
}
}
21 changes: 21 additions & 0 deletions src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,15 @@
import com.gdschongik.gdsc.domain.recruitment.domain.RecruitmentRound;
import com.gdschongik.gdsc.domain.recruitment.domain.RoundType;
import com.gdschongik.gdsc.domain.recruitment.domain.vo.Period;
import com.gdschongik.gdsc.domain.study.dao.StudyAchievementRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyDetailRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyRepository;
import com.gdschongik.gdsc.domain.study.domain.AchievementType;
import com.gdschongik.gdsc.domain.study.domain.Study;
import com.gdschongik.gdsc.domain.study.domain.StudyAchievement;
import com.gdschongik.gdsc.domain.study.domain.StudyDetail;
import com.gdschongik.gdsc.domain.study.domain.StudyHistory;
import com.gdschongik.gdsc.global.security.PrincipalDetails;
import com.gdschongik.gdsc.infra.feign.payment.client.PaymentClient;
import com.gdschongik.gdsc.infra.github.client.GithubClient;
Expand Down Expand Up @@ -81,6 +86,12 @@ public abstract class IntegrationTest {
@Autowired
protected StudyDetailRepository studyDetailRepository;

@Autowired
protected StudyHistoryRepository studyHistoryRepository;

@Autowired
protected StudyAchievementRepository studyAchievementRepository;

@MockBean
protected OnboardingRecruitmentService onboardingRecruitmentService;

Expand Down Expand Up @@ -262,6 +273,16 @@ protected StudyDetail createNewStudyDetail(Long week, Study study, LocalDateTime
return studyDetailRepository.save(studyDetail);
}

protected StudyHistory createStudyHistory(Member member, Study study) {
StudyHistory studyHistory = StudyHistory.create(member, study);
return studyHistoryRepository.save(studyHistory);
}

protected StudyAchievement createStudyAchievement(Member member, Study study, AchievementType achievementType) {
StudyAchievement studyAchievement = StudyAchievement.create(member, study, achievementType);
return studyAchievementRepository.save(studyAchievement);
}

protected StudyDetail publishAssignment(StudyDetail studyDetail) {
studyDetail.publishAssignment(ASSIGNMENT_TITLE, studyDetail.getPeriod().getEndDate(), DESCRIPTION_LINK);
return studyDetailRepository.save(studyDetail);
Expand Down

0 comments on commit f12092a

Please sign in to comment.