diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudyMentorController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudyMentorController.java index 6bac3547e..cdbd1952f 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudyMentorController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudyMentorController.java @@ -20,7 +20,7 @@ public class StudyMentorController { private final StudyMentorService studyMentorService; @Operation(summary = "스터디 과제 개설", description = "멘토만 과제를 개설할 수 있습니다.") - @PutMapping("/assignment/{assignmentId}") + @PutMapping("/assignments/{assignmentId}") public ResponseEntity createStudyAssignment( @PathVariable Long assignmentId, @Valid @RequestBody AssignmentCreateRequest request) { return null; @@ -39,4 +39,11 @@ public ResponseEntity getStudyAssignment(@PathVariable Long AssignmentResponse response = studyMentorService.getAssignment(studyDetailId); return ResponseEntity.ok(response); } + + @Operation(summary = "스터디 과제 휴강 처리", description = "해당 주차 과제를 휴강 처리합니다.") + @PatchMapping("/assignments/{studyDetailId}/cancel") + public ResponseEntity cancelStudyAssignment(@PathVariable Long studyDetailId) { + studyMentorService.cancelStudyAssignment(studyDetailId); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyMentorService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyMentorService.java index 445a11d92..df839c803 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyMentorService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyMentorService.java @@ -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; @@ -17,7 +20,9 @@ @RequiredArgsConstructor public class StudyMentorService { + private final MemberUtil memberUtil; private final StudyDetailRepository studyDetailRepository; + private final StudyDetailValidator studyDetailValidator; @Transactional(readOnly = true) public List getWeeklyAssignments(Long studyId) { @@ -32,4 +37,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(); + studyDetailRepository.save(studyDetail); + + log.info("[StudyMentorService] 과제 휴강 처리: studyDetailId={}", studyDetail.getId()); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyDetail.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyDetail.java index 154ce8a90..bf76a233b 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyDetail.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyDetail.java @@ -69,4 +69,8 @@ public static StudyDetail createStudyDetail(Study study, Long week, String atten .assignment(Assignment.createEmptyAssignment()) .build(); } + + public void cancelAssignment() { + assignment = Assignment.cancelAssignment(); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidator.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidator.java new file mode 100644 index 000000000..ea9992eb7 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidator.java @@ -0,0 +1,21 @@ +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) { + validateMemberIsMentor(currentMember, studyDetail); + } + + // 멘토가 아니라면 과제를 휴강처리 할 수 없다. + private void validateMemberIsMentor(Member member, StudyDetail studyDetail) { + if (!member.equals(studyDetail.getStudy().getMentor())) { + throw new CustomException(ErrorCode.STUDY_DETAIL_NOT_MODIFIABLE_INVALID_ROLE); + } + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/vo/Assignment.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/vo/Assignment.java index c426f35d6..9228bd4c0 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/vo/Assignment.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/vo/Assignment.java @@ -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; @@ -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(); } } diff --git a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java index 9678ba91d..5a5795cca 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -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, "존재하지 않는 스터디 수강 기록입니다."), diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/application/StudyMentorServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/application/StudyMentorServiceTest.java new file mode 100644 index 000000000..c4ecb95ff --- /dev/null +++ b/src/test/java/com/gdschongik/gdsc/domain/study/application/StudyMentorServiceTest.java @@ -0,0 +1,50 @@ +package com.gdschongik.gdsc.domain.study.application; + +import static org.assertj.core.api.Assertions.*; + +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.member.domain.MemberRole; +import com.gdschongik.gdsc.domain.recruitment.domain.vo.Period; +import com.gdschongik.gdsc.domain.study.dao.StudyDetailRepository; +import com.gdschongik.gdsc.domain.study.domain.Study; +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(); + Member mentor = createAssociateMember(); + Study study = createStudy( + mentor, + Period.createPeriod(now.plusDays(5), now.plusDays(10)), + Period.createPeriod(now.minusDays(5), now)); + StudyDetail studyDetail = createStudyDetail(study, 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); + } + } +} diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailTest.java new file mode 100644 index 000000000..13592767f --- /dev/null +++ b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailTest.java @@ -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); + } + } +} diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidatorTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidatorTest.java new file mode 100644 index 000000000..0c933bc6c --- /dev/null +++ b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidatorTest.java @@ -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()); + } + } +} diff --git a/src/test/java/com/gdschongik/gdsc/global/common/constant/StudyConstant.java b/src/test/java/com/gdschongik/gdsc/global/common/constant/StudyConstant.java index e263e49b8..0cc120400 100644 --- a/src/test/java/com/gdschongik/gdsc/global/common/constant/StudyConstant.java +++ b/src/test/java/com/gdschongik/gdsc/global/common/constant/StudyConstant.java @@ -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"; } diff --git a/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java b/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java index b0849699c..f6a4c1cdf 100644 --- a/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java +++ b/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java @@ -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; @@ -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)); + } } diff --git a/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java b/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java index 5a132ee69..ee4635223 100644 --- a/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java +++ b/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java @@ -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; @@ -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; @@ -62,6 +67,12 @@ public abstract class IntegrationTest { @Autowired protected RecruitmentRoundRepository recruitmentRoundRepository; + @Autowired + protected StudyRepository studyRepository; + + @Autowired + protected StudyDetailRepository studyDetailRepository; + @MockBean protected OnboardingRecruitmentService onboardingRecruitmentService; @@ -160,4 +171,26 @@ 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(Study study, LocalDateTime startDate, LocalDateTime endDate) { + StudyDetail studyDetail = + StudyDetail.createStudyDetail(study, 1L, ATTENDANCE_NUMBER, Period.createPeriod(startDate, endDate)); + return studyDetailRepository.save(studyDetail); + } }