From e1e4cf5f60b0a3b7fcffbfaee7e9d4fb036fa013 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:03:10 +0900 Subject: [PATCH 01/22] =?UTF-8?q?feat:=20=EA=B3=BC=EC=A0=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80=20(#543)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 과제 조회 api 추가 * refactor: 트랜잭셔널 어노테이션을 메서드 레벨로 변경 --- .../study/api/StudyMentorController.java | 19 ++++++++++ .../study/application/StudyMentorService.java | 35 +++++++++++++++++++ .../study/dao/StudyDetailRepository.java | 6 +++- .../dto/response/AssignmentResponse.java | 23 ++++++++++++ .../gdsc/global/exception/ErrorCode.java | 3 ++ 5 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/application/StudyMentorService.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentResponse.java 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 5e6f2a344..6bac3547e 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 @@ -1,9 +1,12 @@ package com.gdschongik.gdsc.domain.study.api; +import com.gdschongik.gdsc.domain.study.application.StudyMentorService; import com.gdschongik.gdsc.domain.study.domain.request.AssignmentCreateRequest; +import com.gdschongik.gdsc.domain.study.dto.response.AssignmentResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -14,10 +17,26 @@ @RequiredArgsConstructor public class StudyMentorController { + private final StudyMentorService studyMentorService; + @Operation(summary = "스터디 과제 개설", description = "멘토만 과제를 개설할 수 있습니다.") @PutMapping("/assignment/{assignmentId}") public ResponseEntity createStudyAssignment( @PathVariable Long assignmentId, @Valid @RequestBody AssignmentCreateRequest request) { return null; } + + @Operation(summary = "스터디 주차별 과제 목록 조회", description = "주차별 스터디 과제 목록을 조회합니다.") + @GetMapping("/assignments/{studyId}") + public ResponseEntity> getWeeklyAssignments(@PathVariable Long studyId) { + List response = studyMentorService.getWeeklyAssignments(studyId); + return ResponseEntity.ok(response); + } + + @Operation(summary = "스터디 과제 상세 조회", description = "멘토가 자신의 스터디 과제를 조회합니다.") + @GetMapping("/assignments/{studyDetailId}") + public ResponseEntity getStudyAssignment(@PathVariable Long studyDetailId) { + AssignmentResponse response = studyMentorService.getAssignment(studyDetailId); + return ResponseEntity.ok(response); + } } 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 new file mode 100644 index 000000000..445a11d92 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyMentorService.java @@ -0,0 +1,35 @@ +package com.gdschongik.gdsc.domain.study.application; + +import static com.gdschongik.gdsc.global.exception.ErrorCode.*; + +import com.gdschongik.gdsc.domain.study.dao.StudyDetailRepository; +import com.gdschongik.gdsc.domain.study.domain.StudyDetail; +import com.gdschongik.gdsc.domain.study.dto.response.AssignmentResponse; +import com.gdschongik.gdsc.global.exception.CustomException; +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 StudyMentorService { + + private final StudyDetailRepository studyDetailRepository; + + @Transactional(readOnly = true) + public List getWeeklyAssignments(Long studyId) { + List studyDetails = studyDetailRepository.findAllByStudyId(studyId); + return studyDetails.stream().map(AssignmentResponse::from).toList(); + } + + @Transactional(readOnly = true) + public AssignmentResponse getAssignment(Long studyDetailId) { + StudyDetail studyDetail = studyDetailRepository + .findById(studyDetailId) + .orElseThrow(() -> new CustomException(STUDY_DETAIL_NOT_FOUND)); + return AssignmentResponse.from(studyDetail); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyDetailRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyDetailRepository.java index fe5910c0d..350872753 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyDetailRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyDetailRepository.java @@ -1,6 +1,10 @@ package com.gdschongik.gdsc.domain.study.dao; import com.gdschongik.gdsc.domain.study.domain.StudyDetail; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -public interface StudyDetailRepository extends JpaRepository {} +public interface StudyDetailRepository extends JpaRepository { + + List findAllByStudyId(Long studyId); +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentResponse.java new file mode 100644 index 000000000..33414fed9 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentResponse.java @@ -0,0 +1,23 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import com.gdschongik.gdsc.domain.study.domain.StudyDetail; +import com.gdschongik.gdsc.domain.study.domain.StudyStatus; +import com.gdschongik.gdsc.domain.study.domain.vo.Assignment; +import io.swagger.v3.oas.annotations.media.Schema; + +public record AssignmentResponse( + Long studyDetailId, + @Schema(description = "과제 제목") String title, + @Schema(description = "마감 기한") String deadline, + @Schema(description = "과제 명세 링크") String descriptionLink, + @Schema(description = "과제 상태") StudyStatus assignmentStatus) { + public static AssignmentResponse from(StudyDetail studyDetail) { + Assignment assignment = studyDetail.getAssignment(); + return new AssignmentResponse( + studyDetail.getId(), + assignment.getTitle(), + assignment.getDeadline().toString(), + assignment.getDescriptionLink(), + assignment.getStatus()); + } +} 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 843ecbbcb..9678ba91d 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -109,6 +109,9 @@ public enum ErrorCode { STUDY_NOT_APPLICABLE(HttpStatus.CONFLICT, "스터디 신청기간이 아닙니다."), STUDY_NOT_CANCELABLE_APPLICATION_PERIOD(HttpStatus.CONFLICT, "스터디 신청기간이 아니라면 취소할 수 없습니다."), + // StudyDetail + STUDY_DETAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 상세 정보입니다."), + // StudyHistory STUDY_HISTORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 수강 기록입니다."), STUDY_HISTORY_DUPLICATE(HttpStatus.CONFLICT, "이미 해당 스터디를 신청했습니다."), From 82008b35ab976a427a96b410edba5bb41313ac45 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Fri, 2 Aug 2024 18:46:10 +0900 Subject: [PATCH 02/22] =?UTF-8?q?fix:=20=ED=95=99=EB=B2=88=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=A3=BC=EB=AC=B8=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#557)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: 학번으로 조회시 발생하는 에러 수정 --- .../gdsc/domain/order/dao/OrderCustomRepositoryImpl.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/gdschongik/gdsc/domain/order/dao/OrderCustomRepositoryImpl.java b/src/main/java/com/gdschongik/gdsc/domain/order/dao/OrderCustomRepositoryImpl.java index 35c49206e..d2d7d7b9b 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/order/dao/OrderCustomRepositoryImpl.java +++ b/src/main/java/com/gdschongik/gdsc/domain/order/dao/OrderCustomRepositoryImpl.java @@ -68,6 +68,8 @@ private List getIdsByQueryOption( .from(order) .innerJoin(recruitmentRound) .on(order.recruitmentRoundId.eq(recruitmentRound.id)) + .innerJoin(member) + .on(order.memberId.eq(member.id)) .where(matchesOrderQueryOption(queryOption), predicate) .orderBy(orderSpecifiers) .fetch(); From 285e23fc06fa001a6c9a537981979cab213e4658 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Fri, 2 Aug 2024 19:25:50 +0900 Subject: [PATCH 03/22] =?UTF-8?q?refactor:=20=EC=9E=AC=ED=95=99=EC=83=9D?= =?UTF-8?q?=20=EB=A9=94=EC=9D=BC=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=97=90=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=A0=81=EC=9A=A9=20(#552)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../email/application/UnivEmailVerificationServiceTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/gdschongik/gdsc/domain/email/application/UnivEmailVerificationServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/email/application/UnivEmailVerificationServiceTest.java index 8eb42e455..03b506776 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/email/application/UnivEmailVerificationServiceTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/email/application/UnivEmailVerificationServiceTest.java @@ -38,7 +38,7 @@ class 재학생_메일_인증시 { @Test void 레디스에_이메일인증정보가_존재하지_않으면_실패한다() { // given - Member member = Member.createGuestMember(OAUTH_ID); + Member member = createGuestMember(); memberRepository.save(member); String verificationToken = emailVerificationTokenUtil.generateEmailVerificationToken(member.getId(), UNIV_EMAIL); @@ -53,8 +53,7 @@ class 재학생_메일_인증시 { @Test void 인증토큰과_레디스에_존재하는_인증정보의_토큰이_다르면_실패한다() { // given - // TODO: 아래 두줄 createGuestMember로 대체하기 - Member member = memberRepository.save(Member.createGuestMember(OAUTH_ID)); + Member member = createGuestMember(); logoutAndReloginAs(member.getId(), member.getRole()); // when From 6acc16c16efe55c08add764e3da7e7945b1f80cf Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Fri, 2 Aug 2024 22:13:40 +0900 Subject: [PATCH 04/22] =?UTF-8?q?feat:=20=EA=B3=BC=EC=A0=9C=20=ED=9C=B4?= =?UTF-8?q?=EA=B0=95=20=EC=B2=98=EB=A6=AC=20API=20=EC=B6=94=EA=B0=80=20(#5?= =?UTF-8?q?42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 스터디 휴강 처리 api 추가 * fix: 엔드포인트 수정 * fix: 에러코드 수정 * refactor: study를 외부에서 받아서 사용하도록 수정 * feat: 현재 멤버의 멘토 여부 검증 추가 * refactor: save 호출하도록 수정 * fix: 시큐리티 설정 추가 * remove: 클래스 레벨 트랜잭셔널 어노테이션 제거 * refactor: 현멤버의 멘토 여부 검증 로직을 메서드로 추출 * refactor: 스터디를 주입받도록 수정 --- .../study/api/StudyMentorController.java | 9 +++- .../study/application/StudyMentorService.java | 20 ++++++++ .../gdsc/domain/study/domain/StudyDetail.java | 4 ++ .../study/domain/StudyDetailValidator.java | 21 ++++++++ .../domain/study/domain/vo/Assignment.java | 8 ++- .../gdsc/global/exception/ErrorCode.java | 1 + .../application/StudyMentorServiceTest.java | 50 +++++++++++++++++++ .../domain/study/domain/StudyDetailTest.java | 37 ++++++++++++++ .../domain/StudyDetailValidatorTest.java | 40 +++++++++++++++ .../global/common/constant/StudyConstant.java | 3 ++ .../gdschongik/gdsc/helper/FixtureHelper.java | 5 ++ .../gdsc/helper/IntegrationTest.java | 33 ++++++++++++ 12 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidator.java create mode 100644 src/test/java/com/gdschongik/gdsc/domain/study/application/StudyMentorServiceTest.java create mode 100644 src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailTest.java create mode 100644 src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidatorTest.java 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); + } } From 769f67026c282a16355c181c23833abd5369952a Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Sat, 3 Aug 2024 15:15:39 +0900 Subject: [PATCH 05/22] =?UTF-8?q?feat:=20=EB=A9=A4=EB=B2=84=EA=B0=80=20?= =?UTF-8?q?=EC=97=AC=EB=9F=AC=20=EC=A2=85=EB=A5=98=EC=9D=98=20=EC=97=AD?= =?UTF-8?q?=ED=95=A0=EC=9D=84=20=EA=B0=80=EC=A7=88=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20(#561)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 관리자 역할 추가 * feat: ADMIN을 일반 역할에서 관리자 역할로 변경 * feat: 스터디 역할 추가 * feat: 멤버 도메인에 역할 필드 추가 * feat: 멘토 역할 할당 메서드 추가 * refactor: 정적 팩토리 메서드를 사용하도록 개선 * feat: PrincipalDetails에 관리역할 및 멘토역할 추가 * feat: 멤버 정보 저장하는 DTO 추가 * refactor: AuthInfo를 인자로 받고 파싱 로직을 분리하도록 변경 * refactor: 엑세스 토큰 생성 시 AuthInfo 받도록 변경 * refactor: 엑세스 토큰의 내부 멤버 정보를 AuthInfo로 변경 * feat: 신규 JWT 클레임 관련 상수 추가 * refactor: 임시 토큰 발생 로직에도 AuthInfo 추가 * refactor: CustomOAuth2User의 내부 필드를 AuthInfo로 변경 * refactor: 토큰 재발급 로직 수정 * refactor: 인증주체가 여러 개의 권한을 가질 수 있도록 변경 * test: 통합 테스트 템플릿 인증주체 변경 반영 * fix: 도커 컴포즈 CI 명령어 수정 * chore: 테스트 도커 컴포즈 제거 * refactor: 도커 컴포즈 사용하도록 롤백 및 database cleaner 실행할때 레디스도 같이 초기화하도록 변경 * refactor: workflow 수정 * refactor: workflow 수정 * refactor: RedisCleaner afterPropertiesSet로 값들 초기화 하도록 변경 --------- Co-authored-by: seulgi99 --- .github/workflows/develop_build_deploy.yml | 2 +- .github/workflows/production_build_deploy.yml | 8 ++-- .../workflows/pull_request_gradle_build.yml | 2 +- build.gradle | 1 - .../domain/auth/application/JwtService.java | 21 ++++++-- .../gdsc/domain/auth/dto/AccessTokenDto.java | 4 +- .../discord/domain/DiscordValidator.java | 4 +- .../application/OnboardingMemberService.java | 3 +- .../gdsc/domain/member/domain/Member.java | 21 ++++++++ .../member/domain/MemberManageRole.java | 13 +++++ .../gdsc/domain/member/domain/MemberRole.java | 7 ++- .../domain/member/domain/MemberStudyRole.java | 13 +++++ .../common/constant/SecurityConstant.java | 2 + .../global/security/CustomOAuth2User.java | 7 +-- .../global/security/CustomSuccessHandler.java | 6 +-- .../gdsc/global/security/JwtFilter.java | 12 ++--- .../gdsc/global/security/MemberAuthInfo.java | 15 ++++++ .../global/security/PrincipalDetails.java | 26 ++++++++-- .../gdschongik/gdsc/global/util/JwtUtil.java | 48 ++++++++++++------- .../gdsc/config/TestRedisConfig.java | 26 ++++------ .../UnivEmailVerificationServiceTest.java | 3 -- .../gdsc/helper/IntegrationTest.java | 10 +++- .../gdschongik/gdsc/helper/RedisCleaner.java | 27 +++++++++++ .../gdsc/helper/RepositoryTest.java | 7 ++- 24 files changed, 207 insertions(+), 81 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberManageRole.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberStudyRole.java create mode 100644 src/main/java/com/gdschongik/gdsc/global/security/MemberAuthInfo.java create mode 100644 src/test/java/com/gdschongik/gdsc/helper/RedisCleaner.java diff --git a/.github/workflows/develop_build_deploy.yml b/.github/workflows/develop_build_deploy.yml index 79250f20e..a2d48e3bc 100644 --- a/.github/workflows/develop_build_deploy.yml +++ b/.github/workflows/develop_build_deploy.yml @@ -38,7 +38,7 @@ jobs: # Redis 컨테이너 실행 - name: Start containers - run: docker-compose -f ./docker-compose-test.yaml up -d + run: docker compose -f docker-compose-test.yaml up -d # Gradle 빌드 - name: Setup Gradle diff --git a/.github/workflows/production_build_deploy.yml b/.github/workflows/production_build_deploy.yml index 15ff83407..4feae8fcf 100644 --- a/.github/workflows/production_build_deploy.yml +++ b/.github/workflows/production_build_deploy.yml @@ -36,14 +36,14 @@ jobs: java-version: ${{ matrix.java-version }} distribution: ${{ matrix.distribution }} + # Redis 컨테이너 실행 + - name: Start containers + run: docker compose -f docker-compose-test.yaml up -d + # Gradlew 실행 허용 - name: Run chmod to make gradlew executable run: chmod +x ./gradlew - # Redis 컨테이너 실행 - - name: Start containers - run: docker-compose -f ./docker-compose-test.yaml up -d - # Gradle 빌드 - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 diff --git a/.github/workflows/pull_request_gradle_build.yml b/.github/workflows/pull_request_gradle_build.yml index 93673e0c0..5fe92cce3 100644 --- a/.github/workflows/pull_request_gradle_build.yml +++ b/.github/workflows/pull_request_gradle_build.yml @@ -26,7 +26,7 @@ jobs: # Redis 컨테이너 실행 - name: Start containers - run: docker-compose -f ./docker-compose-test.yaml up -d + run: docker compose -f docker-compose-test.yaml up -d - name: Setup Gradle id: gradle diff --git a/build.gradle b/build.gradle index dfddafb7e..f3579ce80 100644 --- a/build.gradle +++ b/build.gradle @@ -60,7 +60,6 @@ dependencies { // Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' - testImplementation 'org.testcontainers:testcontainers' // Querydsl implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' diff --git a/src/main/java/com/gdschongik/gdsc/domain/auth/application/JwtService.java b/src/main/java/com/gdschongik/gdsc/domain/auth/application/JwtService.java index 4a9d96c1e..e6706422e 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/auth/application/JwtService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/auth/application/JwtService.java @@ -6,8 +6,12 @@ import com.gdschongik.gdsc.domain.auth.domain.RefreshToken; import com.gdschongik.gdsc.domain.auth.dto.AccessTokenDto; import com.gdschongik.gdsc.domain.auth.dto.RefreshTokenDto; +import com.gdschongik.gdsc.domain.member.domain.MemberManageRole; import com.gdschongik.gdsc.domain.member.domain.MemberRole; +import com.gdschongik.gdsc.domain.member.domain.MemberStudyRole; +import com.gdschongik.gdsc.global.security.MemberAuthInfo; import com.gdschongik.gdsc.global.util.JwtUtil; +import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -22,8 +26,8 @@ public class JwtService { private final JwtUtil jwtUtil; private final RefreshTokenRepository refreshTokenRepository; - public AccessTokenDto createAccessToken(Long memberId, MemberRole memberRole) { - return jwtUtil.generateAccessToken(memberId, memberRole); + public AccessTokenDto createAccessToken(MemberAuthInfo authInfo) { + return jwtUtil.generateAccessToken(authInfo); } public RefreshTokenDto createRefreshToken(Long memberId) { @@ -86,9 +90,16 @@ public AccessTokenDto reissueAccessTokenIfExpired(String accessTokenValue) { jwtUtil.parseAccessToken(accessTokenValue); return null; } catch (ExpiredJwtException e) { - Long memberId = Long.parseLong(e.getClaims().getSubject()); - MemberRole memberRole = MemberRole.valueOf(e.getClaims().get(TOKEN_ROLE_NAME, String.class)); - return createAccessToken(memberId, memberRole); + Claims claims = e.getClaims(); + + Long memberId = Long.parseLong(claims.getSubject()); + MemberRole memberRole = MemberRole.valueOf(claims.get(TOKEN_ROLE_NAME, String.class)); + MemberManageRole memberManageRole = + MemberManageRole.valueOf(claims.get(TOKEN_MANAGE_ROLE_NAME, String.class)); + MemberStudyRole memberStudyRole = MemberStudyRole.valueOf(claims.get(TOKEN_STUDY_ROLE_NAME, String.class)); + var authInfo = new MemberAuthInfo(memberId, memberRole, memberManageRole, memberStudyRole); + + return createAccessToken(authInfo); } } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/auth/dto/AccessTokenDto.java b/src/main/java/com/gdschongik/gdsc/domain/auth/dto/AccessTokenDto.java index 503ab11b0..541c4e42c 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/auth/dto/AccessTokenDto.java +++ b/src/main/java/com/gdschongik/gdsc/domain/auth/dto/AccessTokenDto.java @@ -1,5 +1,5 @@ package com.gdschongik.gdsc.domain.auth.dto; -import com.gdschongik.gdsc.domain.member.domain.MemberRole; +import com.gdschongik.gdsc.global.security.MemberAuthInfo; -public record AccessTokenDto(Long memberId, MemberRole memberRole, String tokenValue) {} +public record AccessTokenDto(MemberAuthInfo authInfo, String tokenValue) {} diff --git a/src/main/java/com/gdschongik/gdsc/domain/discord/domain/DiscordValidator.java b/src/main/java/com/gdschongik/gdsc/domain/discord/domain/DiscordValidator.java index 31dafe58c..c8d376a3b 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/discord/domain/DiscordValidator.java +++ b/src/main/java/com/gdschongik/gdsc/domain/discord/domain/DiscordValidator.java @@ -3,7 +3,7 @@ import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import com.gdschongik.gdsc.domain.member.domain.Member; -import com.gdschongik.gdsc.domain.member.domain.MemberRole; +import com.gdschongik.gdsc.domain.member.domain.MemberManageRole; import com.gdschongik.gdsc.global.annotation.DomainService; import com.gdschongik.gdsc.global.exception.CustomException; @@ -32,7 +32,7 @@ public void validateVerifyDiscordCode( } public void validateAdminPermission(Member currentMember) { - if (!currentMember.getRole().equals(MemberRole.ADMIN)) { + if (!currentMember.getManageRole().equals(MemberManageRole.ADMIN)) { throw new CustomException(INVALID_ROLE); } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java index 75a493e20..d6f96c50a 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java @@ -22,6 +22,7 @@ import com.gdschongik.gdsc.domain.recruitment.application.OnboardingRecruitmentService; import com.gdschongik.gdsc.domain.recruitment.domain.RecruitmentRound; import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.security.MemberAuthInfo; import com.gdschongik.gdsc.global.util.EnvironmentUtil; import com.gdschongik.gdsc.global.util.MemberUtil; import java.util.Optional; @@ -88,7 +89,7 @@ public MemberTokenResponse createTemporaryToken(MemberTokenRequest request) { .findByOauthId(request.oauthId()) .orElseThrow(() -> new CustomException(MEMBER_NOT_FOUND)); - AccessTokenDto accessTokenDto = jwtService.createAccessToken(member.getId(), member.getRole()); + AccessTokenDto accessTokenDto = jwtService.createAccessToken(MemberAuthInfo.from(member)); RefreshTokenDto refreshTokenDto = jwtService.createRefreshToken(member.getId()); return new MemberTokenResponse(accessTokenDto.tokenValue(), refreshTokenDto.tokenValue()); diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java index c278899c8..3041479ca 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java @@ -1,6 +1,8 @@ package com.gdschongik.gdsc.domain.member.domain; +import static com.gdschongik.gdsc.domain.member.domain.MemberManageRole.*; import static com.gdschongik.gdsc.domain.member.domain.MemberRole.*; +import static com.gdschongik.gdsc.domain.member.domain.MemberStudyRole.*; import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import com.gdschongik.gdsc.domain.common.model.BaseEntity; @@ -34,6 +36,12 @@ public class Member extends BaseEntity { @Enumerated(EnumType.STRING) private MemberRole role; + @Enumerated(EnumType.STRING) + private MemberManageRole manageRole; + + @Enumerated(EnumType.STRING) + private MemberStudyRole studyRole; + @Enumerated(EnumType.STRING) private MemberStatus status; @@ -67,6 +75,8 @@ public class Member extends BaseEntity { @Builder(access = AccessLevel.PRIVATE) private Member( MemberRole role, + MemberManageRole manageRole, + MemberStudyRole studyRole, MemberStatus status, String name, String studentId, @@ -80,6 +90,8 @@ private Member( String univEmail, AssociateRequirement associateRequirement) { this.role = role; + this.manageRole = manageRole; + this.studyRole = studyRole; this.status = status; this.name = name; this.studentId = studentId; @@ -99,6 +111,8 @@ public static Member createGuestMember(String oauthId) { return Member.builder() .oauthId(oauthId) .role(GUEST) + .manageRole(NONE) + .studyRole(STUDENT) .status(MemberStatus.NORMAL) .associateRequirement(associateRequirement) .build(); @@ -268,6 +282,13 @@ public void demoteToGuest() { associateRequirement.demoteAssociateRequirement(); } + // 기타 역할 변경 로직 + + public void assignToMentor() { + validateStatusUpdatable(); + studyRole = MENTOR; + } + // 기타 상태 변경 로직 public void updateLastLoginAt() { diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberManageRole.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberManageRole.java new file mode 100644 index 000000000..14c6d3e94 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberManageRole.java @@ -0,0 +1,13 @@ +package com.gdschongik.gdsc.domain.member.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum MemberManageRole { + ADMIN("ROLE_ADMIN"), + NONE("ROLE_NONE"); + + private final String value; +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberRole.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberRole.java index 225332638..6a418cb71 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberRole.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberRole.java @@ -1,15 +1,14 @@ package com.gdschongik.gdsc.domain.member.domain; -import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.RequiredArgsConstructor; @Getter -@AllArgsConstructor +@RequiredArgsConstructor public enum MemberRole { GUEST("ROLE_GUEST"), ASSOCIATE("ROLE_ASSOCIATE"), - REGULAR("ROLE_REGULAR"), - ADMIN("ROLE_ADMIN"); + REGULAR("ROLE_REGULAR"); private final String value; } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberStudyRole.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberStudyRole.java new file mode 100644 index 000000000..d5ecca826 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberStudyRole.java @@ -0,0 +1,13 @@ +package com.gdschongik.gdsc.domain.member.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum MemberStudyRole { + MENTOR("ROLE_MENTOR"), + STUDENT("ROLE_STUDENT"); + + private final String value; +} diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java index ebb0770c3..1f72b661f 100644 --- a/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java @@ -6,6 +6,8 @@ public class SecurityConstant { public static final String ACCESS_TOKEN_PARAM = "access"; public static final String REFRESH_TOKEN_PARAM = "refresh"; public static final String TOKEN_ROLE_NAME = "role"; + public static final String TOKEN_MANAGE_ROLE_NAME = "manageRole"; + public static final String TOKEN_STUDY_ROLE_NAME = "studyRole"; public static final String GITHUB_NAME_ATTR_KEY = "id"; public static final String ACCESS_TOKEN_HEADER_PREFIX = "Bearer "; public static final String ACCESS_TOKEN_HEADER_NAME = "Authorization"; diff --git a/src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2User.java b/src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2User.java index b363fcce0..742cce47a 100644 --- a/src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2User.java +++ b/src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2User.java @@ -3,7 +3,6 @@ import static com.gdschongik.gdsc.global.common.constant.SecurityConstant.GITHUB_NAME_ATTR_KEY; import com.gdschongik.gdsc.domain.member.domain.Member; -import com.gdschongik.gdsc.domain.member.domain.MemberRole; import lombok.Getter; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User; @@ -11,14 +10,12 @@ @Getter public class CustomOAuth2User extends DefaultOAuth2User { - private final Long memberId; - private final MemberRole memberRole; + private final MemberAuthInfo memberAuthInfo; private final LandingStatus landingStatus; public CustomOAuth2User(OAuth2User oAuth2User, Member member) { super(oAuth2User.getAuthorities(), oAuth2User.getAttributes(), GITHUB_NAME_ATTR_KEY); - memberId = member.getId(); - memberRole = member.getRole(); + memberAuthInfo = MemberAuthInfo.from(member); landingStatus = LandingStatus.TO_DASHBOARD; } } diff --git a/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java b/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java index 02d6e822d..afd2a6860 100644 --- a/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java +++ b/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java @@ -38,9 +38,9 @@ public void onAuthenticationSuccess( CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal(); // 토큰 생성 후 쿠키에 저장 - AccessTokenDto accessTokenDto = - jwtService.createAccessToken(oAuth2User.getMemberId(), oAuth2User.getMemberRole()); - RefreshTokenDto refreshTokenDto = jwtService.createRefreshToken(oAuth2User.getMemberId()); + MemberAuthInfo memberAuthInfo = oAuth2User.getMemberAuthInfo(); + AccessTokenDto accessTokenDto = jwtService.createAccessToken(memberAuthInfo); + RefreshTokenDto refreshTokenDto = jwtService.createRefreshToken(memberAuthInfo.memberId()); cookieUtil.addTokenCookies(response, accessTokenDto.tokenValue(), refreshTokenDto.tokenValue()); // 임시로 헤더에 엑세스 토큰 추가 diff --git a/src/main/java/com/gdschongik/gdsc/global/security/JwtFilter.java b/src/main/java/com/gdschongik/gdsc/global/security/JwtFilter.java index 2d8c664de..dab5c451a 100644 --- a/src/main/java/com/gdschongik/gdsc/global/security/JwtFilter.java +++ b/src/main/java/com/gdschongik/gdsc/global/security/JwtFilter.java @@ -5,7 +5,6 @@ import com.gdschongik.gdsc.domain.auth.application.JwtService; import com.gdschongik.gdsc.domain.auth.dto.AccessTokenDto; import com.gdschongik.gdsc.domain.auth.dto.RefreshTokenDto; -import com.gdschongik.gdsc.domain.member.domain.MemberRole; import com.gdschongik.gdsc.global.common.constant.JwtConstant; import com.gdschongik.gdsc.global.util.CookieUtil; import jakarta.servlet.FilterChain; @@ -42,7 +41,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (accessTokenHeaderValue != null) { AccessTokenDto accessTokenDto = jwtService.retrieveAccessToken(accessTokenHeaderValue); if (accessTokenDto != null) { - setAuthenticationToContext(accessTokenDto.memberId(), accessTokenDto.memberRole()); + setAuthenticationToContext(PrincipalDetails.from(accessTokenDto)); filterChain.doFilter(request, response); return; } @@ -58,7 +57,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse // AT가 유효하면 통과 if (accessTokenDto != null) { - setAuthenticationToContext(accessTokenDto.memberId(), accessTokenDto.memberRole()); + UserDetails userDetails = PrincipalDetails.from(accessTokenDto); + setAuthenticationToContext(userDetails); filterChain.doFilter(request, response); return; } @@ -72,7 +72,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse AccessTokenDto accessToken = reissueAccessToken.get(); RefreshTokenDto refreshToken = jwtService.createRefreshToken(refreshTokenDto.memberId()); cookieUtil.addTokenCookies(response, accessToken.tokenValue(), refreshToken.tokenValue()); - setAuthenticationToContext(accessToken.memberId(), accessToken.memberRole()); + UserDetails userDetails = PrincipalDetails.from(accessToken); + setAuthenticationToContext(userDetails); } // AT, RT 둘 다 만료되었으면 실패 @@ -92,8 +93,7 @@ private String extractAccessTokenFromHeader(HttpServletRequest request) { .orElse(null); } - private void setAuthenticationToContext(Long memberId, MemberRole memberRole) { - UserDetails userDetails = new PrincipalDetails(memberId, memberRole); + private void setAuthenticationToContext(UserDetails userDetails) { Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); diff --git a/src/main/java/com/gdschongik/gdsc/global/security/MemberAuthInfo.java b/src/main/java/com/gdschongik/gdsc/global/security/MemberAuthInfo.java new file mode 100644 index 000000000..63e18ecfb --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/security/MemberAuthInfo.java @@ -0,0 +1,15 @@ +package com.gdschongik.gdsc.global.security; + +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.member.domain.MemberManageRole; +import com.gdschongik.gdsc.domain.member.domain.MemberRole; +import com.gdschongik.gdsc.domain.member.domain.MemberStudyRole; + +/** + * 엑세스 토큰 및 시큐리티 내부 로직에서 사용되는 회원 정보 DTO입니다. + */ +public record MemberAuthInfo(Long memberId, MemberRole role, MemberManageRole manageRole, MemberStudyRole studyRole) { + public static MemberAuthInfo from(Member member) { + return new MemberAuthInfo(member.getId(), member.getRole(), member.getManageRole(), member.getStudyRole()); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/global/security/PrincipalDetails.java b/src/main/java/com/gdschongik/gdsc/global/security/PrincipalDetails.java index 80427cbb5..473ddc55f 100644 --- a/src/main/java/com/gdschongik/gdsc/global/security/PrincipalDetails.java +++ b/src/main/java/com/gdschongik/gdsc/global/security/PrincipalDetails.java @@ -1,22 +1,38 @@ package com.gdschongik.gdsc.global.security; +import com.gdschongik.gdsc.domain.auth.dto.AccessTokenDto; +import com.gdschongik.gdsc.domain.member.domain.MemberManageRole; import com.gdschongik.gdsc.domain.member.domain.MemberRole; +import com.gdschongik.gdsc.domain.member.domain.MemberStudyRole; +import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; -@AllArgsConstructor +@RequiredArgsConstructor public class PrincipalDetails implements UserDetails { private final Long memberId; - private final MemberRole memberRole; + private final MemberRole role; + private final MemberManageRole manageRole; + private final MemberStudyRole studyRole; + + public static PrincipalDetails from(AccessTokenDto token) { + MemberAuthInfo authInfo = token.authInfo(); + return new PrincipalDetails(authInfo.memberId(), authInfo.role(), authInfo.manageRole(), authInfo.studyRole()); + } @Override public Collection getAuthorities() { - return Collections.singleton(new SimpleGrantedAuthority(memberRole.getValue())); + Collection authorities = new ArrayList<>(); + + authorities.add(new SimpleGrantedAuthority(role.name())); + authorities.add(new SimpleGrantedAuthority(manageRole.name())); + authorities.add(new SimpleGrantedAuthority(studyRole.name())); + + return authorities; } @Override diff --git a/src/main/java/com/gdschongik/gdsc/global/util/JwtUtil.java b/src/main/java/com/gdschongik/gdsc/global/util/JwtUtil.java index 7b1f608f5..a8099f6b4 100644 --- a/src/main/java/com/gdschongik/gdsc/global/util/JwtUtil.java +++ b/src/main/java/com/gdschongik/gdsc/global/util/JwtUtil.java @@ -1,18 +1,20 @@ package com.gdschongik.gdsc.global.util; -import static com.gdschongik.gdsc.global.common.constant.SecurityConstant.TOKEN_ROLE_NAME; +import static com.gdschongik.gdsc.global.common.constant.SecurityConstant.*; import com.gdschongik.gdsc.domain.auth.dto.AccessTokenDto; import com.gdschongik.gdsc.domain.auth.dto.RefreshTokenDto; +import com.gdschongik.gdsc.domain.member.domain.MemberManageRole; import com.gdschongik.gdsc.domain.member.domain.MemberRole; +import com.gdschongik.gdsc.domain.member.domain.MemberStudyRole; import com.gdschongik.gdsc.global.common.constant.JwtConstant; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.exception.ErrorCode; import com.gdschongik.gdsc.global.property.JwtProperty; +import com.gdschongik.gdsc.global.security.MemberAuthInfo; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jws; -import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import java.security.Key; @@ -33,14 +35,27 @@ public class JwtUtil { private final JwtProperty jwtProperty; - public AccessTokenDto generateAccessToken(Long memberId, MemberRole memberRole) { + public AccessTokenDto generateAccessToken(MemberAuthInfo authInfo) { Date issuedAt = new Date(); Date expiredAt = new Date(issuedAt.getTime() + jwtProperty.getToken().get(JwtConstant.ACCESS_TOKEN).expirationMilliTime()); Key key = getKey(JwtConstant.ACCESS_TOKEN); - String tokenValue = buildToken(memberId, memberRole, issuedAt, expiredAt, key); - return new AccessTokenDto(memberId, memberRole, tokenValue); + String tokenValue = buildAccessToken(authInfo, issuedAt, expiredAt, key); + return new AccessTokenDto(authInfo, tokenValue); + } + + private String buildAccessToken(MemberAuthInfo authInfo, Date issuedAt, Date expiredAt, Key key) { + return Jwts.builder() + .setIssuer(jwtProperty.getIssuer()) + .setSubject(authInfo.memberId().toString()) + .setIssuedAt(issuedAt) + .setExpiration(expiredAt) + .claim(TOKEN_ROLE_NAME, authInfo.role().name()) + .claim(TOKEN_MANAGE_ROLE_NAME, authInfo.manageRole().name()) + .claim(TOKEN_STUDY_ROLE_NAME, authInfo.studyRole().name()) + .signWith(key) + .compact(); } public RefreshTokenDto generateRefreshToken(Long memberId) { @@ -49,24 +64,18 @@ public RefreshTokenDto generateRefreshToken(Long memberId) { Date expiredAt = new Date(issuedAt.getTime() + refreshTokenProperty.expirationMilliTime()); Key key = getKey(JwtConstant.REFRESH_TOKEN); - String tokenValue = buildToken(memberId, null, issuedAt, expiredAt, key); + String tokenValue = buildRefreshToken(memberId, issuedAt, expiredAt, key); return new RefreshTokenDto(memberId, tokenValue, refreshTokenProperty.expirationTime()); } - private String buildToken(Long memberId, MemberRole memberRole, Date issuedAt, Date expiredAt, Key key) { - - JwtBuilder jwtBuilder = Jwts.builder() + private String buildRefreshToken(Long memberId, Date issuedAt, Date expiredAt, Key key) { + return Jwts.builder() .setIssuer(jwtProperty.getIssuer()) .setSubject(memberId.toString()) .setIssuedAt(issuedAt) .setExpiration(expiredAt) - .signWith(key); - - if (memberRole != null) { - jwtBuilder.claim(TOKEN_ROLE_NAME, memberRole.name()); - } - - return jwtBuilder.compact(); + .signWith(key) + .compact(); } private Key getKey(JwtConstant jwtConstant) { @@ -78,10 +87,13 @@ public AccessTokenDto parseAccessToken(String accessTokenValue) throws ExpiredJw try { Jws claims = getClaims(JwtConstant.ACCESS_TOKEN, accessTokenValue); - return new AccessTokenDto( + MemberAuthInfo parsedAuthInfo = new MemberAuthInfo( Long.parseLong(claims.getBody().getSubject()), MemberRole.valueOf(claims.getBody().get(TOKEN_ROLE_NAME, String.class)), - accessTokenValue); + MemberManageRole.valueOf(claims.getBody().get(TOKEN_MANAGE_ROLE_NAME, String.class)), + MemberStudyRole.valueOf(claims.getBody().get(TOKEN_STUDY_ROLE_NAME, String.class))); + + return new AccessTokenDto(parsedAuthInfo, accessTokenValue); } catch (ExpiredJwtException e) { throw e; } catch (Exception e) { diff --git a/src/test/java/com/gdschongik/gdsc/config/TestRedisConfig.java b/src/test/java/com/gdschongik/gdsc/config/TestRedisConfig.java index f4e1eb55d..8e827c08c 100644 --- a/src/test/java/com/gdschongik/gdsc/config/TestRedisConfig.java +++ b/src/test/java/com/gdschongik/gdsc/config/TestRedisConfig.java @@ -1,20 +1,12 @@ package com.gdschongik.gdsc.config; -import org.junit.jupiter.api.extension.BeforeAllCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.utility.DockerImageName; +import com.gdschongik.gdsc.global.config.RedisConfig; +import com.gdschongik.gdsc.global.property.RedisProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Import; -public class TestRedisConfig implements BeforeAllCallback { - private static final String REDIS_IMAGE = "redis:alpine"; - private static final int REDIS_PORT = 6379; - private GenericContainer redis; - - @Override - public void beforeAll(ExtensionContext context) { - redis = new GenericContainer(DockerImageName.parse(REDIS_IMAGE)).withExposedPorts(REDIS_PORT); - redis.start(); - System.setProperty("spring.data.redis.host", redis.getHost()); - System.setProperty("spring.data.redis.port", String.valueOf(redis.getMappedPort(REDIS_PORT))); - } -} +@TestConfiguration +@EnableConfigurationProperties({RedisProperty.class}) +@Import({RedisConfig.class}) +public class TestRedisConfig {} diff --git a/src/test/java/com/gdschongik/gdsc/domain/email/application/UnivEmailVerificationServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/email/application/UnivEmailVerificationServiceTest.java index 03b506776..f08f177ce 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/email/application/UnivEmailVerificationServiceTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/email/application/UnivEmailVerificationServiceTest.java @@ -4,7 +4,6 @@ import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.gdschongik.gdsc.config.TestRedisConfig; import com.gdschongik.gdsc.domain.email.dto.request.UnivEmailVerificationRequest; import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.global.exception.CustomException; @@ -13,11 +12,9 @@ import com.gdschongik.gdsc.helper.IntegrationTest; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; -@ExtendWith(TestRedisConfig.class) public class UnivEmailVerificationServiceTest extends IntegrationTest { @Autowired diff --git a/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java b/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java index ee4635223..236887590 100644 --- a/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java +++ b/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java @@ -16,7 +16,9 @@ import com.gdschongik.gdsc.domain.discord.application.handler.DelegateMemberDiscordEventHandler; import com.gdschongik.gdsc.domain.member.dao.MemberRepository; import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.member.domain.MemberManageRole; import com.gdschongik.gdsc.domain.member.domain.MemberRole; +import com.gdschongik.gdsc.domain.member.domain.MemberStudyRole; import com.gdschongik.gdsc.domain.membership.dao.MembershipRepository; import com.gdschongik.gdsc.domain.membership.domain.Membership; import com.gdschongik.gdsc.domain.recruitment.application.OnboardingRecruitmentService; @@ -49,6 +51,9 @@ public abstract class IntegrationTest { @Autowired protected DatabaseCleaner databaseCleaner; + @Autowired + protected RedisCleaner redisCleaner; + @Autowired protected MemberRepository memberRepository; @@ -85,11 +90,14 @@ public abstract class IntegrationTest { @BeforeEach void setUp() { databaseCleaner.execute(); + redisCleaner.execute(); doNothing().when(delegateMemberDiscordEventHandler).delegate(any()); } protected void logoutAndReloginAs(Long memberId, MemberRole memberRole) { - PrincipalDetails principalDetails = new PrincipalDetails(memberId, memberRole); + // TODO: MemberManageRole, MemberStudyRole 추가 + PrincipalDetails principalDetails = + new PrincipalDetails(memberId, memberRole, MemberManageRole.NONE, MemberStudyRole.STUDENT); Authentication authentication = new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); diff --git a/src/test/java/com/gdschongik/gdsc/helper/RedisCleaner.java b/src/test/java/com/gdschongik/gdsc/helper/RedisCleaner.java new file mode 100644 index 000000000..813d1c7d7 --- /dev/null +++ b/src/test/java/com/gdschongik/gdsc/helper/RedisCleaner.java @@ -0,0 +1,27 @@ +package com.gdschongik.gdsc.helper; + +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; + +@Component +public class RedisCleaner implements InitializingBean { + private RedisClient redisClient; + private StatefulRedisConnection connection; + private RedisCommands commands; + + @Override + public void afterPropertiesSet() { + RedisURI redisUri = RedisURI.Builder.redis("localhost", 6379).build(); + redisClient = RedisClient.create(redisUri); + connection = redisClient.connect(); + commands = connection.sync(); + } + + public void execute() { + commands.flushdb(); + } +} diff --git a/src/test/java/com/gdschongik/gdsc/helper/RepositoryTest.java b/src/test/java/com/gdschongik/gdsc/helper/RepositoryTest.java index 3b00b1e35..978874ab2 100644 --- a/src/test/java/com/gdschongik/gdsc/helper/RepositoryTest.java +++ b/src/test/java/com/gdschongik/gdsc/helper/RepositoryTest.java @@ -1,7 +1,6 @@ package com.gdschongik.gdsc.helper; import com.gdschongik.gdsc.config.TestQuerydslConfig; -import com.gdschongik.gdsc.config.TestRedisConfig; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; @@ -11,7 +10,7 @@ import org.springframework.test.context.ActiveProfiles; @DataJpaTest -@Import({TestQuerydslConfig.class, TestRedisConfig.class, DatabaseCleaner.class}) +@Import({TestQuerydslConfig.class, DatabaseCleaner.class, RedisCleaner.class}) @ActiveProfiles("test") @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public abstract class RepositoryTest { @@ -22,8 +21,12 @@ public abstract class RepositoryTest { @Autowired protected TestEntityManager testEntityManager; + @Autowired + protected RedisCleaner redisCleaner; + @BeforeEach void setUp() { databaseCleaner.execute(); + redisCleaner.execute(); } } From 9db11c120c4d21fdd7c8d923dd9e0cfb3d094c37 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Sun, 4 Aug 2024 16:36:30 +0900 Subject: [PATCH 06/22] =?UTF-8?q?feat:=20=EA=B2=B0=EC=A0=9C=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=20=ED=9B=84=20=EC=A0=95=ED=9A=8C=EC=9B=90=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=EC=9D=84=20=ED=9A=8C=EC=88=98=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=A0=95=EC=B1=85=20=EA=B5=AC=ED=98=84=20(#565)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 주문 취소 이벤트 및 핸들러 로직 구현 * feat: 멤버십 회비납입상태 취소 로직 및 이벤트 발행 * refactor: MembershipEventHandler로 이름 변경 * feat: 멤버십 회비납입상태 취소 이벤트 핸들러 로직 구현 * feat: 멤버 준회원 강등 로직 구현 * docs: 예외 명시적으로 던져 발행자로 전파되도록 투두 주석 추가 * test: 주문 취소 후속 로직에 대한 테스트 추가 * docs: 로그 수정 * docs: 멤버십 회비납입 취소 로그 추가 * refactor: 주문 nano id, 결제키 상수로 추출 * test: stubbing 하는 공통 로직 추출 * test: 템플릿 메서드 패턴 활용하여 `@BeforeEach` 상속 적용 * test: setUp이 상속되지 않음을 명시하기 위해 protected 제거 * fix: 오타 수정 * feat: 준회원 강등 시 디스코드 서버 정회원 역할 회수 로직 구현 * refactor: 디스코드 이벤트 핸들러 stubbing 로직 개선 * test: 디스코드 서버 정회원 역할 회수 테스트 추가 --- .../MemberDiscordRoleRevokeHandler.java | 35 +++ ...MemberDemotedToAssociateEventListener.java | 26 ++ .../application/CommonMemberService.java | 19 ++ .../gdsc/domain/member/domain/Member.java | 2 + .../domain/MemberDemotedToAssociateEvent.java | 3 + ...ndler.java => MembershipEventHandler.java} | 11 +- .../application/MembershipService.java | 17 ++ .../domain/membership/domain/Membership.java | 15 + .../domain/MembershipPaymentRevokedEvent.java | 3 + .../order/application/OrderEventHandler.java | 7 + .../order/application/OrderService.java | 2 + .../gdsc/domain/order/domain/Order.java | 3 +- .../order/domain/OrderCanceledEvent.java | 3 + .../gdsc/global/exception/ErrorCode.java | 1 + .../order/application/OrderServiceTest.java | 257 +++++++++++++----- .../global/common/constant/OrderConstant.java | 10 + .../gdsc/helper/IntegrationTest.java | 26 ++ 17 files changed, 362 insertions(+), 78 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/discord/application/handler/MemberDiscordRoleRevokeHandler.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/discord/application/listener/MemberDemotedToAssociateEventListener.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberDemotedToAssociateEvent.java rename src/main/java/com/gdschongik/gdsc/domain/membership/application/{MembershipVerifiedEventHandler.java => MembershipEventHandler.java} (54%) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/membership/domain/MembershipPaymentRevokedEvent.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/order/domain/OrderCanceledEvent.java create mode 100644 src/test/java/com/gdschongik/gdsc/global/common/constant/OrderConstant.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/discord/application/handler/MemberDiscordRoleRevokeHandler.java b/src/main/java/com/gdschongik/gdsc/domain/discord/application/handler/MemberDiscordRoleRevokeHandler.java new file mode 100644 index 000000000..af3ec1ad2 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/discord/application/handler/MemberDiscordRoleRevokeHandler.java @@ -0,0 +1,35 @@ +package com.gdschongik.gdsc.domain.discord.application.handler; + +import static com.gdschongik.gdsc.global.common.constant.DiscordConstant.*; + +import com.gdschongik.gdsc.domain.member.domain.MemberDemotedToAssociateEvent; +import com.gdschongik.gdsc.global.util.DiscordUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Role; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MemberDiscordRoleRevokeHandler implements SpringEventHandler { + + private final DiscordUtil discordUtil; + + @Override + public void delegate(Object context) { + MemberDemotedToAssociateEvent event = (MemberDemotedToAssociateEvent) context; + Guild guild = discordUtil.getCurrentGuild(); + Member member = discordUtil.getMemberById(event.discordId()); + Role role = discordUtil.findRoleByName(MEMBER_ROLE_NAME); + + guild.removeRoleFromMember(member, role).queue(); + + log.info( + "[MemberDiscordRoleRevokeHandler] 디스코드 서버 정회원 역할 제거 완료: memberId={}, discordId={}", + event.memberId(), + event.discordId()); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/discord/application/listener/MemberDemotedToAssociateEventListener.java b/src/main/java/com/gdschongik/gdsc/domain/discord/application/listener/MemberDemotedToAssociateEventListener.java new file mode 100644 index 000000000..8482e9b7f --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/discord/application/listener/MemberDemotedToAssociateEventListener.java @@ -0,0 +1,26 @@ +package com.gdschongik.gdsc.domain.discord.application.listener; + +import com.gdschongik.gdsc.domain.discord.application.handler.MemberDiscordRoleRevokeHandler; +import com.gdschongik.gdsc.domain.member.domain.MemberDemotedToAssociateEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MemberDemotedToAssociateEventListener { + + private final MemberDiscordRoleRevokeHandler memberDiscordRoleRevokeHandler; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void demoteMemberToAssociate(MemberDemotedToAssociateEvent event) { + log.info( + "[MemberDemotedToAssociateEventListener] 회원 준회원 강등 이벤트 수신: memberId={}, discordId={}", + event.memberId(), + event.discordId()); + memberDiscordRoleRevokeHandler.delegate(event); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/CommonMemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/CommonMemberService.java index 61d8b7775..3ae725a12 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/application/CommonMemberService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/CommonMemberService.java @@ -62,4 +62,23 @@ public void advanceMemberToRegularByMembership(Long membershipId) { log.info("[CommonMemberService] 정회원 승급 완료: memberId={}", member.getId()); } } + + /** + * 이벤트 핸들러에서 사용되므로, `@Transactional` 을 사용하지 않습니다. + */ + public void demoteMemberToAssociateByMembership(Long membershipId) { + Membership membership = membershipRepository + .findById(membershipId) + .orElseThrow(() -> new CustomException(MEMBERSHIP_NOT_FOUND)); + + Member member = memberRepository + .findById(membership.getMember().getId()) + .orElseThrow(() -> new CustomException(MEMBER_NOT_FOUND)); + + member.demoteToAssociate(); + + memberRepository.save(member); + + log.info("[CommonMemberService] 준회원 강등 완료: memberId={}", member.getId()); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java index 3041479ca..2270544d3 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Member.java @@ -259,6 +259,8 @@ public void demoteToAssociate() { validateStatusUpdatable(); role = ASSOCIATE; + + registerEvent(new MemberDemotedToAssociateEvent(id, discordId)); } /** diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberDemotedToAssociateEvent.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberDemotedToAssociateEvent.java new file mode 100644 index 000000000..9dcbacb57 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/MemberDemotedToAssociateEvent.java @@ -0,0 +1,3 @@ +package com.gdschongik.gdsc.domain.member.domain; + +public record MemberDemotedToAssociateEvent(Long memberId, String discordId) {} diff --git a/src/main/java/com/gdschongik/gdsc/domain/membership/application/MembershipVerifiedEventHandler.java b/src/main/java/com/gdschongik/gdsc/domain/membership/application/MembershipEventHandler.java similarity index 54% rename from src/main/java/com/gdschongik/gdsc/domain/membership/application/MembershipVerifiedEventHandler.java rename to src/main/java/com/gdschongik/gdsc/domain/membership/application/MembershipEventHandler.java index 4580949d3..6b252fdbd 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/membership/application/MembershipVerifiedEventHandler.java +++ b/src/main/java/com/gdschongik/gdsc/domain/membership/application/MembershipEventHandler.java @@ -1,6 +1,7 @@ package com.gdschongik.gdsc.domain.membership.application; import com.gdschongik.gdsc.domain.member.application.CommonMemberService; +import com.gdschongik.gdsc.domain.membership.domain.MembershipPaymentRevokedEvent; import com.gdschongik.gdsc.domain.membership.domain.MembershipVerifiedEvent; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -10,13 +11,19 @@ @Slf4j @Component @RequiredArgsConstructor -public class MembershipVerifiedEventHandler { +public class MembershipEventHandler { private final CommonMemberService commonMemberService; @EventListener public void handleMembershipVerifiedEvent(MembershipVerifiedEvent event) { - log.info("[MembershipVerifiedEventHandler] 멤버십 인증 이벤트 수신: membershipId={}", event.membershipId()); + log.info("[MembershipEventHandler] 멤버십 인증 이벤트 수신: membershipId={}", event.membershipId()); commonMemberService.advanceMemberToRegularByMembership(event.membershipId()); } + + @EventListener + public void handleMembershipPaymentRevokedEvent(MembershipPaymentRevokedEvent event) { + log.info("[MembershipEventHandler] 멤버십 회비납입 취소 이벤트 수신: membershipId={}", event.membershipId()); + commonMemberService.demoteMemberToAssociateByMembership(event.membershipId()); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/membership/application/MembershipService.java b/src/main/java/com/gdschongik/gdsc/domain/membership/application/MembershipService.java index bc24e7f0c..717fef1bf 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/membership/application/MembershipService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/membership/application/MembershipService.java @@ -97,4 +97,21 @@ public void deleteMembership(Member member) { myMembershipOpt.ifPresent(membershipRepository::delete); } + + /** + * 이벤트 핸들러에서 사용되므로, `@Transactional` 을 사용하지 않습니다. + */ + public void revokePaymentStatus(Long orderId) { + Order order = orderRepository.findById(orderId).orElseThrow(() -> new CustomException(ORDER_NOT_FOUND)); + + Membership membership = membershipRepository + .findById(order.getMembershipId()) + .orElseThrow(() -> new CustomException(MEMBERSHIP_NOT_FOUND)); + + membership.revokePaymentStatus(); + + membershipRepository.save(membership); + + log.info("[MembershipService] 멤버십 회비납입 취소 완료: membershipId={}", membership.getId()); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/membership/domain/Membership.java b/src/main/java/com/gdschongik/gdsc/domain/membership/domain/Membership.java index fdad73bb7..7b6836173 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/membership/domain/Membership.java +++ b/src/main/java/com/gdschongik/gdsc/domain/membership/domain/Membership.java @@ -75,6 +75,21 @@ public void verifyPaymentStatus() { registerEvent(new MembershipVerifiedEvent(id)); } + public void revokePaymentStatus() { + validatePaymentStatusRevocable(); + + regularRequirement.updatePaymentStatus(PENDING); + + registerEvent(new MembershipPaymentRevokedEvent(id)); + } + + private void validatePaymentStatusRevocable() { + // TODO: 이벤트로 트리거되는 로직이더라도 예외 던지도록 수정 + if (!regularRequirement.isPaymentSatisfied()) { + throw new CustomException(MEMBERSHIP_PAYMENT_NOT_REVOCABLE_NOT_SATISFIED); + } + } + // 데이터 전달 로직 public boolean isRegularRequirementAllSatisfied() { diff --git a/src/main/java/com/gdschongik/gdsc/domain/membership/domain/MembershipPaymentRevokedEvent.java b/src/main/java/com/gdschongik/gdsc/domain/membership/domain/MembershipPaymentRevokedEvent.java new file mode 100644 index 000000000..128b9734a --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/membership/domain/MembershipPaymentRevokedEvent.java @@ -0,0 +1,3 @@ +package com.gdschongik.gdsc.domain.membership.domain; + +public record MembershipPaymentRevokedEvent(Long membershipId) {} diff --git a/src/main/java/com/gdschongik/gdsc/domain/order/application/OrderEventHandler.java b/src/main/java/com/gdschongik/gdsc/domain/order/application/OrderEventHandler.java index 3af0f182f..8065544bc 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/order/application/OrderEventHandler.java +++ b/src/main/java/com/gdschongik/gdsc/domain/order/application/OrderEventHandler.java @@ -1,6 +1,7 @@ package com.gdschongik.gdsc.domain.order.application; import com.gdschongik.gdsc.domain.membership.application.MembershipService; +import com.gdschongik.gdsc.domain.order.domain.OrderCanceledEvent; import com.gdschongik.gdsc.domain.order.domain.OrderCompletedEvent; import com.gdschongik.gdsc.domain.order.domain.OrderCreatedEvent; import lombok.RequiredArgsConstructor; @@ -32,4 +33,10 @@ public void handleOrderCompletedEvent(OrderCompletedEvent orderCompletedEvent) { log.info("[OrderEventHandler] 주문 완료 이벤트 수신: nanoId={}", orderCompletedEvent.nanoId()); membershipService.verifyPaymentStatus(orderCompletedEvent.nanoId()); } + + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) + public void handleOrderCanceledEvent(OrderCanceledEvent orderCanceledEvent) { + log.info("[OrderEventHandler] 주문 취소 이벤트 수신: orderId={}", orderCanceledEvent.orderId()); + membershipService.revokePaymentStatus(orderCanceledEvent.orderId()); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/order/application/OrderService.java b/src/main/java/com/gdschongik/gdsc/domain/order/application/OrderService.java index e3f61802e..cfbf6dd72 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/order/application/OrderService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/order/application/OrderService.java @@ -129,6 +129,8 @@ public void cancelOrder(Long orderId, OrderCancelRequest request) { order.cancel(canceledAt); + orderRepository.save(order); + log.info("[OrderService] 주문 취소: orderId={}", order.getId()); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/order/domain/Order.java b/src/main/java/com/gdschongik/gdsc/domain/order/domain/Order.java index 37d82f9fa..08dc19ba1 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/order/domain/Order.java +++ b/src/main/java/com/gdschongik/gdsc/domain/order/domain/Order.java @@ -141,10 +141,11 @@ public void complete(String paymentKey, ZonedDateTime approvedAt) { * 상태 변경 및 취소 시각을 저장하며, 예외를 발생시키지 않도록 외부 취소 요청 전에 validateCancelable을 호출합니다. */ public void cancel(ZonedDateTime canceledAt) { - // TODO: 취소 이벤트 발행을 통해 멤버십 및 멤버 상태에 대한 변경 로직 추가 validateCancelable(); this.status = OrderStatus.CANCELED; this.canceledAt = canceledAt; + + registerEvent(new OrderCanceledEvent(id)); } public void validateCancelable() { diff --git a/src/main/java/com/gdschongik/gdsc/domain/order/domain/OrderCanceledEvent.java b/src/main/java/com/gdschongik/gdsc/domain/order/domain/OrderCanceledEvent.java new file mode 100644 index 000000000..6a0d1a437 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/order/domain/OrderCanceledEvent.java @@ -0,0 +1,3 @@ +package com.gdschongik.gdsc.domain.order.domain; + +public record OrderCanceledEvent(Long orderId) {} 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 5a5795cca..ba35fdace 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -76,6 +76,7 @@ public enum ErrorCode { MEMBERSHIP_ALREADY_SATISFIED(HttpStatus.CONFLICT, "이미 이번 학기에 정회원 승급을 완료한 회원입니다."), MEMBERSHIP_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 멤버십이 존재하지 않습니다."), MEMBERSHIP_RECRUITMENT_ROUND_NOT_OPEN(HttpStatus.CONFLICT, "리크루팅 회차 모집기간이 아닙니다."), + MEMBERSHIP_PAYMENT_NOT_REVOCABLE_NOT_SATISFIED(HttpStatus.CONFLICT, "회비납부를 완료한 경우에만 멤버십 회비납부상태를 취소할 수 있습니다."), // Recruitment DATE_PRECEDENCE_INVALID(HttpStatus.BAD_REQUEST, "종료일이 시작일과 같거나 앞설 수 없습니다."), diff --git a/src/test/java/com/gdschongik/gdsc/domain/order/application/OrderServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/order/application/OrderServiceTest.java index a3176a564..5fc713ee8 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/order/application/OrderServiceTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/order/application/OrderServiceTest.java @@ -1,5 +1,6 @@ package com.gdschongik.gdsc.domain.order.application; +import static com.gdschongik.gdsc.global.common.constant.OrderConstant.*; import static com.gdschongik.gdsc.global.common.constant.RecruitmentConstant.*; import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import static org.assertj.core.api.Assertions.*; @@ -29,6 +30,7 @@ import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -49,6 +51,18 @@ class OrderServiceTest extends IntegrationTest { @Autowired private OrderRepository orderRepository; + @Override + protected void doStubTemplate() { + stubPaymentConfirm(); + } + + private void stubPaymentConfirm() { + ZonedDateTime approvedAt = ZonedDateTime.now(); + PaymentResponse mockPaymentResponse = mock(PaymentResponse.class); + when(mockPaymentResponse.approvedAt()).thenReturn(approvedAt); + when(paymentClient.confirm(any(PaymentConfirmRequest.class))).thenReturn(mockPaymentResponse); + } + @Nested class 임시주문_생성할때 { @@ -72,7 +86,7 @@ class 임시주문_생성할때 { // when var request = new OrderCreateRequest( - "HnbMWoSZRq3qK1W3tPXCW", + ORDER_NANO_ID, membership.getId(), issuedCoupon.getId(), BigDecimal.valueOf(20000), @@ -105,30 +119,22 @@ class 주문_완료할때 { Membership membership = createMembership(member, recruitmentRound); IssuedCoupon issuedCoupon = createAndIssue(MONEY_5000_WON, member); - String orderNanoId = "HnbMWoSZRq3qK1W3tPXCW"; orderService.createPendingOrder(new OrderCreateRequest( - orderNanoId, + ORDER_NANO_ID, membership.getId(), issuedCoupon.getId(), BigDecimal.valueOf(20000), BigDecimal.valueOf(5000), BigDecimal.valueOf(15000))); - String paymentKey = "testPaymentKey"; - - ZonedDateTime approvedAt = ZonedDateTime.now(); - PaymentResponse mockPaymentResponse = mock(PaymentResponse.class); - when(mockPaymentResponse.approvedAt()).thenReturn(approvedAt); - when(paymentClient.confirm(any(PaymentConfirmRequest.class))).thenReturn(mockPaymentResponse); - // when - var request = new OrderCompleteRequest(paymentKey, orderNanoId, 15000L); + var request = new OrderCompleteRequest(ORDER_PAYMENT_KEY, ORDER_NANO_ID, 15000L); orderService.completeOrder(request); // then - Order completedOrder = orderRepository.findByNanoId(orderNanoId).orElseThrow(); + Order completedOrder = orderRepository.findByNanoId(ORDER_NANO_ID).orElseThrow(); assertThat(completedOrder.getStatus()).isEqualTo(OrderStatus.COMPLETED); - assertThat(completedOrder.getPaymentKey()).isEqualTo(paymentKey); + assertThat(completedOrder.getPaymentKey()).isEqualTo(ORDER_PAYMENT_KEY); IssuedCoupon usedCoupon = issuedCouponRepository.findById(issuedCoupon.getId()).orElseThrow(); @@ -154,24 +160,16 @@ class 주문_완료할때 { Membership membership = createMembership(member, recruitmentRound); IssuedCoupon issuedCoupon = createAndIssue(MONEY_5000_WON, member); - String orderNanoId = "HnbMWoSZRq3qK1W3tPXCW"; orderService.createPendingOrder(new OrderCreateRequest( - orderNanoId, + ORDER_NANO_ID, membership.getId(), issuedCoupon.getId(), BigDecimal.valueOf(20000), BigDecimal.valueOf(5000), BigDecimal.valueOf(15000))); - String paymentKey = "testPaymentKey"; - - ZonedDateTime approvedAt = ZonedDateTime.now(); - PaymentResponse mockPaymentResponse = mock(PaymentResponse.class); - when(mockPaymentResponse.approvedAt()).thenReturn(approvedAt); - when(paymentClient.confirm(any(PaymentConfirmRequest.class))).thenReturn(mockPaymentResponse); - // when - var request = new OrderCompleteRequest(paymentKey, orderNanoId, 15000L); + var request = new OrderCompleteRequest(ORDER_PAYMENT_KEY, ORDER_NANO_ID, 15000L); orderService.completeOrder(request); // then @@ -198,24 +196,16 @@ class 주문_완료할때 { Membership membership = createMembership(member, recruitmentRound); IssuedCoupon issuedCoupon = createAndIssue(MONEY_5000_WON, member); - String orderNanoId = "HnbMWoSZRq3qK1W3tPXCW"; orderService.createPendingOrder(new OrderCreateRequest( - orderNanoId, + ORDER_NANO_ID, membership.getId(), issuedCoupon.getId(), BigDecimal.valueOf(20000), BigDecimal.valueOf(5000), BigDecimal.valueOf(15000))); - String paymentKey = "testPaymentKey"; - - ZonedDateTime approvedAt = ZonedDateTime.now(); - PaymentResponse mockPaymentResponse = mock(PaymentResponse.class); - when(mockPaymentResponse.approvedAt()).thenReturn(approvedAt); - when(paymentClient.confirm(any(PaymentConfirmRequest.class))).thenReturn(mockPaymentResponse); - // when - var request = new OrderCompleteRequest(paymentKey, orderNanoId, 15000L); + var request = new OrderCompleteRequest(ORDER_PAYMENT_KEY, ORDER_NANO_ID, 15000L); orderService.completeOrder(request); // then @@ -227,6 +217,22 @@ class 주문_완료할때 { @Nested class 주문_취소할때 { + @BeforeEach + void setUp() { + stubPaymentCancel(); + } + + private void stubPaymentCancel() { + ZonedDateTime canceledAt = ZonedDateTime.now(); + PaymentResponse mockCancelResponse = mock(PaymentResponse.class); + PaymentResponse.CancelDto mockCancelDto = mock(PaymentResponse.CancelDto.class); + + when(mockCancelResponse.cancels()).thenReturn(List.of(mockCancelDto)); + when(mockCancelDto.canceledAt()).thenReturn(canceledAt); + when(paymentClient.cancelPayment(eq(ORDER_PAYMENT_KEY), any(PaymentCancelRequest.class))) + .thenReturn(mockCancelResponse); + } + @Test void 성공한다() { // given @@ -244,38 +250,21 @@ class 주문_취소할때 { Membership membership = createMembership(member, recruitmentRound); IssuedCoupon issuedCoupon = createAndIssue(MONEY_5000_WON, member); - String orderNanoId = "HnbMWoSZRq3qK1W3tPXCW"; orderService.createPendingOrder(new OrderCreateRequest( - orderNanoId, + ORDER_NANO_ID, membership.getId(), issuedCoupon.getId(), BigDecimal.valueOf(20000), BigDecimal.valueOf(5000), BigDecimal.valueOf(15000))); - String paymentKey = "testPaymentKey"; - - ZonedDateTime approvedAt = ZonedDateTime.now(); - PaymentResponse mockPaymentResponse = mock(PaymentResponse.class); - when(mockPaymentResponse.approvedAt()).thenReturn(approvedAt); - when(paymentClient.confirm(any(PaymentConfirmRequest.class))).thenReturn(mockPaymentResponse); - - var completeRequest = new OrderCompleteRequest(paymentKey, orderNanoId, 15000L); + var completeRequest = new OrderCompleteRequest(ORDER_PAYMENT_KEY, ORDER_NANO_ID, 15000L); orderService.completeOrder(completeRequest); - Order completedOrder = orderRepository.findByNanoId(orderNanoId).orElseThrow(); - - ZonedDateTime canceledAt = ZonedDateTime.now(); - PaymentResponse mockCancelResponse = mock(PaymentResponse.class); - PaymentResponse.CancelDto mockCancelDto = mock(PaymentResponse.CancelDto.class); - - when(mockCancelResponse.cancels()).thenReturn(List.of(mockCancelDto)); - when(mockCancelDto.canceledAt()).thenReturn(canceledAt); - when(paymentClient.cancelPayment(eq(paymentKey), any(PaymentCancelRequest.class))) - .thenReturn(mockCancelResponse); + Order completedOrder = orderRepository.findByNanoId(ORDER_NANO_ID).orElseThrow(); // when - var cancelRequest = new OrderCancelRequest("테스트 취소 사유"); + var cancelRequest = new OrderCancelRequest(ORDER_CANCEL_REASON); orderService.cancelOrder(completedOrder.getId(), cancelRequest); // then @@ -284,7 +273,7 @@ class 주문_취소할때 { assertThat(canceledOrder.getStatus()).isEqualTo(OrderStatus.CANCELED); assertThat(canceledOrder.getCanceledAt()).isNotNull(); - verify(paymentClient).cancelPayment(eq(paymentKey), any(PaymentCancelRequest.class)); + verify(paymentClient).cancelPayment(eq(ORDER_PAYMENT_KEY), any(PaymentCancelRequest.class)); } @Test @@ -303,19 +292,18 @@ class 주문_취소할때 { Membership membership = createMembership(member, recruitmentRound); - String orderNanoId = "HnbMWoSZRq3qK1W3tPXCW"; orderService.createPendingOrder(new OrderCreateRequest( - orderNanoId, + ORDER_NANO_ID, membership.getId(), null, BigDecimal.valueOf(20000), BigDecimal.valueOf(0), BigDecimal.valueOf(20000))); - Order pendingOrder = orderRepository.findByNanoId(orderNanoId).orElseThrow(); + Order pendingOrder = orderRepository.findByNanoId(ORDER_NANO_ID).orElseThrow(); Long id = pendingOrder.getId(); - OrderCancelRequest request = new OrderCancelRequest("테스트 취소 사유"); + OrderCancelRequest request = new OrderCancelRequest(ORDER_CANCEL_REASON); // when & then assertThatThrownBy(() -> orderService.cancelOrder(id, request)) @@ -324,6 +312,136 @@ class 주문_취소할때 { verify(paymentClient, never()).cancelPayment(any(), any()); } + + @Test + void 멤버십의_회비납부상태가_취소된다() { + // given + Member member = createMember(); + logoutAndReloginAs(1L, MemberRole.ASSOCIATE); + RecruitmentRound recruitmentRound = createRecruitmentRound( + RECRUITMENT_ROUND_NAME, + LocalDateTime.now().minusDays(1), + LocalDateTime.now().plusDays(1), + ACADEMIC_YEAR, + SEMESTER_TYPE, + ROUND_TYPE, + MONEY_20000_WON); + + Membership membership = createMembership(member, recruitmentRound); + IssuedCoupon issuedCoupon = createAndIssue(MONEY_5000_WON, member); + + orderService.createPendingOrder(new OrderCreateRequest( + ORDER_NANO_ID, + membership.getId(), + issuedCoupon.getId(), + BigDecimal.valueOf(20000), + BigDecimal.valueOf(5000), + BigDecimal.valueOf(15000))); + + var completeRequest = new OrderCompleteRequest(ORDER_PAYMENT_KEY, ORDER_NANO_ID, 15000L); + orderService.completeOrder(completeRequest); + + Order completedOrder = orderRepository.findByNanoId(ORDER_NANO_ID).orElseThrow(); + + var cancelRequest = new OrderCancelRequest(ORDER_CANCEL_REASON); + + // when + orderService.cancelOrder(completedOrder.getId(), cancelRequest); + + // then + Membership verifiedMembership = + membershipRepository.findById(membership.getId()).orElseThrow(); + assertThat(verifiedMembership.getRegularRequirement().isPaymentSatisfied()) + .isFalse(); + } + + @Test + void 준회원으로_강등된다() { + // given + Member member = createMember(); + logoutAndReloginAs(1L, MemberRole.ASSOCIATE); + RecruitmentRound recruitmentRound = createRecruitmentRound( + RECRUITMENT_ROUND_NAME, + LocalDateTime.now().minusDays(1), + LocalDateTime.now().plusDays(1), + ACADEMIC_YEAR, + SEMESTER_TYPE, + ROUND_TYPE, + MONEY_20000_WON); + + Membership membership = createMembership(member, recruitmentRound); + IssuedCoupon issuedCoupon = createAndIssue(MONEY_5000_WON, member); + + orderService.createPendingOrder(new OrderCreateRequest( + ORDER_NANO_ID, + membership.getId(), + issuedCoupon.getId(), + BigDecimal.valueOf(20000), + BigDecimal.valueOf(5000), + BigDecimal.valueOf(15000))); + + var completeRequest = new OrderCompleteRequest(ORDER_PAYMENT_KEY, ORDER_NANO_ID, 15000L); + orderService.completeOrder(completeRequest); + + Order completedOrder = orderRepository.findByNanoId(ORDER_NANO_ID).orElseThrow(); + Member orderCompletedMember = + memberRepository.findById(member.getId()).orElseThrow(); + + var cancelRequest = new OrderCancelRequest(ORDER_CANCEL_REASON); + + // when + orderService.cancelOrder(completedOrder.getId(), cancelRequest); + + // then + Member orderCanceledMember = + memberRepository.findById(member.getId()).orElseThrow(); + + assertThat(orderCompletedMember.isRegular()).isTrue(); + assertThat(orderCanceledMember.isAssociate()).isTrue(); + } + + @Test + void 디스코드_서버_정회원_역할을_회수한다() { + // given + Member member = createMember(); + logoutAndReloginAs(1L, MemberRole.ASSOCIATE); + RecruitmentRound recruitmentRound = createRecruitmentRound( + RECRUITMENT_ROUND_NAME, + LocalDateTime.now().minusDays(1), + LocalDateTime.now().plusDays(1), + ACADEMIC_YEAR, + SEMESTER_TYPE, + ROUND_TYPE, + MONEY_20000_WON); + + Membership membership = createMembership(member, recruitmentRound); + IssuedCoupon issuedCoupon = createAndIssue(MONEY_5000_WON, member); + + orderService.createPendingOrder(new OrderCreateRequest( + ORDER_NANO_ID, + membership.getId(), + issuedCoupon.getId(), + BigDecimal.valueOf(20000), + BigDecimal.valueOf(5000), + BigDecimal.valueOf(15000))); + + var completeRequest = new OrderCompleteRequest(ORDER_PAYMENT_KEY, ORDER_NANO_ID, 15000L); + orderService.completeOrder(completeRequest); + + Order completedOrder = orderRepository.findByNanoId(ORDER_NANO_ID).orElseThrow(); + + // when + var cancelRequest = new OrderCancelRequest(ORDER_CANCEL_REASON); + orderService.cancelOrder(completedOrder.getId(), cancelRequest); + + // then + Order canceledOrder = + orderRepository.findById(completedOrder.getId()).orElseThrow(); + assertThat(canceledOrder.getStatus()).isEqualTo(OrderStatus.CANCELED); + assertThat(canceledOrder.getCanceledAt()).isNotNull(); + + verify(memberDiscordRoleRevokeHandler).delegate(any()); + } } @Disabled // TODO: CI 환경에서만 실패하는 테스트, TZ 관련 설정 확인 필요 @@ -347,22 +465,15 @@ class 일자기준으로_주문목록_조회시 { Membership membership = createMembership(member, recruitmentRound); IssuedCoupon issuedCoupon = createAndIssue(MONEY_5000_WON, member); - String orderNanoId = "HnbMWoSZRq3qK1W3tPXCW"; orderService.createPendingOrder(new OrderCreateRequest( - orderNanoId, + ORDER_NANO_ID, membership.getId(), issuedCoupon.getId(), BigDecimal.valueOf(20000), BigDecimal.valueOf(5000), BigDecimal.valueOf(15000))); - String paymentKey = "testPaymentKey"; - - ZonedDateTime approvedAt = ZonedDateTime.now(); - PaymentResponse mockPaymentResponse = mock(PaymentResponse.class); - when(mockPaymentResponse.approvedAt()).thenReturn(approvedAt); - when(paymentClient.confirm(any(PaymentConfirmRequest.class))).thenReturn(mockPaymentResponse); - var request = new OrderCompleteRequest(paymentKey, orderNanoId, 15000L); + var request = new OrderCompleteRequest(ORDER_PAYMENT_KEY, ORDER_NANO_ID, 15000L); orderService.completeOrder(request); LocalDate date = LocalDate.now(); @@ -373,7 +484,7 @@ class 일자기준으로_주문목록_조회시 { // then boolean orderExists = orderResponse.getContent().stream() - .anyMatch(order -> order.nanoId().equals(orderNanoId)); + .anyMatch(order -> order.nanoId().equals(ORDER_NANO_ID)); assertThat(orderExists).isTrue(); } @@ -399,10 +510,8 @@ class 무료주문_생성할때 { Membership membership = createMembership(member, recruitmentRound); IssuedCoupon issuedCoupon = createAndIssue(MONEY_20000_WON, member); - String orderNanoId = "HnbMWoSZRq3qK1W3tPXCW"; - var request = new OrderCreateRequest( - orderNanoId, + ORDER_NANO_ID, membership.getId(), issuedCoupon.getId(), BigDecimal.valueOf(20000), @@ -436,10 +545,8 @@ class 무료주문_생성할때 { Membership membership = createMembership(member, recruitmentRound); IssuedCoupon issuedCoupon = createAndIssue(MONEY_20000_WON, member); - String orderNanoId = "HnbMWoSZRq3qK1W3tPXCW"; - var request = new OrderCreateRequest( - orderNanoId, + ORDER_NANO_ID, membership.getId(), issuedCoupon.getId(), BigDecimal.valueOf(20000), diff --git a/src/test/java/com/gdschongik/gdsc/global/common/constant/OrderConstant.java b/src/test/java/com/gdschongik/gdsc/global/common/constant/OrderConstant.java new file mode 100644 index 000000000..5addd7d9c --- /dev/null +++ b/src/test/java/com/gdschongik/gdsc/global/common/constant/OrderConstant.java @@ -0,0 +1,10 @@ +package com.gdschongik.gdsc.global.common.constant; + +public class OrderConstant { + + public static final String ORDER_NANO_ID = "HnbMWoSZRq3qK1W3tPXCW"; + public static final String ORDER_PAYMENT_KEY = "testPaymentKey"; + public static final String ORDER_CANCEL_REASON = "테스트 주문 취소 사유"; + + private OrderConstant() {} +} diff --git a/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java b/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java index 236887590..3401a349a 100644 --- a/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java +++ b/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java @@ -14,6 +14,8 @@ import com.gdschongik.gdsc.domain.coupon.domain.Coupon; import com.gdschongik.gdsc.domain.coupon.domain.IssuedCoupon; import com.gdschongik.gdsc.domain.discord.application.handler.DelegateMemberDiscordEventHandler; +import com.gdschongik.gdsc.domain.discord.application.handler.MemberDiscordRoleRevokeHandler; +import com.gdschongik.gdsc.domain.discord.application.handler.SpringEventHandler; import com.gdschongik.gdsc.domain.member.dao.MemberRepository; import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.member.domain.MemberManageRole; @@ -87,11 +89,35 @@ public abstract class IntegrationTest { @MockBean protected DelegateMemberDiscordEventHandler delegateMemberDiscordEventHandler; + @MockBean + protected MemberDiscordRoleRevokeHandler memberDiscordRoleRevokeHandler; + @BeforeEach void setUp() { databaseCleaner.execute(); redisCleaner.execute(); + doStubDiscordEventHandler(); + doStubTemplate(); + } + + /** + * stubbing에 사용할 템플릿 메서드입니다. + * 하위 클래스에서 이 메서드를 오버라이드하여 stubbing을 수행합니다. + * 오버라이드된 경우, `@BeforeEach`의 맨 마지막에 호출됩니다. + */ + protected void doStubTemplate() { + // 기본적으로 아무 것도 하지 않습니다. 필요한 경우에만 오버라이드하여 사용합니다. + } + + /** + * {@link SpringEventHandler#delegate} 메서드를 stubbing합니다. + * 해당 핸들러 메서드는 스프링 이벤트를 수신하여 JDA를 통해 디스코드 관련 로직을 처리합니다. + * JDA는 외부 API의 커넥션을 필요로 하기 때문에 테스트에서는 `@MockBean`으로 주입한 핸들러를 stubbing하여 verify로 호출 여부만 확인합니다. + * 기본적으로 아무 것도 하지 않도록 설정합니다. + */ + private void doStubDiscordEventHandler() { doNothing().when(delegateMemberDiscordEventHandler).delegate(any()); + doNothing().when(memberDiscordRoleRevokeHandler).delegate(any()); } protected void logoutAndReloginAs(Long memberId, MemberRole memberRole) { From 0ec74c03860b4bbd51ac1ec0d52c4f57b776c5a3 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Sun, 4 Aug 2024 16:53:13 +0900 Subject: [PATCH 07/22] =?UTF-8?q?fix:=20DTO=EB=A5=BC=20=EC=A0=81=EC=A0=88?= =?UTF-8?q?=ED=95=9C=20=EC=9C=84=EC=B9=98=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?(#569)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: dto를 적절한 위치로 이동 --- .../gdschongik/gdsc/domain/study/api/StudyMentorController.java | 2 +- .../study/{domain => dto}/request/AssignmentCreateRequest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/main/java/com/gdschongik/gdsc/domain/study/{domain => dto}/request/AssignmentCreateRequest.java (89%) 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 cdbd1952f..7e670e052 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 @@ -1,7 +1,7 @@ package com.gdschongik.gdsc.domain.study.api; import com.gdschongik.gdsc.domain.study.application.StudyMentorService; -import com.gdschongik.gdsc.domain.study.domain.request.AssignmentCreateRequest; +import com.gdschongik.gdsc.domain.study.dto.request.AssignmentCreateRequest; import com.gdschongik.gdsc.domain.study.dto.response.AssignmentResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/request/AssignmentCreateRequest.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/AssignmentCreateRequest.java similarity index 89% rename from src/main/java/com/gdschongik/gdsc/domain/study/domain/request/AssignmentCreateRequest.java rename to src/main/java/com/gdschongik/gdsc/domain/study/dto/request/AssignmentCreateRequest.java index 160e492eb..295720ce4 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/request/AssignmentCreateRequest.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/AssignmentCreateRequest.java @@ -1,4 +1,4 @@ -package com.gdschongik.gdsc.domain.study.domain.request; +package com.gdschongik.gdsc.domain.study.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Future; From 10712b8b2c44e0384211d1b29e2b868bacdd22b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=ED=95=9C=EB=B9=84?= <99820610+AlmondBreez3@users.noreply.github.com> Date: Mon, 5 Aug 2024 00:00:23 +0900 Subject: [PATCH 08/22] =?UTF-8?q?feat:=20=EA=B3=BC=EC=A0=9C=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A4=ED=95=98=EA=B8=B0=20API=20=EA=B5=AC=ED=98=84=20(#562)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 과제 개설 api 구현 * develop과 컨플릭트 해결 * feat: validate Mentor * feat: 테스트 케이스 작성 * feat: 불필요한 단어 제거 * feat: validator 객체 전달을 id전달로 변경 * fix: 코드 통일성 유지 * feat: test코드 상수 적용 * feat: 개행 추가 * feat: validate 멘토 로직 수정 * feat: validate Mentor 메소드 에러명 수정 * fix: 배포 오류 * feat: 스터디 멘토 권한 여부 에러코드 수정 * fix: 주석 내용 수정 * feat: 스터디 멘토 권한 에러 코드 409로 변경 --- .../study/api/StudyMentorController.java | 9 ++-- .../study/application/StudyMentorService.java | 16 +++++++ .../gdsc/domain/study/domain/StudyDetail.java | 5 ++ .../study/domain/StudyDetailValidator.java | 29 ++++++++--- .../domain/study/domain/vo/Assignment.java | 11 +++++ .../gdsc/global/exception/ErrorCode.java | 7 ++- .../domain/study/domain/StudyDetailTest.java | 28 +++++++++-- .../domain/StudyDetailValidatorTest.java | 48 ++++++++++++++++++- .../global/common/constant/StudyConstant.java | 4 ++ 9 files changed, 140 insertions(+), 17 deletions(-) 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 7e670e052..a0f26a8cc 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,10 +20,11 @@ public class StudyMentorController { private final StudyMentorService studyMentorService; @Operation(summary = "스터디 과제 개설", description = "멘토만 과제를 개설할 수 있습니다.") - @PutMapping("/assignments/{assignmentId}") - public ResponseEntity createStudyAssignment( - @PathVariable Long assignmentId, @Valid @RequestBody AssignmentCreateRequest request) { - return null; + @PutMapping("/assignments/{studyDetailId}") + public ResponseEntity publishStudyAssignment( + @PathVariable Long studyDetailId, @Valid @RequestBody AssignmentCreateRequest request) { + studyMentorService.publishStudyAssignment(studyDetailId, request); + return ResponseEntity.ok().build(); } @Operation(summary = "스터디 주차별 과제 목록 조회", description = "주차별 스터디 과제 목록을 조회합니다.") 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 df839c803..44f3344b0 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 @@ -6,6 +6,7 @@ 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.request.AssignmentCreateRequest; import com.gdschongik.gdsc.domain.study.dto.response.AssignmentResponse; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.util.MemberUtil; @@ -52,4 +53,19 @@ public void cancelStudyAssignment(Long studyDetailId) { log.info("[StudyMentorService] 과제 휴강 처리: studyDetailId={}", studyDetail.getId()); } + + @Transactional + public void publishStudyAssignment(Long studyDetailId, AssignmentCreateRequest request) { + Member currentMember = memberUtil.getCurrentMember(); + StudyDetail studyDetail = studyDetailRepository + .findById(studyDetailId) + .orElseThrow(() -> new CustomException(STUDY_DETAIL_NOT_FOUND)); + + studyDetailValidator.validatePublishStudyAssignment(currentMember, studyDetail, request); + + studyDetail.publishAssignment(request.title(), request.deadLine(), request.descriptionNotionLink()); + studyDetailRepository.save(studyDetail); + + log.info("[StudyMentorService] 과제 개설 완료: studyDetailId={}", studyDetailId); + } } 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 bf76a233b..323cb62e0 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 @@ -5,6 +5,7 @@ import com.gdschongik.gdsc.domain.study.domain.vo.Assignment; import com.gdschongik.gdsc.domain.study.domain.vo.Session; import jakarta.persistence.*; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -73,4 +74,8 @@ public static StudyDetail createStudyDetail(Study study, Long week, String atten public void cancelAssignment() { assignment = Assignment.cancelAssignment(); } + + public void publishAssignment(String title, LocalDateTime deadLine, String descriptionNotionLink) { + assignment = Assignment.publishAssignment(title, deadLine, descriptionNotionLink); + } } 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 index ea9992eb7..0e1986e54 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidator.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidator.java @@ -1,21 +1,36 @@ package com.gdschongik.gdsc.domain.study.domain; +import static com.gdschongik.gdsc.global.exception.ErrorCode.*; + import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.study.dto.request.AssignmentCreateRequest; import com.gdschongik.gdsc.global.annotation.DomainService; import com.gdschongik.gdsc.global.exception.CustomException; -import com.gdschongik.gdsc.global.exception.ErrorCode; +import java.time.LocalDateTime; @DomainService public class StudyDetailValidator { - public void validateCancelStudyAssignment(Member currentMember, StudyDetail studyDetail) { - validateMemberIsMentor(currentMember, studyDetail); + public void validateCancelStudyAssignment(Member member, StudyDetail studyDetail) { + validateStudyMentorAuthorization(member, studyDetail); + } + + public void validatePublishStudyAssignment( + Member member, StudyDetail studyDetail, AssignmentCreateRequest request) { + validateStudyMentorAuthorization(member, studyDetail); + validateDeadLine(request.deadLine()); + } + + // 해당 스터디의 멘토가 아니라면 스터디에 대한 권한이 없다. + private void validateStudyMentorAuthorization(Member member, StudyDetail studyDetail) { + if (!member.getId().equals(studyDetail.getStudy().getMentor().getId())) { + throw new CustomException(STUDY_DETAIL_UPDATE_RESTRICTED_TO_MENTOR); + } } - // 멘토가 아니라면 과제를 휴강처리 할 수 없다. - private void validateMemberIsMentor(Member member, StudyDetail studyDetail) { - if (!member.equals(studyDetail.getStudy().getMentor())) { - throw new CustomException(ErrorCode.STUDY_DETAIL_NOT_MODIFIABLE_INVALID_ROLE); + private void validateDeadLine(LocalDateTime deadline) { + if (deadline.isBefore(LocalDateTime.now())) { + throw new CustomException(ASSIGNMENT_DEADLINE_INVALID); } } } 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 9228bd4c0..09ea9f4fd 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,6 +1,8 @@ package com.gdschongik.gdsc.domain.study.domain.vo; import static com.gdschongik.gdsc.domain.study.domain.StudyStatus.*; +import static com.gdschongik.gdsc.domain.study.domain.StudyStatus.CANCELLED; +import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import com.gdschongik.gdsc.domain.study.domain.Difficulty; import com.gdschongik.gdsc.domain.study.domain.StudyStatus; @@ -54,4 +56,13 @@ public static Assignment createEmptyAssignment() { public static Assignment cancelAssignment() { return Assignment.builder().status(CANCELLED).build(); } + + public static Assignment publishAssignment(String title, LocalDateTime deadline, String descriptionLink) { + return Assignment.builder() + .title(title) + .deadline(deadline) + .descriptionLink(descriptionLink) + .status(StudyStatus.OPEN) + .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 ba35fdace..251f6e9aa 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -112,7 +112,7 @@ public enum ErrorCode { // StudyDetail STUDY_DETAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 상세 정보입니다."), - STUDY_DETAIL_NOT_MODIFIABLE_INVALID_ROLE(HttpStatus.FORBIDDEN, "수정할 수 있는 권한이 없습니다."), + STUDY_DETAIL_UPDATE_RESTRICTED_TO_MENTOR(HttpStatus.CONFLICT, "해당 스터디의 멘토만 수정할 수 있습니다."), // StudyHistory STUDY_HISTORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 수강 기록입니다."), @@ -140,7 +140,10 @@ public enum ErrorCode { // Order - MoneyInfo ORDER_FINAL_PAYMENT_AMOUNT_MISMATCH(HttpStatus.CONFLICT, "주문 최종결제금액은 주문총액에서 할인금액을 뺀 값이어야 합니다."), - ; + + // Assignment + ASSIGNMENT_CAN_NOT_BE_UPDATED(HttpStatus.CONFLICT, "휴강인 과제는 수정할 수 없습니다."), + ASSIGNMENT_DEADLINE_INVALID(HttpStatus.CONFLICT, "과제 마감 기한이 현재보다 빠릅니다."); private final HttpStatus status; private final String message; 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 index 13592767f..b4bd8023c 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailTest.java @@ -1,5 +1,6 @@ package com.gdschongik.gdsc.domain.study.domain; +import static com.gdschongik.gdsc.global.common.constant.StudyConstant.*; import static org.assertj.core.api.Assertions.*; import com.gdschongik.gdsc.domain.member.domain.Member; @@ -11,11 +12,10 @@ public class StudyDetailTest { + FixtureHelper fixtureHelper = new FixtureHelper(); + @Nested class 과제_휴강_처리시 { - - FixtureHelper fixtureHelper = new FixtureHelper(); - @Test void 과제_상태가_휴강이_된다() { // given @@ -34,4 +34,26 @@ class 과제_휴강_처리시 { assertThat(studyDetail.getAssignment().getStatus()).isEqualTo(StudyStatus.CANCELLED); } } + + @Nested + class 과제_개설시 { + + @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.publishAssignment(ASSIGNMENT_TITLE, now.plusDays(1), DESCRIPTION_LINK); + + // then + assertThat(studyDetail.getAssignment().getStatus()).isEqualTo(StudyStatus.OPEN); + } + } } 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 index 0c933bc6c..789b236c8 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidatorTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidatorTest.java @@ -1,10 +1,12 @@ package com.gdschongik.gdsc.domain.study.domain; +import static com.gdschongik.gdsc.global.common.constant.StudyConstant.*; 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.domain.study.dto.request.AssignmentCreateRequest; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.helper.FixtureHelper; import java.time.LocalDateTime; @@ -34,7 +36,51 @@ class 과제_휴강_처리시 { // when & then assertThatThrownBy(() -> studyDetailValidator.validateCancelStudyAssignment(anotherMember, studyDetail)) .isInstanceOf(CustomException.class) - .hasMessage(STUDY_DETAIL_NOT_MODIFIABLE_INVALID_ROLE.getMessage()); + .hasMessage(STUDY_DETAIL_UPDATE_RESTRICTED_TO_MENTOR.getMessage()); + } + } + + @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); + AssignmentCreateRequest request = + new AssignmentCreateRequest(ASSIGNMENT_TITLE, DESCRIPTION_LINK, now.plusDays(2)); + + // when & then + assertThatThrownBy(() -> + studyDetailValidator.validatePublishStudyAssignment(anotherMember, studyDetail, request)) + .isInstanceOf(CustomException.class) + .hasMessage(STUDY_DETAIL_UPDATE_RESTRICTED_TO_MENTOR.getMessage()); + } + + @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)); + AssignmentCreateRequest request = + new AssignmentCreateRequest(ASSIGNMENT_TITLE, DESCRIPTION_LINK, now.minusDays(2)); + + // when & then + assertThatThrownBy(() -> studyDetailValidator.validatePublishStudyAssignment(mentor, studyDetail, request)) + .isInstanceOf(CustomException.class) + .hasMessage(ASSIGNMENT_DEADLINE_INVALID.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 0cc120400..b9839811b 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 @@ -16,4 +16,8 @@ private StudyConstant() {} // StudyDetail public static final String ATTENDANCE_NUMBER = "1234"; + + // Assignment + public static final String ASSIGNMENT_TITLE = "testTitle"; + public static final String DESCRIPTION_LINK = "www.link.com"; } From 1e6239ca5ad4eb498a676a31a016041c62b41638 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:05:16 +0900 Subject: [PATCH 09/22] =?UTF-8?q?fix:=20PrincipalDetails=EA=B0=80=20?= =?UTF-8?q?=EC=97=AD=ED=95=A0=EC=9D=98=20=EA=B0=92=EC=9D=84=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#5?= =?UTF-8?q?73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: 멤버가 여러 역할로 시큐리티를 통과하도록 수정 --- .../gdschongik/gdsc/global/security/PrincipalDetails.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/global/security/PrincipalDetails.java b/src/main/java/com/gdschongik/gdsc/global/security/PrincipalDetails.java index 473ddc55f..38e0649ce 100644 --- a/src/main/java/com/gdschongik/gdsc/global/security/PrincipalDetails.java +++ b/src/main/java/com/gdschongik/gdsc/global/security/PrincipalDetails.java @@ -28,9 +28,9 @@ public static PrincipalDetails from(AccessTokenDto token) { public Collection getAuthorities() { Collection authorities = new ArrayList<>(); - authorities.add(new SimpleGrantedAuthority(role.name())); - authorities.add(new SimpleGrantedAuthority(manageRole.name())); - authorities.add(new SimpleGrantedAuthority(studyRole.name())); + authorities.add(new SimpleGrantedAuthority(role.getValue())); + authorities.add(new SimpleGrantedAuthority(manageRole.getValue())); + authorities.add(new SimpleGrantedAuthority(studyRole.getValue())); return authorities; } From 84ab2922ad241ab4b6aac8083b5c30f2c52319ef Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:11:11 +0900 Subject: [PATCH 10/22] =?UTF-8?q?feat:=20=EA=B9=83=ED=97=88=EB=B8=8C=20api?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(#566)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 깃허브 api 연결 설정 추가 * rename: GithubClient로 이름 변경 --- build.gradle | 3 +++ .../gdsc/global/config/GithubConfig.java | 21 +++++++++++++++++++ .../gdsc/global/config/PropertyConfig.java | 4 +++- .../gdsc/global/property/GithubProperty.java | 12 +++++++++++ .../infra/client/github/GithubClient.java | 12 +++++++++++ src/main/resources/application-github.yml | 2 ++ src/main/resources/application.yml | 1 + 7 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gdschongik/gdsc/global/config/GithubConfig.java create mode 100644 src/main/java/com/gdschongik/gdsc/global/property/GithubProperty.java create mode 100644 src/main/java/com/gdschongik/gdsc/infra/client/github/GithubClient.java create mode 100644 src/main/resources/application-github.yml diff --git a/build.gradle b/build.gradle index f3579ce80..c09ffb85b 100644 --- a/build.gradle +++ b/build.gradle @@ -92,6 +92,9 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' implementation 'io.github.openfeign:feign-jackson' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + + // Github + implementation 'org.kohsuke:github-api:1.323' } tasks.named('test') { diff --git a/src/main/java/com/gdschongik/gdsc/global/config/GithubConfig.java b/src/main/java/com/gdschongik/gdsc/global/config/GithubConfig.java new file mode 100644 index 000000000..eb5dd6ac5 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/config/GithubConfig.java @@ -0,0 +1,21 @@ +package com.gdschongik.gdsc.global.config; + +import com.gdschongik.gdsc.global.property.GithubProperty; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class GithubConfig { + + private final GithubProperty githubProperty; + + @Bean + public GitHub github() throws IOException { + return new GitHubBuilder().withOAuthToken(githubProperty.getSecretKey()).build(); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/global/config/PropertyConfig.java b/src/main/java/com/gdschongik/gdsc/global/config/PropertyConfig.java index 4b304a2c7..e27273d6b 100644 --- a/src/main/java/com/gdschongik/gdsc/global/config/PropertyConfig.java +++ b/src/main/java/com/gdschongik/gdsc/global/config/PropertyConfig.java @@ -3,6 +3,7 @@ import com.gdschongik.gdsc.global.property.BasicAuthProperty; import com.gdschongik.gdsc.global.property.DiscordProperty; import com.gdschongik.gdsc.global.property.EmailProperty; +import com.gdschongik.gdsc.global.property.GithubProperty; import com.gdschongik.gdsc.global.property.JwtProperty; import com.gdschongik.gdsc.global.property.PaymentProperty; import com.gdschongik.gdsc.global.property.RedisProperty; @@ -15,7 +16,8 @@ BasicAuthProperty.class, DiscordProperty.class, EmailProperty.class, - PaymentProperty.class + PaymentProperty.class, + GithubProperty.class }) @Configuration public class PropertyConfig {} diff --git a/src/main/java/com/gdschongik/gdsc/global/property/GithubProperty.java b/src/main/java/com/gdschongik/gdsc/global/property/GithubProperty.java new file mode 100644 index 000000000..2e95cb533 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/property/GithubProperty.java @@ -0,0 +1,12 @@ +package com.gdschongik.gdsc.global.property; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@AllArgsConstructor +@ConfigurationProperties(prefix = "github") +public class GithubProperty { + private final String secretKey; +} diff --git a/src/main/java/com/gdschongik/gdsc/infra/client/github/GithubClient.java b/src/main/java/com/gdschongik/gdsc/infra/client/github/GithubClient.java new file mode 100644 index 000000000..e3235dfb7 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/infra/client/github/GithubClient.java @@ -0,0 +1,12 @@ +package com.gdschongik.gdsc.infra.client.github; + +import lombok.RequiredArgsConstructor; +import org.kohsuke.github.GitHub; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class GithubClient { + + private final GitHub github; +} diff --git a/src/main/resources/application-github.yml b/src/main/resources/application-github.yml new file mode 100644 index 000000000..131a13821 --- /dev/null +++ b/src/main/resources/application-github.yml @@ -0,0 +1,2 @@ +github: + secret-key: ${GITHUB_SECRET_KEY:} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 899546814..1c4604ac1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,6 +12,7 @@ spring: - discord - email - payment + - github logging: level: From fd20de141e8773acfbd8a728a93d5411c68ca4ea Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:01:10 +0900 Subject: [PATCH 11/22] =?UTF-8?q?refactor:=20=EB=A9=98=ED=86=A0=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=A0=95=EB=A6=AC=20(#574)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 멘토 컨트롤러 엔드포인트 정리 * feat: 멘토 api 접근 가능한 권한을 설정 * fix: 가독성 위해 하이픈 추가 * fix: path variable로 변경 * rename: 서비스 클래스 이름 변경 --- ....java => MentorStudyDetailController.java} | 37 +++++++++++-------- ...ice.java => MentorStudyDetailService.java} | 2 +- .../gdsc/global/config/WebSecurityConfig.java | 2 + ...java => MentorStudyDetailServiceTest.java} | 6 +-- 4 files changed, 28 insertions(+), 19 deletions(-) rename src/main/java/com/gdschongik/gdsc/domain/study/api/{StudyMentorController.java => MentorStudyDetailController.java} (53%) rename src/main/java/com/gdschongik/gdsc/domain/study/application/{StudyMentorService.java => MentorStudyDetailService.java} (98%) rename src/test/java/com/gdschongik/gdsc/domain/study/application/{StudyMentorServiceTest.java => MentorStudyDetailServiceTest.java} (89%) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudyMentorController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyDetailController.java similarity index 53% rename from src/main/java/com/gdschongik/gdsc/domain/study/api/StudyMentorController.java rename to src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyDetailController.java index a0f26a8cc..7e8c3fbee 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudyMentorController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyDetailController.java @@ -1,6 +1,6 @@ package com.gdschongik.gdsc.domain.study.api; -import com.gdschongik.gdsc.domain.study.application.StudyMentorService; +import com.gdschongik.gdsc.domain.study.application.MentorStudyDetailService; import com.gdschongik.gdsc.domain.study.dto.request.AssignmentCreateRequest; import com.gdschongik.gdsc.domain.study.dto.response.AssignmentResponse; import io.swagger.v3.oas.annotations.Operation; @@ -9,42 +9,49 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +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 Study", description = "멘토 스터디 관리 API입니다.") +@Tag(name = "Mentor StudyDetail", description = "멘토 스터디 상세 관리 API입니다.") @RestController -@RequestMapping("/mentor/studies") +@RequestMapping("/mentor/study-details") @RequiredArgsConstructor -public class StudyMentorController { +public class MentorStudyDetailController { - private final StudyMentorService studyMentorService; + private final MentorStudyDetailService mentorStudyDetailService; @Operation(summary = "스터디 과제 개설", description = "멘토만 과제를 개설할 수 있습니다.") - @PutMapping("/assignments/{studyDetailId}") + @PutMapping("/{studyDetailId}/assignments") public ResponseEntity publishStudyAssignment( @PathVariable Long studyDetailId, @Valid @RequestBody AssignmentCreateRequest request) { - studyMentorService.publishStudyAssignment(studyDetailId, request); + mentorStudyDetailService.publishStudyAssignment(studyDetailId, request); return ResponseEntity.ok().build(); } @Operation(summary = "스터디 주차별 과제 목록 조회", description = "주차별 스터디 과제 목록을 조회합니다.") - @GetMapping("/assignments/{studyId}") - public ResponseEntity> getWeeklyAssignments(@PathVariable Long studyId) { - List response = studyMentorService.getWeeklyAssignments(studyId); + @GetMapping("/assignments") + public ResponseEntity> getWeeklyAssignments(@RequestParam(name = "studyId") Long studyId) { + List response = mentorStudyDetailService.getWeeklyAssignments(studyId); return ResponseEntity.ok(response); } @Operation(summary = "스터디 과제 상세 조회", description = "멘토가 자신의 스터디 과제를 조회합니다.") - @GetMapping("/assignments/{studyDetailId}") + @GetMapping("/{studyDetailId}/assignments") public ResponseEntity getStudyAssignment(@PathVariable Long studyDetailId) { - AssignmentResponse response = studyMentorService.getAssignment(studyDetailId); + AssignmentResponse response = mentorStudyDetailService.getAssignment(studyDetailId); return ResponseEntity.ok(response); } @Operation(summary = "스터디 과제 휴강 처리", description = "해당 주차 과제를 휴강 처리합니다.") - @PatchMapping("/assignments/{studyDetailId}/cancel") + @PatchMapping("/{studyDetailId}/assignments/cancel") public ResponseEntity cancelStudyAssignment(@PathVariable Long studyDetailId) { - studyMentorService.cancelStudyAssignment(studyDetailId); + mentorStudyDetailService.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/MentorStudyDetailService.java similarity index 98% rename from src/main/java/com/gdschongik/gdsc/domain/study/application/StudyMentorService.java rename to src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java index 44f3344b0..88f561e9a 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyMentorService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java @@ -19,7 +19,7 @@ @Slf4j @Service @RequiredArgsConstructor -public class StudyMentorService { +public class MentorStudyDetailService { private final MemberUtil memberUtil; private final StudyDetailRepository studyDetailRepository; diff --git a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java index 077a68923..64086e85b 100644 --- a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java +++ b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java @@ -116,6 +116,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authenticated() .requestMatchers("/admin/**") .hasRole("ADMIN") + .requestMatchers("/mentor/**") + .hasRole("MENTOR") .anyRequest() .authenticated()); diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/application/StudyMentorServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailServiceTest.java similarity index 89% rename from src/test/java/com/gdschongik/gdsc/domain/study/application/StudyMentorServiceTest.java rename to src/test/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailServiceTest.java index c4ecb95ff..f428d04c7 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/study/application/StudyMentorServiceTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailServiceTest.java @@ -15,10 +15,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -public class StudyMentorServiceTest extends IntegrationTest { +public class MentorStudyDetailServiceTest extends IntegrationTest { @Autowired - private StudyMentorService studyMentorService; + private MentorStudyDetailService mentorStudyDetailService; @Autowired private StudyDetailRepository studyDetailRepository; @@ -39,7 +39,7 @@ class 스터디_과제_휴강_처리시 { logoutAndReloginAs(studyDetail.getStudy().getMentor().getId(), MemberRole.ASSOCIATE); // when - studyMentorService.cancelStudyAssignment(studyDetail.getId()); + mentorStudyDetailService.cancelStudyAssignment(studyDetail.getId()); // then StudyDetail cancelledStudyDetail = From 6fe9e89d3e48ff19a3ece49cbe2fc5657cbb4e1d Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Tue, 6 Aug 2024 20:36:08 +0900 Subject: [PATCH 12/22] =?UTF-8?q?feat:=20=EA=B3=BC=EC=A0=9C=20=ED=9E=88?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#570)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 과제 히스토리 테이블 추가 * rename: 변수명 수정 * rename: enum name 수정 * refactor: 과제 제출 결과와 실패 사유를 분리 --- .../study/domain/AssignmentHistory.java | 78 +++++++++++++++++++ .../domain/AssignmentSubmissionStatus.java | 15 ++++ .../study/domain/SubmissionFailureType.java | 14 ++++ 3 files changed, 107 insertions(+) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistory.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionStatus.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/domain/SubmissionFailureType.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistory.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistory.java new file mode 100644 index 000000000..e340b458e --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistory.java @@ -0,0 +1,78 @@ +package com.gdschongik.gdsc.domain.study.domain; + +import com.gdschongik.gdsc.domain.common.model.BaseEntity; +import com.gdschongik.gdsc.domain.member.domain.Member; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AssignmentHistory extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "assignment_history_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "study_detail_id") + private StudyDetail studyDetail; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Column(columnDefinition = "TEXT") + private String submissionLink; + + private String commitHash; + + private Long contentLength; + + @Enumerated(EnumType.STRING) + private AssignmentSubmissionStatus submissionStatus; + + @Enumerated(EnumType.STRING) + private SubmissionFailureType submissionFailureType; + + @Builder(access = AccessLevel.PRIVATE) + private AssignmentHistory( + StudyDetail studyDetail, + Member member, + String submissionLink, + String commitHash, + Long contentLength, + AssignmentSubmissionStatus submissionStatus) { + this.studyDetail = studyDetail; + this.member = member; + this.submissionLink = submissionLink; + this.commitHash = commitHash; + this.contentLength = contentLength; + this.submissionStatus = submissionStatus; + } + + public static AssignmentHistory create( + StudyDetail studyDetail, Member member, String submissionLink, String commitHash, Long contentLength) { + return AssignmentHistory.builder() + .studyDetail(studyDetail) + .member(member) + .submissionLink(submissionLink) + .commitHash(commitHash) + .contentLength(contentLength) + .submissionStatus(AssignmentSubmissionStatus.PENDING) + .build(); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionStatus.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionStatus.java new file mode 100644 index 000000000..4ac62cd11 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionStatus.java @@ -0,0 +1,15 @@ +package com.gdschongik.gdsc.domain.study.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AssignmentSubmissionStatus { + PENDING("제출 전"), + FAILURE("제출 실패"), + SUCCESS("제출 성공"), + CANCELLED("과제 휴강"); + + private final String value; +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/SubmissionFailureType.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/SubmissionFailureType.java new file mode 100644 index 000000000..252b556e9 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/SubmissionFailureType.java @@ -0,0 +1,14 @@ +package com.gdschongik.gdsc.domain.study.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum SubmissionFailureType { + NOT_SUBMITTED("미제출"), + WORD_COUNT_INSUFFICIENT("글자수 부족"), + LOCATION_UNIDENTIFIABLE("위치 확인불가"); + + private final String value; +} From 1292b0fa4d9d47c24baf5915ef4be1b7d63215bd Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Tue, 6 Aug 2024 22:14:46 +0900 Subject: [PATCH 13/22] =?UTF-8?q?refactor:=20=EB=AA=A8=EC=A7=91=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EC=A7=91=ED=9A=8C=EC=B0=A8=EA=B0=80=20?= =?UTF-8?q?=EC=97=86=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20null=EC=9D=84=20=EB=8D=98=EC=A7=80?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#579)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 모집중인 모집회차가 없을 경우 예외 대신 null을 던지도록 수정 * fix: 옵셔널 언래핑 로직 제거 --- .../domain/member/application/OnboardingMemberService.java | 7 ++++--- .../member/dto/response/MemberDashboardResponse.java | 4 ++-- .../application/OnboardingRecruitmentService.java | 7 ++----- .../com/gdschongik/gdsc/global/exception/ErrorCode.java | 1 - .../member/application/OnboardingMemberServiceTest.java | 4 +++- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java index d6f96c50a..846d66d48 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberService.java @@ -71,15 +71,16 @@ public MemberBasicInfoResponse getMemberBasicInfo() { public MemberDashboardResponse getDashboard() { final Member member = memberUtil.getCurrentMember(); - final RecruitmentRound currentRecruitmentRound = onboardingRecruitmentService.findCurrentRecruitmentRound(); - final Optional myMembership = membershipService.findMyMembership(member, currentRecruitmentRound); final Optional univEmailVerification = univEmailVerificationService.getUnivEmailVerificationFromRedis(member.getId()); UnivVerificationStatus univVerificationStatus = emailVerificationStatusService.determineStatus(member, univEmailVerification); + Optional currentRecruitmentRound = onboardingRecruitmentService.findCurrentRecruitmentRound(); + Optional myMembership = currentRecruitmentRound.flatMap( + recruitmentRound -> membershipService.findMyMembership(member, recruitmentRound)); return MemberDashboardResponse.of( - member, univVerificationStatus, currentRecruitmentRound, myMembership.orElse(null)); + member, univVerificationStatus, currentRecruitmentRound.orElse(null), myMembership.orElse(null)); } public MemberTokenResponse createTemporaryToken(MemberTokenRequest request) { diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberDashboardResponse.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberDashboardResponse.java index ca88b1b23..85b996ee4 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberDashboardResponse.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberDashboardResponse.java @@ -11,7 +11,7 @@ public record MemberDashboardResponse( MemberFullDto member, - RecruitmentRoundFullDto currentRecruitmentRound, + @Nullable RecruitmentRoundFullDto currentRecruitmentRound, @Nullable MembershipFullDto currentMembership) { public static MemberDashboardResponse of( Member member, @@ -20,7 +20,7 @@ public static MemberDashboardResponse of( Membership currentMembership) { return new MemberDashboardResponse( MemberFullDto.of(member, univVerificationStatus), - RecruitmentRoundFullDto.from(currentRecruitmentRound), + currentRecruitmentRound == null ? null : RecruitmentRoundFullDto.from(currentRecruitmentRound), currentMembership == null ? null : MembershipFullDto.from(currentMembership)); } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/recruitment/application/OnboardingRecruitmentService.java b/src/main/java/com/gdschongik/gdsc/domain/recruitment/application/OnboardingRecruitmentService.java index f2f43a5ec..3913d6a46 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/recruitment/application/OnboardingRecruitmentService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/recruitment/application/OnboardingRecruitmentService.java @@ -2,8 +2,6 @@ import com.gdschongik.gdsc.domain.recruitment.dao.RecruitmentRoundRepository; import com.gdschongik.gdsc.domain.recruitment.domain.RecruitmentRound; -import com.gdschongik.gdsc.global.exception.CustomException; -import com.gdschongik.gdsc.global.exception.ErrorCode; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -17,11 +15,10 @@ public class OnboardingRecruitmentService { private final RecruitmentRoundRepository recruitmentRoundRepository; // TODO: 모집기간과 별도로 표시기간 사용하여 필터링하도록 변경 - public RecruitmentRound findCurrentRecruitmentRound() { + public Optional findCurrentRecruitmentRound() { return recruitmentRoundRepository.findAll().stream() .filter(RecruitmentRound::isOpen) // isOpen -> isDisplayable - .findFirst() - .orElseThrow(() -> new CustomException(ErrorCode.RECRUITMENT_ROUND_OPEN_NOT_FOUND)); + .findFirst(); } /** 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 251f6e9aa..02b1dcf3f 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -89,7 +89,6 @@ public enum ErrorCode { RECRUITMENT_ROUND_TYPE_OVERLAP(HttpStatus.BAD_REQUEST, "모집 차수가 중복됩니다."), RECRUITMENT_ROUND_STARTDATE_ALREADY_PASSED(HttpStatus.BAD_REQUEST, "이미 모집 시작일이 지난 모집회차입니다."), ROUND_ONE_DOES_NOT_EXIST(HttpStatus.CONFLICT, "1차 모집이 존재하지 않습니다."), - RECRUITMENT_ROUND_OPEN_NOT_FOUND(HttpStatus.NOT_FOUND, "진행중인 모집회차가 존재하지 않습니다."), // Coupon COUPON_DISCOUNT_AMOUNT_NOT_POSITIVE(HttpStatus.CONFLICT, "쿠폰의 할인 금액은 0보다 커야 합니다."), diff --git a/src/test/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberServiceTest.java index a1af8c6b6..4438d08e3 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberServiceTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/member/application/OnboardingMemberServiceTest.java @@ -11,6 +11,7 @@ import com.gdschongik.gdsc.domain.recruitment.domain.RecruitmentRound; import com.gdschongik.gdsc.domain.recruitment.domain.vo.Period; import com.gdschongik.gdsc.helper.IntegrationTest; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -31,7 +32,8 @@ class 대시보드_조회할때 { @BeforeEach void setUp() { RecruitmentRound recruitmentRound = createRecruitmentRound(); - when(onboardingRecruitmentService.findCurrentRecruitmentRound()).thenReturn(recruitmentRound); + when(onboardingRecruitmentService.findCurrentRecruitmentRound()) + .thenReturn(Optional.ofNullable(recruitmentRound)); } @Test From cafb05a53172d1fdbed73ab439dae8db84c9bd10 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Wed, 7 Aug 2024 00:07:36 +0900 Subject: [PATCH 14/22] =?UTF-8?q?feat:=20=EC=BF=A0=ED=82=A4=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EC=89=BD=EA=B2=8C=20=EA=B0=9C=EC=84=A0=20(#576)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 로컬 서버와 클라이언트 같이 구동하는 케이스 추가 * feat: 임시 헤더 토큰 심는 로직 제거 * feat: 리다이렉트 URI에서 파라미터 제거 * feat: 테스트 전용 유저 생성 API 구현 * feat: LandingStatus 제거 * feat: base uri 검증 조건 및 로그 수정 * feat: 미사용 상수 제거 * feat: 쿠키 사용 시 로컬 프록시를 전제하도록 로직 변경 * chore: 미사용 import 제거 * feat: referer를 사용하여 처리하도록 base uri 관련 로직 제거 --- .../member/api/TestMemberController.java | 9 ++++ .../member/application/TestMemberService.java | 49 +++++++++++++++++++ .../common/constant/SecurityConstant.java | 6 --- .../global/common/constant/UrlConstant.java | 8 --- .../gdsc/global/config/WebSecurityConfig.java | 3 +- .../gdsc/global/exception/ErrorCode.java | 2 - .../global/security/CustomOAuth2User.java | 2 - .../global/security/CustomSuccessHandler.java | 42 ++-------------- .../gdsc/global/security/LandingStatus.java | 5 -- .../gdsc/global/util/CookieUtil.java | 26 +++------- 10 files changed, 71 insertions(+), 81 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/member/application/TestMemberService.java delete mode 100644 src/main/java/com/gdschongik/gdsc/global/security/LandingStatus.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/api/TestMemberController.java b/src/main/java/com/gdschongik/gdsc/domain/member/api/TestMemberController.java index c16800b27..eaf3a9dc4 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/api/TestMemberController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/api/TestMemberController.java @@ -2,6 +2,7 @@ import com.gdschongik.gdsc.domain.member.application.AdminMemberService; import com.gdschongik.gdsc.domain.member.application.OnboardingMemberService; +import com.gdschongik.gdsc.domain.member.application.TestMemberService; import com.gdschongik.gdsc.domain.member.dto.request.MemberTokenRequest; import com.gdschongik.gdsc.domain.member.dto.response.MemberTokenResponse; import io.swagger.v3.oas.annotations.Operation; @@ -17,9 +18,17 @@ @RequiredArgsConstructor public class TestMemberController { + private final TestMemberService testMemberService; private final OnboardingMemberService onboardingMemberService; private final AdminMemberService adminMemberService; + @Operation(summary = "게스트 회원 생성", description = "테스트용 API입니다. 깃허브 핸들명을 입력받아 임시 회원을 생성합니다.") + @PostMapping + public ResponseEntity createTemporaryMember(@RequestParam("handle") String githubHandle) { + testMemberService.createTestMember(githubHandle); + return ResponseEntity.ok().build(); + } + @Operation(summary = "임시 토큰 생성", description = "테스트용 API입니다. oauth_id를 입력받아 해당하는 유저의 토큰을 생성합니다.") @PostMapping("/token") public ResponseEntity createTemporaryToken(@Valid @RequestBody MemberTokenRequest request) { diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/TestMemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/TestMemberService.java new file mode 100644 index 000000000..d30703784 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/TestMemberService.java @@ -0,0 +1,49 @@ +package com.gdschongik.gdsc.domain.member.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.global.exception.CustomException; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestClient; + +@Slf4j +@Service +public class TestMemberService { + + private final MemberRepository memberRepository; + private final RestClient restClient; + + public TestMemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + this.restClient = RestClient.builder().baseUrl("https://api.github.com").build(); + } + + @Transactional + public void createTestMember(String githubHandle) { + String githubOauthId = getGithubOauthId(githubHandle); + + if (memberRepository.findByOauthId(githubOauthId).isPresent()) { + throw new CustomException(INTERNAL_SERVER_ERROR); + } + + Member guestMember = Member.createGuestMember(githubOauthId); + memberRepository.save(guestMember); + } + + private String getGithubOauthId(String githubHandle) { + return Optional.ofNullable(restClient + .get() + .uri("/users/{githubHandle}", githubHandle) + .retrieve() + .body(GithubUser.class)) + .map(GithubUser::id) + .orElseThrow(() -> new CustomException(INTERNAL_SERVER_ERROR)); + } + + private record GithubUser(String id) {} +} diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java index 1f72b661f..1df68bb01 100644 --- a/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java @@ -2,17 +2,11 @@ public class SecurityConstant { - public static final String LANDING_STATUS_PARAM = "landing-status"; - public static final String ACCESS_TOKEN_PARAM = "access"; - public static final String REFRESH_TOKEN_PARAM = "refresh"; public static final String TOKEN_ROLE_NAME = "role"; public static final String TOKEN_MANAGE_ROLE_NAME = "manageRole"; public static final String TOKEN_STUDY_ROLE_NAME = "studyRole"; public static final String GITHUB_NAME_ATTR_KEY = "id"; public static final String ACCESS_TOKEN_HEADER_PREFIX = "Bearer "; - public static final String ACCESS_TOKEN_HEADER_NAME = "Authorization"; - public static final String OAUTH_BASE_URI_COOKIE_NAME = "oauth-base-uri"; - public static final String OAUTH_REDIRECT_PATH_SEGMENT = "/social-login/redirect"; private SecurityConstant() {} } diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/UrlConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/UrlConstant.java index e307e0269..a4623c5d8 100644 --- a/src/main/java/com/gdschongik/gdsc/global/common/constant/UrlConstant.java +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/UrlConstant.java @@ -1,7 +1,5 @@ package com.gdschongik.gdsc.global.common.constant; -import java.util.List; - public class UrlConstant { private UrlConstant() {} @@ -16,12 +14,6 @@ private UrlConstant() {} public static final String LOCAL_VITE_CLIENT_URL = "http://localhost:5173"; public static final String LOCAL_VITE_CLIENT_SECURE_URL = "https://localhost:5173"; public static final String LOCAL_PROXY_CLIENT_ONBOARDING_URL = "https://local-onboarding.gdschongik.com"; - public static final List LOCAL_CLIENT_URLS = List.of( - LOCAL_REACT_CLIENT_URL, - LOCAL_REACT_CLIENT_SECURE_URL, - LOCAL_VITE_CLIENT_URL, - LOCAL_VITE_CLIENT_SECURE_URL, - LOCAL_PROXY_CLIENT_ONBOARDING_URL); // 서버 URL public static final String PROD_SERVER_URL = "https://api.gdschongik.com"; diff --git a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java index 64086e85b..290114147 100644 --- a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java +++ b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java @@ -174,10 +174,11 @@ public CorsConfigurationSource corsConfigurationSource() { configuration.addAllowedOriginPattern(LOCAL_REACT_CLIENT_SECURE_URL); configuration.addAllowedOriginPattern(LOCAL_VITE_CLIENT_URL); configuration.addAllowedOriginPattern(LOCAL_VITE_CLIENT_SECURE_URL); - configuration.addAllowedOriginPattern(LOCAL_PROXY_CLIENT_ONBOARDING_URL); configuration.addAllowedOriginPattern(DEV_SERVER_URL); } + configuration.addAllowedOriginPattern(LOCAL_PROXY_CLIENT_ONBOARDING_URL); + configuration.addAllowedHeader("*"); configuration.addAllowedMethod("*"); configuration.setAllowCredentials(true); 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 02b1dcf3f..5478c2499 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -18,8 +18,6 @@ public enum ErrorCode { EXPIRED_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 JWT 토큰입니다."), AUTH_NOT_EXIST(HttpStatus.INTERNAL_SERVER_ERROR, "시큐리티 인증 정보가 존재하지 않습니다."), AUTH_NOT_PARSABLE(HttpStatus.INTERNAL_SERVER_ERROR, "시큐리티 인증 정보 파싱에 실패했습니다."), - BASE_URI_COOKIE_NOT_FOUND(HttpStatus.NOT_FOUND, "Base URI 쿠키가 존재하지 않습니다."), - NOT_ALLOWED_BASE_URI(HttpStatus.FORBIDDEN, "허용되지 않은 Base URI입니다."), INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."), INVALID_ROLE(HttpStatus.FORBIDDEN, "권한이 없습니다."), diff --git a/src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2User.java b/src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2User.java index 742cce47a..5a7b62cb0 100644 --- a/src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2User.java +++ b/src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2User.java @@ -11,11 +11,9 @@ public class CustomOAuth2User extends DefaultOAuth2User { private final MemberAuthInfo memberAuthInfo; - private final LandingStatus landingStatus; public CustomOAuth2User(OAuth2User oAuth2User, Member member) { super(oAuth2User.getAuthorities(), oAuth2User.getAttributes(), GITHUB_NAME_ATTR_KEY); memberAuthInfo = MemberAuthInfo.from(member); - landingStatus = LandingStatus.TO_DASHBOARD; } } diff --git a/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java b/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java index afd2a6860..f263c3dc5 100644 --- a/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java +++ b/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java @@ -7,16 +7,14 @@ import com.gdschongik.gdsc.domain.auth.application.JwtService; import com.gdschongik.gdsc.domain.auth.dto.AccessTokenDto; import com.gdschongik.gdsc.domain.auth.dto.RefreshTokenDto; -import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.util.CookieUtil; -import jakarta.servlet.http.Cookie; +import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; -import org.springframework.web.util.UriComponentsBuilder; @Slf4j public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { @@ -27,14 +25,13 @@ public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler public CustomSuccessHandler(JwtService jwtService, CookieUtil cookieUtil) { this.jwtService = jwtService; this.cookieUtil = cookieUtil; + setUseReferer(true); } @Override public void onAuthenticationSuccess( HttpServletRequest request, HttpServletResponse response, Authentication authentication) - throws IOException { - String baseUri = determineTargetUrl(request, response); - + throws IOException, ServletException { CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal(); // 토큰 생성 후 쿠키에 저장 @@ -43,37 +40,6 @@ public void onAuthenticationSuccess( RefreshTokenDto refreshTokenDto = jwtService.createRefreshToken(memberAuthInfo.memberId()); cookieUtil.addTokenCookies(response, accessTokenDto.tokenValue(), refreshTokenDto.tokenValue()); - // 임시로 헤더에 엑세스 토큰 추가 - response.addHeader(ACCESS_TOKEN_HEADER_NAME, ACCESS_TOKEN_HEADER_PREFIX + accessTokenDto.tokenValue()); - - String redirectUri = UriComponentsBuilder.fromHttpUrl(baseUri) - .path(OAUTH_REDIRECT_PATH_SEGMENT) - .queryParam(LANDING_STATUS_PARAM, oAuth2User.getLandingStatus().name()) - .queryParam(ACCESS_TOKEN_PARAM, accessTokenDto.tokenValue()) - .queryParam(REFRESH_TOKEN_PARAM, refreshTokenDto.tokenValue()) - .toUriString(); - - getRedirectStrategy().sendRedirect(request, response, redirectUri); - } - - @Override - protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) { - Cookie baseUriCookie = cookieUtil - .findCookie(request, OAUTH_BASE_URI_COOKIE_NAME) - .orElseThrow(() -> new CustomException(BASE_URI_COOKIE_NOT_FOUND)); - - String baseUri = baseUriCookie.getValue(); - validateBaseUri(baseUri); - - cookieUtil.deleteCookie(response, baseUriCookie); - - return baseUri; - } - - private void validateBaseUri(String baseUri) { - if (!baseUri.endsWith(ROOT_DOMAIN) && !LOCAL_CLIENT_URLS.contains(baseUri)) { - log.error("허용되지 않은 BASE URI로의 리다이렉트 요청 발생: {}", baseUri); - throw new CustomException(NOT_ALLOWED_BASE_URI); - } + super.onAuthenticationSuccess(request, response, authentication); } } diff --git a/src/main/java/com/gdschongik/gdsc/global/security/LandingStatus.java b/src/main/java/com/gdschongik/gdsc/global/security/LandingStatus.java deleted file mode 100644 index 01aa94fb5..000000000 --- a/src/main/java/com/gdschongik/gdsc/global/security/LandingStatus.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.gdschongik.gdsc.global.security; - -public enum LandingStatus { - TO_DASHBOARD; -} diff --git a/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java b/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java index b8722d1b9..c600ab51b 100644 --- a/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java +++ b/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java @@ -1,52 +1,40 @@ package com.gdschongik.gdsc.global.util; +import static com.gdschongik.gdsc.global.common.constant.UrlConstant.*; + import com.gdschongik.gdsc.global.common.constant.JwtConstant; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.Arrays; import java.util.Optional; -import lombok.RequiredArgsConstructor; import org.springframework.boot.web.server.Cookie; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; @Component -@RequiredArgsConstructor public class CookieUtil { - private final EnvironmentUtil environmentUtil; - public void addTokenCookies(HttpServletResponse response, String accessToken, String refreshToken) { - - String sameSite = determineSameSitePolicy(); - - ResponseCookie accessTokenCookie = - generateTokenCookie(JwtConstant.ACCESS_TOKEN.getCookieName(), accessToken, sameSite); + ResponseCookie accessTokenCookie = generateTokenCookie(JwtConstant.ACCESS_TOKEN.getCookieName(), accessToken); ResponseCookie refreshTokenCookie = - generateTokenCookie(JwtConstant.REFRESH_TOKEN.getCookieName(), refreshToken, sameSite); + generateTokenCookie(JwtConstant.REFRESH_TOKEN.getCookieName(), refreshToken); response.addHeader(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); } - private ResponseCookie generateTokenCookie(String cookieName, String tokenValue, String sameSite) { + private ResponseCookie generateTokenCookie(String cookieName, String tokenValue) { return ResponseCookie.from(cookieName, tokenValue) .path("/") .secure(true) - .sameSite(sameSite) + .sameSite(Cookie.SameSite.LAX.attributeValue()) + .domain(ROOT_DOMAIN) .httpOnly(true) .build(); } - private String determineSameSitePolicy() { - if (environmentUtil.isProdProfile()) { - return Cookie.SameSite.LAX.attributeValue(); - } - return Cookie.SameSite.NONE.attributeValue(); - } - public Optional findCookie(HttpServletRequest request, String cookieName) { return Arrays.stream(request.getCookies()) .filter(cookie -> cookie.getName().equals(cookieName)) From 8e7c7d990171bebf0710be0d6c7b14ac78ead9f4 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Wed, 7 Aug 2024 21:27:48 +0900 Subject: [PATCH 15/22] =?UTF-8?q?fix:=20=EB=AA=A8=EC=A7=91=20=EA=B8=B0?= =?UTF-8?q?=EA=B0=84=20=EC=8B=9C=EC=9E=91=20=ED=9B=84=EC=97=90=EB=8F=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#583)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/AdminRecruitmentController.java | 8 ++++---- .../application/AdminRecruitmentService.java | 7 ++++--- ...ava => RecruitmentRoundCreateRequest.java} | 2 +- .../RecruitmentRoundUpdateRequest.java | 20 +++++++++++++++++++ .../AdminRecruitmentServiceTest.java | 9 +++++---- 5 files changed, 34 insertions(+), 12 deletions(-) rename src/main/java/com/gdschongik/gdsc/domain/recruitment/dto/request/{RecruitmentRoundCreateUpdateRequest.java => RecruitmentRoundCreateRequest.java} (95%) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/recruitment/dto/request/RecruitmentRoundUpdateRequest.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/recruitment/api/AdminRecruitmentController.java b/src/main/java/com/gdschongik/gdsc/domain/recruitment/api/AdminRecruitmentController.java index 90ee88fa5..b77495e70 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/recruitment/api/AdminRecruitmentController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/recruitment/api/AdminRecruitmentController.java @@ -2,7 +2,8 @@ import com.gdschongik.gdsc.domain.recruitment.application.AdminRecruitmentService; import com.gdschongik.gdsc.domain.recruitment.dto.request.RecruitmentCreateRequest; -import com.gdschongik.gdsc.domain.recruitment.dto.request.RecruitmentRoundCreateUpdateRequest; +import com.gdschongik.gdsc.domain.recruitment.dto.request.RecruitmentRoundCreateRequest; +import com.gdschongik.gdsc.domain.recruitment.dto.request.RecruitmentRoundUpdateRequest; import com.gdschongik.gdsc.domain.recruitment.dto.response.AdminRecruitmentResponse; import com.gdschongik.gdsc.domain.recruitment.dto.response.AdminRecruitmentRoundResponse; import io.swagger.v3.oas.annotations.Operation; @@ -50,8 +51,7 @@ public ResponseEntity> getAllRecruitmentRoun @Operation(summary = "모집회차 생성", description = "새로운 모집회차를 생성합니다. 모집기간은 학기 시작일로부터 2주 이내입니다.") @PostMapping("/rounds") - public ResponseEntity createRecruitmentRound( - @Valid @RequestBody RecruitmentRoundCreateUpdateRequest request) { + public ResponseEntity createRecruitmentRound(@Valid @RequestBody RecruitmentRoundCreateRequest request) { adminRecruitmentService.createRecruitmentRound(request); return ResponseEntity.ok().build(); } @@ -59,7 +59,7 @@ public ResponseEntity createRecruitmentRound( @Operation(summary = "모집회차 수정", description = "기존 모집회차를 수정합니다. 학년도와 학기는 수정 대상이 아닙니다.") @PutMapping("/rounds/{recruitmentRoundId}") public ResponseEntity updateRecruitmentRound( - @PathVariable Long recruitmentRoundId, @Valid @RequestBody RecruitmentRoundCreateUpdateRequest request) { + @PathVariable Long recruitmentRoundId, @Valid @RequestBody RecruitmentRoundUpdateRequest request) { adminRecruitmentService.updateRecruitmentRound(recruitmentRoundId, request); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/recruitment/application/AdminRecruitmentService.java b/src/main/java/com/gdschongik/gdsc/domain/recruitment/application/AdminRecruitmentService.java index e69f8b104..23bab5cd0 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/recruitment/application/AdminRecruitmentService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/recruitment/application/AdminRecruitmentService.java @@ -11,7 +11,8 @@ import com.gdschongik.gdsc.domain.recruitment.domain.RecruitmentValidator; import com.gdschongik.gdsc.domain.recruitment.domain.vo.Period; import com.gdschongik.gdsc.domain.recruitment.dto.request.RecruitmentCreateRequest; -import com.gdschongik.gdsc.domain.recruitment.dto.request.RecruitmentRoundCreateUpdateRequest; +import com.gdschongik.gdsc.domain.recruitment.dto.request.RecruitmentRoundCreateRequest; +import com.gdschongik.gdsc.domain.recruitment.dto.request.RecruitmentRoundUpdateRequest; import com.gdschongik.gdsc.domain.recruitment.dto.response.AdminRecruitmentResponse; import com.gdschongik.gdsc.domain.recruitment.dto.response.AdminRecruitmentRoundResponse; import com.gdschongik.gdsc.global.exception.CustomException; @@ -63,7 +64,7 @@ public List getAllRecruitmentRounds() { } @Transactional - public void createRecruitmentRound(RecruitmentRoundCreateUpdateRequest request) { + public void createRecruitmentRound(RecruitmentRoundCreateRequest request) { Recruitment recruitment = recruitmentRepository .findByAcademicYearAndSemesterType(request.academicYear(), request.semesterType()) .orElseThrow(() -> new CustomException(RECRUITMENT_NOT_FOUND)); @@ -87,7 +88,7 @@ public void createRecruitmentRound(RecruitmentRoundCreateUpdateRequest request) } @Transactional - public void updateRecruitmentRound(Long recruitmentRoundId, RecruitmentRoundCreateUpdateRequest request) { + public void updateRecruitmentRound(Long recruitmentRoundId, RecruitmentRoundUpdateRequest request) { List recruitmentRounds = recruitmentRoundRepository.findAllByAcademicYearAndSemesterType( request.academicYear(), request.semesterType()); diff --git a/src/main/java/com/gdschongik/gdsc/domain/recruitment/dto/request/RecruitmentRoundCreateUpdateRequest.java b/src/main/java/com/gdschongik/gdsc/domain/recruitment/dto/request/RecruitmentRoundCreateRequest.java similarity index 95% rename from src/main/java/com/gdschongik/gdsc/domain/recruitment/dto/request/RecruitmentRoundCreateUpdateRequest.java rename to src/main/java/com/gdschongik/gdsc/domain/recruitment/dto/request/RecruitmentRoundCreateRequest.java index 80b377fdc..c19ae5fb7 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/recruitment/dto/request/RecruitmentRoundCreateUpdateRequest.java +++ b/src/main/java/com/gdschongik/gdsc/domain/recruitment/dto/request/RecruitmentRoundCreateRequest.java @@ -10,7 +10,7 @@ import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; -public record RecruitmentRoundCreateUpdateRequest( +public record RecruitmentRoundCreateRequest( @NotNull(message = "학년도는 null이 될 수 없습니다.") @Schema(description = "학년도", pattern = ACADEMIC_YEAR) Integer academicYear, @NotNull(message = "학기는 null이 될 수 없습니다.") @Schema(description = "학기") SemesterType semesterType, diff --git a/src/main/java/com/gdschongik/gdsc/domain/recruitment/dto/request/RecruitmentRoundUpdateRequest.java b/src/main/java/com/gdschongik/gdsc/domain/recruitment/dto/request/RecruitmentRoundUpdateRequest.java new file mode 100644 index 000000000..72f1cf5f6 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/recruitment/dto/request/RecruitmentRoundUpdateRequest.java @@ -0,0 +1,20 @@ +package com.gdschongik.gdsc.domain.recruitment.dto.request; + +import static com.gdschongik.gdsc.global.common.constant.RegexConstant.*; + +import com.gdschongik.gdsc.domain.common.model.SemesterType; +import com.gdschongik.gdsc.domain.recruitment.domain.RoundType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; + +public record RecruitmentRoundUpdateRequest( + @NotNull(message = "학년도는 null이 될 수 없습니다.") @Schema(description = "학년도", pattern = ACADEMIC_YEAR) + Integer academicYear, + @NotNull(message = "학기는 null이 될 수 없습니다.") @Schema(description = "학기") SemesterType semesterType, + @NotBlank @Schema(description = "이름") String name, + @Schema(description = "모집기간 시작일", pattern = DATETIME) LocalDateTime startDate, + @Future @Schema(description = "모집기간 종료일", pattern = DATETIME) LocalDateTime endDate, + @NotNull(message = "모집 차수는 null이 될 수 없습니다.") @Schema(description = "모집 차수") RoundType roundType) {} diff --git a/src/test/java/com/gdschongik/gdsc/domain/recruitment/application/AdminRecruitmentServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/recruitment/application/AdminRecruitmentServiceTest.java index 4f3e2172b..ca1ee1fbc 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/recruitment/application/AdminRecruitmentServiceTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/recruitment/application/AdminRecruitmentServiceTest.java @@ -11,7 +11,8 @@ import com.gdschongik.gdsc.domain.recruitment.domain.RecruitmentRound; import com.gdschongik.gdsc.domain.recruitment.domain.vo.Period; import com.gdschongik.gdsc.domain.recruitment.dto.request.RecruitmentCreateRequest; -import com.gdschongik.gdsc.domain.recruitment.dto.request.RecruitmentRoundCreateUpdateRequest; +import com.gdschongik.gdsc.domain.recruitment.dto.request.RecruitmentRoundCreateRequest; +import com.gdschongik.gdsc.domain.recruitment.dto.request.RecruitmentRoundUpdateRequest; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.helper.IntegrationTest; import java.time.LocalDateTime; @@ -53,7 +54,7 @@ class 모집회차_생성시 { @Test void 학년도와_학기가_일치하는_리쿠르팅이_존재하지_않는다면_실패한다() { // given - RecruitmentRoundCreateUpdateRequest request = new RecruitmentRoundCreateUpdateRequest( + RecruitmentRoundCreateRequest request = new RecruitmentRoundCreateRequest( ACADEMIC_YEAR, SEMESTER_TYPE, RECRUITMENT_ROUND_NAME, START_DATE, END_DATE, ROUND_TYPE); // when & then @@ -77,7 +78,7 @@ class 모집회차_수정시 { RECRUITMENT_ROUND_NAME, now.plusDays(1), now.plusDays(2), recruitment, ROUND_TYPE); recruitmentRoundRepository.save(recruitmentRound); - RecruitmentRoundCreateUpdateRequest request = new RecruitmentRoundCreateUpdateRequest( + RecruitmentRoundUpdateRequest request = new RecruitmentRoundUpdateRequest( ACADEMIC_YEAR, SEMESTER_TYPE, "수정된 모집회차 이름", now.plusDays(2), now.plusDays(3), ROUND_TYPE); // when @@ -95,7 +96,7 @@ class 모집회차_수정시 { @Test void 모집회차가_존재하지_않는다면_실패한다() { // given - RecruitmentRoundCreateUpdateRequest request = new RecruitmentRoundCreateUpdateRequest( + RecruitmentRoundUpdateRequest request = new RecruitmentRoundUpdateRequest( ACADEMIC_YEAR, SEMESTER_TYPE, RECRUITMENT_ROUND_NAME, START_DATE, END_DATE, ROUND_TYPE); // when & then From 388c25b662afcb9b224a918dc3c574561d36e401 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Wed, 7 Aug 2024 22:04:01 +0900 Subject: [PATCH 16/22] =?UTF-8?q?fix:=20=EB=8B=A4=EB=A5=B8=20=EC=8A=AC?= =?UTF-8?q?=EB=9E=98=EC=8B=9C=20=EC=BB=A4=EB=A7=A8=EB=93=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=8B=9C=20=ED=98=B8=EC=B6=9C=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#587)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/listener/DiscordIdBatchCommandListener.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/discord/application/listener/DiscordIdBatchCommandListener.java b/src/main/java/com/gdschongik/gdsc/domain/discord/application/listener/DiscordIdBatchCommandListener.java index ef67bdc9a..9e0c4e32f 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/discord/application/listener/DiscordIdBatchCommandListener.java +++ b/src/main/java/com/gdschongik/gdsc/domain/discord/application/listener/DiscordIdBatchCommandListener.java @@ -1,5 +1,7 @@ package com.gdschongik.gdsc.domain.discord.application.listener; +import static com.gdschongik.gdsc.global.common.constant.DiscordConstant.*; + import com.gdschongik.gdsc.domain.discord.application.handler.DiscordIdBatchCommandHandler; import lombok.RequiredArgsConstructor; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; @@ -15,6 +17,8 @@ public class DiscordIdBatchCommandListener extends ListenerAdapter { private final DiscordIdBatchCommandHandler discordIdBatchCommandHandler; public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) { - discordIdBatchCommandHandler.delegate(event); + if (event.getName().equals(COMMAND_NAME_BATCH_DISCORD_ID)) { + discordIdBatchCommandHandler.delegate(event); + } } } From 4c17848bf81e8853f793a4d39b23341f1d7e9950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=ED=95=9C=EB=B9=84?= <99820610+AlmondBreez3@users.noreply.github.com> Date: Wed, 7 Aug 2024 22:29:57 +0900 Subject: [PATCH 17/22] =?UTF-8?q?feat:=20=EA=B3=BC=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=98=EA=B8=B0=20API=20=EA=B5=AC=ED=98=84=20(#581)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 스터디 과제 수정 * feat: develop과 버전 맞추기 * fix: 메소드명 맞게 수정하기 * feat: 과제 수정 조건 주석 수정 * feat: request통일 반영 * feat: 테스트 코드 작성 * feat: 오타 수정 및 에러 코드 추가 * feat: 테스트 메서드명 명료하게 수정하기 * fix: 테스트 메소드명 오타 수정 * feat: 테스트 메소드명 띄어쓰기 수정, 성공 응답 200으로 통일 --- .../api/MentorStudyDetailController.java | 12 ++- .../application/MentorStudyDetailService.java | 23 ++++- .../gdsc/domain/study/domain/StudyDetail.java | 6 +- .../study/domain/StudyDetailValidator.java | 25 +++++- .../domain/study/domain/vo/Assignment.java | 2 +- ...ava => AssignmentCreateUpdateRequest.java} | 2 +- .../gdsc/global/exception/ErrorCode.java | 2 + .../domain/StudyDetailValidatorTest.java | 84 +++++++++++++++++-- 8 files changed, 140 insertions(+), 16 deletions(-) rename src/main/java/com/gdschongik/gdsc/domain/study/dto/request/{AssignmentCreateRequest.java => AssignmentCreateUpdateRequest.java} (91%) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyDetailController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyDetailController.java index 7e8c3fbee..e74145482 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyDetailController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyDetailController.java @@ -1,7 +1,7 @@ package com.gdschongik.gdsc.domain.study.api; import com.gdschongik.gdsc.domain.study.application.MentorStudyDetailService; -import com.gdschongik.gdsc.domain.study.dto.request.AssignmentCreateRequest; +import com.gdschongik.gdsc.domain.study.dto.request.AssignmentCreateUpdateRequest; import com.gdschongik.gdsc.domain.study.dto.response.AssignmentResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -26,10 +26,18 @@ public class MentorStudyDetailController { private final MentorStudyDetailService mentorStudyDetailService; + @Operation(summary = "스터디 과제 수정", description = "멘토만 과제를 수정할 수 있습니다.") + @PatchMapping("/{studyDetailId}/assignments") + public ResponseEntity updateStudyAssignment( + @PathVariable Long studyDetailId, @Valid @RequestBody AssignmentCreateUpdateRequest request) { + mentorStudyDetailService.updateStudyAssignment(studyDetailId, request); + return ResponseEntity.ok().build(); + } + @Operation(summary = "스터디 과제 개설", description = "멘토만 과제를 개설할 수 있습니다.") @PutMapping("/{studyDetailId}/assignments") public ResponseEntity publishStudyAssignment( - @PathVariable Long studyDetailId, @Valid @RequestBody AssignmentCreateRequest request) { + @PathVariable Long studyDetailId, @Valid @RequestBody AssignmentCreateUpdateRequest request) { mentorStudyDetailService.publishStudyAssignment(studyDetailId, request); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java index 88f561e9a..a6c3c5485 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyDetailService.java @@ -6,7 +6,7 @@ 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.request.AssignmentCreateRequest; +import com.gdschongik.gdsc.domain.study.dto.request.AssignmentCreateUpdateRequest; import com.gdschongik.gdsc.domain.study.dto.response.AssignmentResponse; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.util.MemberUtil; @@ -51,11 +51,11 @@ public void cancelStudyAssignment(Long studyDetailId) { studyDetail.cancelAssignment(); studyDetailRepository.save(studyDetail); - log.info("[StudyMentorService] 과제 휴강 처리: studyDetailId={}", studyDetail.getId()); + log.info("[MentorStudyDetailService] 과제 휴강 처리: studyDetailId={}", studyDetail.getId()); } @Transactional - public void publishStudyAssignment(Long studyDetailId, AssignmentCreateRequest request) { + public void publishStudyAssignment(Long studyDetailId, AssignmentCreateUpdateRequest request) { Member currentMember = memberUtil.getCurrentMember(); StudyDetail studyDetail = studyDetailRepository .findById(studyDetailId) @@ -66,6 +66,21 @@ public void publishStudyAssignment(Long studyDetailId, AssignmentCreateRequest r studyDetail.publishAssignment(request.title(), request.deadLine(), request.descriptionNotionLink()); studyDetailRepository.save(studyDetail); - log.info("[StudyMentorService] 과제 개설 완료: studyDetailId={}", studyDetailId); + log.info("[MentorStudyDetailService] 과제 개설 완료: studyDetailId={}", studyDetailId); + } + + @Transactional + public void updateStudyAssignment(Long studyDetailId, AssignmentCreateUpdateRequest request) { + Member currentMember = memberUtil.getCurrentMember(); + StudyDetail studyDetail = studyDetailRepository + .findById(studyDetailId) + .orElseThrow(() -> new CustomException(STUDY_DETAIL_NOT_FOUND)); + + studyDetailValidator.validateUpdateStudyAssignment(currentMember, studyDetail, request); + + studyDetail.updateAssignment(request.title(), request.deadLine(), request.descriptionNotionLink()); + studyDetailRepository.save(studyDetail); + + log.info("[MentorStudyDetailService] 과제 수정 완료: studyDetailId={}", studyDetailId); } } 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 323cb62e0..6057fa5ea 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 @@ -76,6 +76,10 @@ public void cancelAssignment() { } public void publishAssignment(String title, LocalDateTime deadLine, String descriptionNotionLink) { - assignment = Assignment.publishAssignment(title, deadLine, descriptionNotionLink); + assignment = Assignment.generateAssignment(title, deadLine, descriptionNotionLink); + } + + public void updateAssignment(String title, LocalDateTime deadLine, String descriptionNotionLink) { + assignment = Assignment.generateAssignment(title, deadLine, descriptionNotionLink); } } 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 index 0e1986e54..59ab1afb8 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidator.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidator.java @@ -3,7 +3,7 @@ import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import com.gdschongik.gdsc.domain.member.domain.Member; -import com.gdschongik.gdsc.domain.study.dto.request.AssignmentCreateRequest; +import com.gdschongik.gdsc.domain.study.dto.request.AssignmentCreateUpdateRequest; import com.gdschongik.gdsc.global.annotation.DomainService; import com.gdschongik.gdsc.global.exception.CustomException; import java.time.LocalDateTime; @@ -16,7 +16,7 @@ public void validateCancelStudyAssignment(Member member, StudyDetail studyDetail } public void validatePublishStudyAssignment( - Member member, StudyDetail studyDetail, AssignmentCreateRequest request) { + Member member, StudyDetail studyDetail, AssignmentCreateUpdateRequest request) { validateStudyMentorAuthorization(member, studyDetail); validateDeadLine(request.deadLine()); } @@ -33,4 +33,25 @@ private void validateDeadLine(LocalDateTime deadline) { throw new CustomException(ASSIGNMENT_DEADLINE_INVALID); } } + + public void validateUpdateStudyAssignment( + Member currentMember, StudyDetail studyDetail, AssignmentCreateUpdateRequest request) { + + validateStudyMentorAuthorization(currentMember, studyDetail); + validateUpdateDeadline(LocalDateTime.now(), studyDetail.getAssignment().getDeadline(), request.deadLine()); + } + + /** + * 과제 마감기한이 수정 시점보다 앞서거나 수정할 마감기한이 기존 마감기한보다 앞서면 안된다. + */ + private void validateUpdateDeadline( + LocalDateTime currentTime, LocalDateTime deadLine, LocalDateTime updateDeadLine) { + if (currentTime.isAfter(deadLine)) { + throw new CustomException(STUDY_DETAIL_ASSIGNMENT_INVALID_DEADLINE); + } + + if (deadLine.isAfter(updateDeadLine)) { + throw new CustomException(STUDY_DETAIL_ASSIGNMENT_INVALID_UPDATE_DEADLINE); + } + } } 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 09ea9f4fd..ea480bed8 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 @@ -57,7 +57,7 @@ public static Assignment cancelAssignment() { return Assignment.builder().status(CANCELLED).build(); } - public static Assignment publishAssignment(String title, LocalDateTime deadline, String descriptionLink) { + public static Assignment generateAssignment(String title, LocalDateTime deadline, String descriptionLink) { return Assignment.builder() .title(title) .deadline(deadline) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/AssignmentCreateRequest.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/AssignmentCreateUpdateRequest.java similarity index 91% rename from src/main/java/com/gdschongik/gdsc/domain/study/dto/request/AssignmentCreateRequest.java rename to src/main/java/com/gdschongik/gdsc/domain/study/dto/request/AssignmentCreateUpdateRequest.java index 295720ce4..23c82e948 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/AssignmentCreateRequest.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/AssignmentCreateUpdateRequest.java @@ -5,7 +5,7 @@ import jakarta.validation.constraints.NotBlank; import java.time.LocalDateTime; -public record AssignmentCreateRequest( +public record AssignmentCreateUpdateRequest( @NotBlank @Schema(description = "과제 제목") String title, @NotBlank @Schema(description = "과제 명세 노션 링크") String descriptionNotionLink, @Future @Schema(description = "과제 마감일") LocalDateTime deadLine) {} 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 5478c2499..17686780c 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -110,6 +110,8 @@ public enum ErrorCode { // StudyDetail STUDY_DETAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 상세 정보입니다."), STUDY_DETAIL_UPDATE_RESTRICTED_TO_MENTOR(HttpStatus.CONFLICT, "해당 스터디의 멘토만 수정할 수 있습니다."), + STUDY_DETAIL_ASSIGNMENT_INVALID_DEADLINE(HttpStatus.CONFLICT, "마감기한이 지난 과제의 마감기한을 수정할 수 없습니다"), + STUDY_DETAIL_ASSIGNMENT_INVALID_UPDATE_DEADLINE(HttpStatus.CONFLICT, "수정하려고 하는 과제의 마감기한은 기존의 마감기한보다 빠르면 안됩니다."), // StudyHistory STUDY_HISTORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 수강 기록입니다."), 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 index 789b236c8..0c536814f 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidatorTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyDetailValidatorTest.java @@ -6,7 +6,7 @@ import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.recruitment.domain.vo.Period; -import com.gdschongik.gdsc.domain.study.dto.request.AssignmentCreateRequest; +import com.gdschongik.gdsc.domain.study.dto.request.AssignmentCreateUpdateRequest; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.helper.FixtureHelper; import java.time.LocalDateTime; @@ -54,8 +54,8 @@ class 과제_개설시 { Period.createPeriod(now.minusDays(5), now)); StudyDetail studyDetail = fixtureHelper.createStudyDetail(study, now, now.plusDays(7)); Member anotherMember = fixtureHelper.createAssociateMember(2L); - AssignmentCreateRequest request = - new AssignmentCreateRequest(ASSIGNMENT_TITLE, DESCRIPTION_LINK, now.plusDays(2)); + AssignmentCreateUpdateRequest request = + new AssignmentCreateUpdateRequest(ASSIGNMENT_TITLE, DESCRIPTION_LINK, now.plusDays(2)); // when & then assertThatThrownBy(() -> @@ -74,8 +74,8 @@ class 과제_개설시 { Period.createPeriod(now.plusDays(5), now.plusDays(10)), Period.createPeriod(now.minusDays(5), now)); StudyDetail studyDetail = fixtureHelper.createStudyDetail(study, now, now.plusDays(7)); - AssignmentCreateRequest request = - new AssignmentCreateRequest(ASSIGNMENT_TITLE, DESCRIPTION_LINK, now.minusDays(2)); + AssignmentCreateUpdateRequest request = + new AssignmentCreateUpdateRequest(ASSIGNMENT_TITLE, DESCRIPTION_LINK, now.minusDays(2)); // when & then assertThatThrownBy(() -> studyDetailValidator.validatePublishStudyAssignment(mentor, studyDetail, request)) @@ -83,4 +83,78 @@ class 과제_개설시 { .hasMessage(ASSIGNMENT_DEADLINE_INVALID.getMessage()); } } + + @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(10)); + Member anotherMember = fixtureHelper.createAssociateMember(2L); + studyDetail.publishAssignment(ASSIGNMENT_TITLE, now.plusDays(2), DESCRIPTION_LINK); + + AssignmentCreateUpdateRequest request = + new AssignmentCreateUpdateRequest(ASSIGNMENT_TITLE, DESCRIPTION_LINK, now.plusDays(3)); + + // when & then + assertThatThrownBy(() -> + studyDetailValidator.validateUpdateStudyAssignment(anotherMember, studyDetail, request)) + .isInstanceOf(CustomException.class) + .hasMessage(STUDY_DETAIL_UPDATE_RESTRICTED_TO_MENTOR.getMessage()); + } + + @Test + void 기존마감기한이_수정시점보다_앞서면_실패한다() { + // given + Member mentor = fixtureHelper.createAssociateMember(1L); + LocalDateTime assignmentCreatedDate = LocalDateTime.now().minusDays(1); + Study study = fixtureHelper.createStudy( + mentor, + Period.createPeriod(assignmentCreatedDate.plusDays(5), assignmentCreatedDate.plusDays(10)), + Period.createPeriod(assignmentCreatedDate.minusDays(5), assignmentCreatedDate)); + StudyDetail studyDetail = + fixtureHelper.createStudyDetail(study, assignmentCreatedDate, assignmentCreatedDate.plusDays(1)); + studyDetail.publishAssignment(ASSIGNMENT_TITLE, assignmentCreatedDate.plusDays(1), DESCRIPTION_LINK); + + LocalDateTime assignmentUpdateDate = assignmentCreatedDate.plusDays(3); + AssignmentCreateUpdateRequest request = + new AssignmentCreateUpdateRequest(ASSIGNMENT_TITLE, DESCRIPTION_LINK, assignmentUpdateDate); + + // when & then + assertThatThrownBy(() -> studyDetailValidator.validateUpdateStudyAssignment(mentor, studyDetail, request)) + .isInstanceOf(CustomException.class) + .hasMessage(STUDY_DETAIL_ASSIGNMENT_INVALID_DEADLINE.getMessage()); + } + + @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(10)); + LocalDateTime savedDeadLine = now.minusDays(1); + studyDetail.publishAssignment(ASSIGNMENT_TITLE, savedDeadLine, DESCRIPTION_LINK); + + LocalDateTime updatedDeadLine = savedDeadLine.minusDays(4); + AssignmentCreateUpdateRequest request = + new AssignmentCreateUpdateRequest(ASSIGNMENT_TITLE, DESCRIPTION_LINK, updatedDeadLine); + + // when & then + assertThatThrownBy(() -> studyDetailValidator.validateUpdateStudyAssignment(mentor, studyDetail, request)) + .isInstanceOf(CustomException.class) + .hasMessage(STUDY_DETAIL_ASSIGNMENT_INVALID_DEADLINE.getMessage()); + } + } } From 23e082a458790d9a0f5f8eff70e056f0a917ea6f Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Wed, 7 Aug 2024 22:37:44 +0900 Subject: [PATCH 18/22] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=ED=9B=84=20=EB=B3=84=EB=8F=84=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EB=A1=9C=20=EB=A6=AC=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EB=A0=89=ED=8A=B8=20=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#590)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 미사용 import 제거 * feat: 지정된 URL로 리다이렉트 하도록 변경 --- .../global/common/constant/SecurityConstant.java | 1 + .../gdsc/global/security/CustomSuccessHandler.java | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java index 1df68bb01..15e402d9d 100644 --- a/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java @@ -7,6 +7,7 @@ public class SecurityConstant { public static final String TOKEN_STUDY_ROLE_NAME = "studyRole"; public static final String GITHUB_NAME_ATTR_KEY = "id"; public static final String ACCESS_TOKEN_HEADER_PREFIX = "Bearer "; + public static final String OAUTH_REDIRECT_PATH_SEGMENT = "/social-login/redirect"; private SecurityConstant() {} } diff --git a/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java b/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java index f263c3dc5..08c1d687d 100644 --- a/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java +++ b/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java @@ -1,8 +1,6 @@ package com.gdschongik.gdsc.global.security; import static com.gdschongik.gdsc.global.common.constant.SecurityConstant.*; -import static com.gdschongik.gdsc.global.common.constant.UrlConstant.*; -import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import com.gdschongik.gdsc.domain.auth.application.JwtService; import com.gdschongik.gdsc.domain.auth.dto.AccessTokenDto; @@ -15,6 +13,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.web.util.UriComponentsBuilder; @Slf4j public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { @@ -42,4 +41,13 @@ public void onAuthenticationSuccess( super.onAuthenticationSuccess(request, response, authentication); } + + @Override + protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) { + String baseUrl = super.determineTargetUrl(request, response); + return UriComponentsBuilder.fromHttpUrl(baseUrl) + .path(OAUTH_REDIRECT_PATH_SEGMENT) + .build() + .toUriString(); + } } From 407b3809f0e68115ef30ecefaa89fdc293747dd3 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Wed, 7 Aug 2024 23:02:56 +0900 Subject: [PATCH 19/22] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20API=20=EA=B5=AC=ED=98=84=20(#593)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 쿠키 찾기 및 삭제 로직 제거 * feat: 쿠키 삭제 로직 변경 * feat: 쿠키 이름 직접 참조 가능하도록 수정 * feat: 로그아웃 API 구현 --- .../gdsc/domain/auth/api/AuthController.java | 33 +++++++++++++++++++ .../global/common/constant/JwtConstant.java | 5 ++- .../gdsc/global/util/CookieUtil.java | 12 +------ 3 files changed, 38 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/auth/api/AuthController.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/auth/api/AuthController.java b/src/main/java/com/gdschongik/gdsc/domain/auth/api/AuthController.java new file mode 100644 index 000000000..f62e96771 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/auth/api/AuthController.java @@ -0,0 +1,33 @@ +package com.gdschongik.gdsc.domain.auth.api; + +import static com.gdschongik.gdsc.global.common.constant.JwtConstant.Constants.*; + +import com.gdschongik.gdsc.global.util.CookieUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.Cookie; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Auth", description = "인증 API입니다.") +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthController { + + private final CookieUtil cookieUtil; + + @Operation(summary = "로그아웃", description = "현재 엑세스 토큰 및 리프레시 토큰 쿠키를 만료시킵니다.") + @GetMapping("/logout") + public ResponseEntity logout( + @CookieValue(ACCESS_TOKEN_COOKIE_NAME) Cookie accessToken, + @CookieValue(REFRESH_TOKEN_COOKIE_NAME) Cookie refreshToken) { + cookieUtil.deleteCookie(accessToken); + cookieUtil.deleteCookie(refreshToken); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/JwtConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/JwtConstant.java index 49599aa37..2b3bf7cb7 100644 --- a/src/main/java/com/gdschongik/gdsc/global/common/constant/JwtConstant.java +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/JwtConstant.java @@ -1,7 +1,9 @@ package com.gdschongik.gdsc.global.common.constant; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @AllArgsConstructor @@ -12,7 +14,8 @@ public enum JwtConstant { private final String cookieName; - private static class Constants { + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class Constants { public static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken"; public static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; public static final String EMAIL_VERIFICATION_TOKEN_NAME = "emailVerificationToken"; diff --git a/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java b/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java index c600ab51b..3786b3f64 100644 --- a/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java +++ b/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java @@ -3,10 +3,7 @@ import static com.gdschongik.gdsc.global.common.constant.UrlConstant.*; import com.gdschongik.gdsc.global.common.constant.JwtConstant; -import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.util.Arrays; -import java.util.Optional; import org.springframework.boot.web.server.Cookie; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; @@ -35,16 +32,9 @@ private ResponseCookie generateTokenCookie(String cookieName, String tokenValue) .build(); } - public Optional findCookie(HttpServletRequest request, String cookieName) { - return Arrays.stream(request.getCookies()) - .filter(cookie -> cookie.getName().equals(cookieName)) - .findFirst(); - } - - public void deleteCookie(HttpServletResponse response, jakarta.servlet.http.Cookie cookie) { + public void deleteCookie(jakarta.servlet.http.Cookie cookie) { cookie.setPath("/"); cookie.setValue(""); cookie.setMaxAge(0); - response.addCookie(cookie); } } From 317ac6d0ac301b7dffdce144573b2938ed4275a7 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Sun, 11 Aug 2024 15:46:55 +0900 Subject: [PATCH 20/22] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20API=EA=B0=80=20=EC=BF=A0=ED=82=A4=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=ED=95=98=EC=A7=80=20=EB=AA=BB=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95=20(#597)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: 수정된 쿠키를 응답에 담아보내도록 수정 --- .../gdschongik/gdsc/domain/auth/api/AuthController.java | 8 +++++--- .../java/com/gdschongik/gdsc/global/util/CookieUtil.java | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/auth/api/AuthController.java b/src/main/java/com/gdschongik/gdsc/domain/auth/api/AuthController.java index f62e96771..e50fbb60e 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/auth/api/AuthController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/auth/api/AuthController.java @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CookieValue; @@ -25,9 +26,10 @@ public class AuthController { @GetMapping("/logout") public ResponseEntity logout( @CookieValue(ACCESS_TOKEN_COOKIE_NAME) Cookie accessToken, - @CookieValue(REFRESH_TOKEN_COOKIE_NAME) Cookie refreshToken) { - cookieUtil.deleteCookie(accessToken); - cookieUtil.deleteCookie(refreshToken); + @CookieValue(REFRESH_TOKEN_COOKIE_NAME) Cookie refreshToken, + HttpServletResponse response) { + cookieUtil.deleteCookie(accessToken, response); + cookieUtil.deleteCookie(refreshToken, response); return ResponseEntity.ok().build(); } } diff --git a/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java b/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java index 3786b3f64..2c6d5fb0f 100644 --- a/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java +++ b/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java @@ -32,9 +32,10 @@ private ResponseCookie generateTokenCookie(String cookieName, String tokenValue) .build(); } - public void deleteCookie(jakarta.servlet.http.Cookie cookie) { + public void deleteCookie(jakarta.servlet.http.Cookie cookie, HttpServletResponse response) { cookie.setPath("/"); cookie.setValue(""); cookie.setMaxAge(0); + response.addCookie(cookie); } } From ff65ee9f12d3450b8bde4e76aa613c9641b3125e Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Sun, 11 Aug 2024 21:04:11 +0900 Subject: [PATCH 21/22] =?UTF-8?q?refactor:=20=EC=8A=B9=EA=B8=89=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EC=83=81=ED=83=9C=20enum=20=EA=B0=92=EC=9D=84=20UN?= =?UTF-8?q?SATISFIED=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 승급조건 enum을 PENDING에서 UNSATISFIED로 변경 * refactor: 재학생 이메일 인증상태의 enum값을 UNSATISFIED로 변경 * refactor: 변경된 값 메서드명에도 적용 --- .../domain/common/model/RequirementStatus.java | 2 +- .../domain/EmailVerificationStatusService.java | 2 +- .../domain/member/api/TestMemberController.java | 2 +- .../member/application/AdminMemberService.java | 5 +---- .../member/domain/AssociateRequirement.java | 16 ++++++++-------- .../member/dto/UnivVerificationStatus.java | 2 +- .../domain/membership/domain/Membership.java | 2 +- .../membership/domain/RegularRequirement.java | 2 +- .../gdsc/domain/member/domain/MemberTest.java | 10 +++++----- 9 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/common/model/RequirementStatus.java b/src/main/java/com/gdschongik/gdsc/domain/common/model/RequirementStatus.java index a2b2fe48c..c29522376 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/common/model/RequirementStatus.java +++ b/src/main/java/com/gdschongik/gdsc/domain/common/model/RequirementStatus.java @@ -6,7 +6,7 @@ @Getter @AllArgsConstructor public enum RequirementStatus { - PENDING("PENDING"), + UNSATISFIED("UNSATISFIED"), SATISFIED("SATISFIED"); private final String value; diff --git a/src/main/java/com/gdschongik/gdsc/domain/email/domain/EmailVerificationStatusService.java b/src/main/java/com/gdschongik/gdsc/domain/email/domain/EmailVerificationStatusService.java index 9767fbb7e..66fd9918a 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/email/domain/EmailVerificationStatusService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/email/domain/EmailVerificationStatusService.java @@ -15,7 +15,7 @@ public UnivVerificationStatus determineStatus( } else { return univEmailVerification.isPresent() ? UnivVerificationStatus.IN_PROGRESS - : UnivVerificationStatus.PENDING; + : UnivVerificationStatus.UNSATISFIED; } } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/api/TestMemberController.java b/src/main/java/com/gdschongik/gdsc/domain/member/api/TestMemberController.java index eaf3a9dc4..0c093d3e4 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/api/TestMemberController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/api/TestMemberController.java @@ -39,7 +39,7 @@ public ResponseEntity createTemporaryToken(@Valid @RequestB @Operation(summary = "게스트로 강등", description = "테스트용 API입니다. 현재 멤버 역할을 게스트로 강등시키기 위해 사용합니다.") @PatchMapping("/demotion") public ResponseEntity demoteToGuest() { - adminMemberService.demoteToGuestAndRegularRequirementToPending(); + adminMemberService.demoteToGuestAndRegularRequirementToUnsatisfied(); return ResponseEntity.ok().build(); } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java index 573f63c51..38a91e815 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java @@ -88,11 +88,8 @@ public void demoteAllRegularMembersToAssociate(MemberDemoteRequest request) { regularMembers.stream().map(Member::getId).toList()); } - /** - * 정회원 조건 PENDING으로 변경, 준회원 조건 PENDING으로 변경 - */ @Transactional - public void demoteToGuestAndRegularRequirementToPending() { + public void demoteToGuestAndRegularRequirementToUnsatisfied() { validateProfile(); Member member = memberUtil.getCurrentMember(); member.demoteToGuest(); diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/AssociateRequirement.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/AssociateRequirement.java index 772d08fa7..648a495a9 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/AssociateRequirement.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/AssociateRequirement.java @@ -46,10 +46,10 @@ private AssociateRequirement( public static AssociateRequirement createRequirement() { return AssociateRequirement.builder() - .univStatus(PENDING) - .discordStatus(PENDING) - .bevyStatus(PENDING) - .infoStatus(PENDING) + .univStatus(UNSATISFIED) + .discordStatus(UNSATISFIED) + .bevyStatus(UNSATISFIED) + .infoStatus(UNSATISFIED) .build(); } @@ -119,9 +119,9 @@ public void checkVerifiableUniv() { * 모든 준회원 조건을 강등합니다. */ public void demoteAssociateRequirement() { - bevyStatus = PENDING; - discordStatus = PENDING; - infoStatus = PENDING; - univStatus = PENDING; + bevyStatus = UNSATISFIED; + discordStatus = UNSATISFIED; + infoStatus = UNSATISFIED; + univStatus = UNSATISFIED; } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/UnivVerificationStatus.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/UnivVerificationStatus.java index 7152df0af..19674e9f9 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dto/UnivVerificationStatus.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/UnivVerificationStatus.java @@ -6,7 +6,7 @@ @Getter @AllArgsConstructor public enum UnivVerificationStatus { - PENDING("PENDING"), + UNSATISFIED("UNSATISFIED"), IN_PROGRESS("IN_PROGRESS"), SATISFIED("SATISFIED"); diff --git a/src/main/java/com/gdschongik/gdsc/domain/membership/domain/Membership.java b/src/main/java/com/gdschongik/gdsc/domain/membership/domain/Membership.java index 7b6836173..128836c8a 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/membership/domain/Membership.java +++ b/src/main/java/com/gdschongik/gdsc/domain/membership/domain/Membership.java @@ -78,7 +78,7 @@ public void verifyPaymentStatus() { public void revokePaymentStatus() { validatePaymentStatusRevocable(); - regularRequirement.updatePaymentStatus(PENDING); + regularRequirement.updatePaymentStatus(UNSATISFIED); registerEvent(new MembershipPaymentRevokedEvent(id)); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/membership/domain/RegularRequirement.java b/src/main/java/com/gdschongik/gdsc/domain/membership/domain/RegularRequirement.java index d522fcd30..9bc49ebeb 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/membership/domain/RegularRequirement.java +++ b/src/main/java/com/gdschongik/gdsc/domain/membership/domain/RegularRequirement.java @@ -29,7 +29,7 @@ private RegularRequirement(RequirementStatus paymentStatus) { public static RegularRequirement createUnsatisfiedRequirement() { return RegularRequirement.builder() - .paymentStatus(RequirementStatus.PENDING) + .paymentStatus(RequirementStatus.UNSATISFIED) .build(); } diff --git a/src/test/java/com/gdschongik/gdsc/domain/member/domain/MemberTest.java b/src/test/java/com/gdschongik/gdsc/domain/member/domain/MemberTest.java index 7fda0bdbe..5904d96df 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/member/domain/MemberTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/member/domain/MemberTest.java @@ -50,10 +50,10 @@ class 게스트_회원가입시 { AssociateRequirement requirement = member.getAssociateRequirement(); // then - assertThat(requirement.getUnivStatus()).isEqualTo(PENDING); - assertThat(requirement.getDiscordStatus()).isEqualTo(PENDING); - assertThat(requirement.getBevyStatus()).isEqualTo(PENDING); - assertThat(requirement.getInfoStatus()).isEqualTo(PENDING); + assertThat(requirement.getUnivStatus()).isEqualTo(UNSATISFIED); + assertThat(requirement.getDiscordStatus()).isEqualTo(UNSATISFIED); + assertThat(requirement.getBevyStatus()).isEqualTo(UNSATISFIED); + assertThat(requirement.getInfoStatus()).isEqualTo(UNSATISFIED); } } @@ -370,7 +370,7 @@ class 비회원으로_강등시 { AssociateRequirement::getInfoStatus, AssociateRequirement::getBevyStatus, AssociateRequirement::getUnivStatus) - .containsExactly(PENDING, PENDING, PENDING, PENDING); + .containsExactly(UNSATISFIED, UNSATISFIED, UNSATISFIED, UNSATISFIED); } } } From 94bff51ddec4abef71d375b1d1b442dc9505b5f3 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Sun, 11 Aug 2024 22:40:04 +0900 Subject: [PATCH 22/22] =?UTF-8?q?feat:=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20API=EC=97=90=20=EC=8B=A0=EA=B7=9C=20=EC=97=AD?= =?UTF-8?q?=ED=95=A0=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#604)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: MemberFullDto에 신규 역할 필드 추가 --- .../gdschongik/gdsc/domain/member/dto/MemberFullDto.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/MemberFullDto.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/MemberFullDto.java index abfc4b842..e638961e9 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dto/MemberFullDto.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/MemberFullDto.java @@ -3,7 +3,9 @@ import com.gdschongik.gdsc.domain.common.model.RequirementStatus; import com.gdschongik.gdsc.domain.member.domain.Department; import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.member.domain.MemberManageRole; import com.gdschongik.gdsc.domain.member.domain.MemberRole; +import com.gdschongik.gdsc.domain.member.domain.MemberStudyRole; import com.gdschongik.gdsc.global.util.formatter.PhoneFormatter; import io.swagger.v3.oas.annotations.media.Schema; import java.util.Optional; @@ -11,6 +13,8 @@ public record MemberFullDto( Long memberId, @Schema(description = "멤버 역할", implementation = MemberRole.class) MemberRole role, + @Schema(description = "멤버 관리자 역할", implementation = MemberManageRole.class) MemberManageRole manageRole, + @Schema(description = "멤버 스터디 역할", implementation = MemberStudyRole.class) MemberStudyRole studyRole, @Schema(description = "회원정보", implementation = MemberBasicInfoDto.class) MemberBasicInfoDto basicInfo, @Schema(description = "인증상태정보", implementation = MemberAssociateRequirementDto.class) MemberAssociateRequirementDto associateRequirement) { @@ -18,6 +22,8 @@ public static MemberFullDto of(Member member, UnivVerificationStatus univVerific return new MemberFullDto( member.getId(), member.getRole(), + member.getManageRole(), + member.getStudyRole(), MemberBasicInfoDto.from(member), MemberAssociateRequirementDto.of(member, univVerificationStatus)); }