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

Merged
merged 11 commits into from
Aug 2, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class StudyMentorController {
private final StudyMentorService studyMentorService;

@Operation(summary = "스터디 과제 개설", description = "멘토만 과제를 개설할 수 있습니다.")
@PutMapping("/assignment/{assignmentId}")
@PutMapping("/assignments/{assignmentId}")
public ResponseEntity<Void> createStudyAssignment(
@PathVariable Long assignmentId, @Valid @RequestBody AssignmentCreateRequest request) {
return null;
Expand All @@ -39,4 +39,11 @@ public ResponseEntity<AssignmentResponse> getStudyAssignment(@PathVariable Long
AssignmentResponse response = studyMentorService.getAssignment(studyDetailId);
return ResponseEntity.ok(response);
}

@Operation(summary = "스터디 과제 휴강 처리", description = "해당 주차 과제를 휴강 처리합니다.")
@PatchMapping("/assignments/{studyDetailId}/cancel")
public ResponseEntity<Void> cancelStudyAssignment(@PathVariable Long studyDetailId) {
studyMentorService.cancelStudyAssignment(studyDetailId);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

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

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.study.dao.StudyDetailRepository;
import com.gdschongik.gdsc.domain.study.domain.StudyDetail;
import com.gdschongik.gdsc.domain.study.domain.StudyDetailValidator;
import com.gdschongik.gdsc.domain.study.dto.response.AssignmentResponse;
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;
Expand All @@ -15,9 +18,12 @@
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class StudyMentorService {

private final MemberUtil memberUtil;
private final StudyDetailRepository studyDetailRepository;
private final StudyDetailValidator studyDetailValidator;

@Transactional(readOnly = true)
public List<AssignmentResponse> getWeeklyAssignments(Long studyId) {
Expand All @@ -32,4 +38,19 @@ public AssignmentResponse getAssignment(Long studyDetailId) {
.orElseThrow(() -> new CustomException(STUDY_DETAIL_NOT_FOUND));
return AssignmentResponse.from(studyDetail);
}

@Transactional
public void cancelStudyAssignment(Long studyDetailId) {
Member currentMember = memberUtil.getCurrentMember();
StudyDetail studyDetail = studyDetailRepository
.findById(studyDetailId)
.orElseThrow(() -> new CustomException(STUDY_DETAIL_NOT_FOUND));

studyDetailValidator.validateCancelStudyAssignment(currentMember, studyDetail);

studyDetail.cancelAssignment();
Sangwook02 marked this conversation as resolved.
Show resolved Hide resolved
studyDetailRepository.save(studyDetail);

log.info("[StudyMentorService] 과제 휴강 처리: studyDetailId={}", studyDetail.getId());
}
Sangwook02 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,8 @@ public static StudyDetail createStudyDetail(Study study, Long week, String atten
.assignment(Assignment.createEmptyAssignment())
.build();
}

public void cancelAssignment() {
assignment = Assignment.cancelAssignment();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.gdschongik.gdsc.domain.study.domain;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.global.annotation.DomainService;
import com.gdschongik.gdsc.global.exception.CustomException;
import com.gdschongik.gdsc.global.exception.ErrorCode;

@DomainService
public class StudyDetailValidator {

public void validateCancelStudyAssignment(Member currentMember, StudyDetail studyDetail) {
// 멘토가 아니라면 과제를 휴강처리 할 수 없다.
if (!currentMember.equals(studyDetail.getStudy().getMentor())) {
Copy link
Member

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.

수많은 멘토 중 해당 스터디의 멘토만 수정 가능해야 한다는 맥락에서 이런 검증을 넣었습니다

Copy link
Member

Choose a reason for hiding this comment

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

그리고 그냥 든 생각인데
validateCancel
validatePublish
이렇게 여러개 생성하지 말고 그냥 공통으로 쓸 수 있게 구현하는게 좋지 않을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

StudyDetail 관련된 모든 수정시 수많은 멘토 중 해당 스터디의 멘토만 수정 가능해야 한다라는 검증을 해야할텐데,
그렇다면 제가 지금 구현해둔걸 나중에 private으로 메서드로 추출해서 각 validate 메서드에서 호출하면 될 것 같습니다.
항상 검증할게 이것만 있는건 아니니까요!

Copy link
Member

Choose a reason for hiding this comment

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

아아~ 알겠습니다 그럼 저도 그냥 validatePublish~~이런 메소드 저기다가 생성해서 과제 생성 api pr에 올리겠습니다~

Copy link
Member Author

Choose a reason for hiding this comment

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

제가 메서드 추출해뒀어요.
pull 받아서 활용하시면 될 것 같아요

Copy link
Member

Choose a reason for hiding this comment

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

그럼 머지 되면 언급만 해주세용~

throw new CustomException(ErrorCode.STUDY_DETAIL_NOT_MODIFIABLE_INVALID_ROLE);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.gdschongik.gdsc.domain.study.domain.vo;

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

import com.gdschongik.gdsc.domain.study.domain.Difficulty;
import com.gdschongik.gdsc.domain.study.domain.StudyStatus;
import jakarta.persistence.Column;
Expand Down Expand Up @@ -46,6 +48,10 @@ private Assignment(
}

public static Assignment createEmptyAssignment() {
return Assignment.builder().status(StudyStatus.NONE).build();
return Assignment.builder().status(NONE).build();
}

public static Assignment cancelAssignment() {
return Assignment.builder().status(CANCELLED).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public enum ErrorCode {

// StudyDetail
STUDY_DETAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 상세 정보입니다."),
STUDY_DETAIL_NOT_MODIFIABLE_INVALID_ROLE(HttpStatus.FORBIDDEN, "수정할 수 있는 권한이 없습니다."),

// StudyHistory
STUDY_HISTORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 수강 기록입니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.gdschongik.gdsc.domain.study.application;

import static org.assertj.core.api.Assertions.*;

import com.gdschongik.gdsc.domain.member.domain.MemberRole;
import com.gdschongik.gdsc.domain.study.dao.StudyDetailRepository;
import com.gdschongik.gdsc.domain.study.domain.StudyDetail;
import com.gdschongik.gdsc.domain.study.domain.StudyStatus;
import com.gdschongik.gdsc.helper.IntegrationTest;
import java.time.LocalDateTime;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

public class StudyMentorServiceTest extends IntegrationTest {

@Autowired
private StudyMentorService studyMentorService;

@Autowired
private StudyDetailRepository studyDetailRepository;

@Nested
class 스터디_과제_휴강_처리시 {

@Test
void 성공한다() {
// given
LocalDateTime now = LocalDateTime.now();
StudyDetail studyDetail = createStudyDetail(now, now.plusDays(7));
logoutAndReloginAs(studyDetail.getStudy().getMentor().getId(), MemberRole.ASSOCIATE);

// when
studyMentorService.cancelStudyAssignment(studyDetail.getId());

// then
StudyDetail cancelledStudyDetail =
studyDetailRepository.findById(studyDetail.getId()).get();
assertThat(cancelledStudyDetail.getAssignment().getStatus()).isEqualTo(StudyStatus.CANCELLED);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.gdschongik.gdsc.domain.study.domain;

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.helper.FixtureHelper;
import java.time.LocalDateTime;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

public class StudyDetailTest {

@Nested
class 과제_휴강_처리시 {

FixtureHelper fixtureHelper = new FixtureHelper();

@Test
void 과제_상태가_휴강이_된다() {
// given
Member mentor = fixtureHelper.createAssociateMember(1L);
LocalDateTime now = LocalDateTime.now();
Study study = fixtureHelper.createStudy(
mentor,
Period.createPeriod(now.plusDays(5), now.plusDays(10)),
Period.createPeriod(now.minusDays(5), now));
StudyDetail studyDetail = fixtureHelper.createStudyDetail(study, now, now.plusDays(7));

// when
studyDetail.cancelAssignment();

// then
assertThat(studyDetail.getAssignment().getStatus()).isEqualTo(StudyStatus.CANCELLED);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.gdschongik.gdsc.domain.study.domain;

import static com.gdschongik.gdsc.global.exception.ErrorCode.*;
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.global.exception.CustomException;
import com.gdschongik.gdsc.helper.FixtureHelper;
import java.time.LocalDateTime;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

public class StudyDetailValidatorTest {

FixtureHelper fixtureHelper = new FixtureHelper();
StudyDetailValidator studyDetailValidator = new StudyDetailValidator();

@Nested
class 과제_휴강_처리시 {

@Test
void 멘토가_아니라면_실패한다() {
// given
LocalDateTime now = LocalDateTime.now();
Member mentor = fixtureHelper.createAssociateMember(1L);
Study study = fixtureHelper.createStudy(
mentor,
Period.createPeriod(now.plusDays(5), now.plusDays(10)),
Period.createPeriod(now.minusDays(5), now));
StudyDetail studyDetail = fixtureHelper.createStudyDetail(study, now, now.plusDays(7));
Member anotherMember = fixtureHelper.createAssociateMember(2L);

// when & then
assertThatThrownBy(() -> studyDetailValidator.validateCancelStudyAssignment(anotherMember, studyDetail))
.isInstanceOf(CustomException.class)
.hasMessage(STUDY_DETAIL_NOT_MODIFIABLE_INVALID_ROLE.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ private StudyConstant() {}
public static final DayOfWeek DAY_OF_WEEK = DayOfWeek.FRIDAY;
public static final LocalTime STUDY_START_TIME = LocalTime.of(19, 0, 0);
public static final LocalTime STUDY_END_TIME = LocalTime.of(20, 0, 0);

// StudyDetail
public static final String ATTENDANCE_NUMBER = "1234";
}
5 changes: 5 additions & 0 deletions src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.gdschongik.gdsc.domain.recruitment.domain.RoundType;
import com.gdschongik.gdsc.domain.recruitment.domain.vo.Period;
import com.gdschongik.gdsc.domain.study.domain.Study;
import com.gdschongik.gdsc.domain.study.domain.StudyDetail;
import java.time.LocalDateTime;
import org.springframework.test.util.ReflectionTestUtils;

Expand Down Expand Up @@ -79,4 +80,8 @@ public Study createStudy(Member mentor, Period period, Period applicationPeriod)
STUDY_START_TIME,
STUDY_END_TIME);
}

public StudyDetail createStudyDetail(Study study, LocalDateTime startDate, LocalDateTime endDate) {
return StudyDetail.createStudyDetail(study, 1L, ATTENDANCE_NUMBER, Period.createPeriod(startDate, endDate));
}
}
40 changes: 40 additions & 0 deletions src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static com.gdschongik.gdsc.global.common.constant.MemberConstant.*;
import static com.gdschongik.gdsc.global.common.constant.RecruitmentConstant.*;
import static com.gdschongik.gdsc.global.common.constant.SemesterConstant.*;
import static com.gdschongik.gdsc.global.common.constant.StudyConstant.*;
import static org.mockito.Mockito.*;

import com.gdschongik.gdsc.domain.common.model.SemesterType;
Expand All @@ -25,6 +26,10 @@
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.StudyDetailRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyRepository;
import com.gdschongik.gdsc.domain.study.domain.Study;
import com.gdschongik.gdsc.domain.study.domain.StudyDetail;
import com.gdschongik.gdsc.global.security.PrincipalDetails;
import com.gdschongik.gdsc.infra.feign.payment.client.PaymentClient;
import java.time.LocalDateTime;
Expand Down Expand Up @@ -62,6 +67,12 @@ public abstract class IntegrationTest {
@Autowired
protected RecruitmentRoundRepository recruitmentRoundRepository;

@Autowired
protected StudyRepository studyRepository;

@Autowired
protected StudyDetailRepository studyDetailRepository;

@MockBean
protected OnboardingRecruitmentService onboardingRecruitmentService;

Expand Down Expand Up @@ -160,4 +171,33 @@ protected IssuedCoupon createAndIssue(Money money, Member member) {
IssuedCoupon issuedCoupon = IssuedCoupon.issue(coupon, member);
return issuedCouponRepository.save(issuedCoupon);
}

protected Study createStudy(Member mentor, Period period, Period applicationPeriod) {
Study study = Study.createStudy(
ACADEMIC_YEAR,
SEMESTER_TYPE,
mentor,
period,
applicationPeriod,
TOTAL_WEEK,
ONLINE_STUDY,
DAY_OF_WEEK,
STUDY_START_TIME,
STUDY_END_TIME);

return studyRepository.save(study);
}

protected StudyDetail createStudyDetail(LocalDateTime startDate, LocalDateTime endDate) {
Member mentor = createAssociateMember();
LocalDateTime now = LocalDateTime.now();
Study study = createStudy(
mentor,
Period.createPeriod(now.plusDays(5), now.plusDays(10)),
Period.createPeriod(now.minusDays(5), now));

StudyDetail studyDetail =
StudyDetail.createStudyDetail(study, 1L, ATTENDANCE_NUMBER, Period.createPeriod(startDate, endDate));
return studyDetailRepository.save(studyDetail);
}
}