From e2a8008342973ba6680ca8f8b237047e7d083a85 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Tue, 13 Aug 2024 12:11:10 +0900 Subject: [PATCH 01/40] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20CORS=20=EC=84=A4=EC=A0=95=20(#611?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 와우클래스 클라이언트 도메인 추가 * refactor: 상용 및 개발환경 클라이언트 URL 목록을 묶어서 등록 --- .../gdsc/global/common/constant/UrlConstant.java | 12 ++++++++++++ .../gdsc/global/config/WebSecurityConfig.java | 6 ++---- 2 files changed, 14 insertions(+), 4 deletions(-) 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 a4623c5d8..a2992c151 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,5 +1,7 @@ package com.gdschongik.gdsc.global.common.constant; +import java.util.List; + public class UrlConstant { private UrlConstant() {} @@ -7,14 +9,24 @@ private UrlConstant() {} // 클라이언트 URL public static final String PROD_CLIENT_ONBOARDING_URL = "https://onboarding.gdschongik.com"; public static final String PROD_CLIENT_ADMIN_URL = "https://admin.gdschongik.com"; + public static final String PROD_CLIENT_STUDY_URL = "https://study.gdschongik.com"; + public static final String PROD_CLIENT_STUDY_MENTOR_URL = "https://mentor.study.gdschongik.com"; public static final String DEV_CLIENT_ONBOARDING_URL = "https://dev-onboarding.gdschongik.com"; public static final String DEV_CLIENT_ADMIN_URL = "https://dev-admin.gdschongik.com"; + public static final String DEV_CLIENT_STUDY_URL = "https://dev-study.gdschongik.com"; + public static final String DEV_CLIENT_STUDY_MENTOR_URL = "https://dev-mentor.study.gdschongik.com"; public static final String LOCAL_REACT_CLIENT_URL = "http://localhost:3000"; public static final String LOCAL_REACT_CLIENT_SECURE_URL = "https://localhost:3000"; 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 PROD_CLIENT_URLS = List.of( + PROD_CLIENT_ONBOARDING_URL, PROD_CLIENT_ADMIN_URL, PROD_CLIENT_STUDY_URL, PROD_CLIENT_STUDY_MENTOR_URL); + + public static final List DEV_CLIENT_URLS = + List.of(DEV_CLIENT_ONBOARDING_URL, DEV_CLIENT_ADMIN_URL, DEV_CLIENT_STUDY_URL, DEV_CLIENT_STUDY_MENTOR_URL); + // 서버 URL public static final String PROD_SERVER_URL = "https://api.gdschongik.com"; public static final String DEV_SERVER_URL = "https://dev-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 290114147..d8ca35ec0 100644 --- a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java +++ b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java @@ -163,13 +163,11 @@ public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); if (environmentUtil.isProdProfile()) { - configuration.addAllowedOriginPattern(PROD_CLIENT_ONBOARDING_URL); - configuration.addAllowedOriginPattern(PROD_CLIENT_ADMIN_URL); + configuration.setAllowedOriginPatterns(PROD_CLIENT_URLS); } if (environmentUtil.isDevProfile()) { - configuration.addAllowedOriginPattern(DEV_CLIENT_ONBOARDING_URL); - configuration.addAllowedOriginPattern(DEV_CLIENT_ADMIN_URL); + configuration.setAllowedOriginPatterns(DEV_CLIENT_URLS); configuration.addAllowedOriginPattern(LOCAL_REACT_CLIENT_URL); configuration.addAllowedOriginPattern(LOCAL_REACT_CLIENT_SECURE_URL); configuration.addAllowedOriginPattern(LOCAL_VITE_CLIENT_URL); From 78a85bbf25b18cb85d84253bab1ccbd50afb4300 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:13:19 +0900 Subject: [PATCH 02/40] =?UTF-8?q?feat:=20=EB=82=B4=20=EB=8B=B4=EB=8B=B9=20?= =?UTF-8?q?=EC=8A=A4=ED=84=B0=EB=94=94=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80=20(#613)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 내 담당 스터디 목록 조회 api 추가 * rename: 메서드명 수정 * refactor: 스터디 종류를 enum으로 반환하도록 수정 --- .../study/api/MentorStudyController.java | 28 +++++++++++++++++++ .../study/application/MentorStudyService.java | 26 +++++++++++++++++ .../domain/study/dao/StudyRepository.java | 7 ++++- .../dto/response/MentorStudyResponse.java | 25 +++++++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/MentorStudyResponse.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java new file mode 100644 index 000000000..8ce49f9bc --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java @@ -0,0 +1,28 @@ +package com.gdschongik.gdsc.domain.study.api; + +import com.gdschongik.gdsc.domain.study.application.MentorStudyService; +import com.gdschongik.gdsc.domain.study.dto.response.MentorStudyResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Mentor Study", description = "멘토 스터디 API입니다.") +@RestController +@RequestMapping("/mentor/studies") +@RequiredArgsConstructor +public class MentorStudyController { + + private final MentorStudyService mentorStudyService; + + @Operation(summary = "내 스터디 조회", description = "내가 멘토로 있는 스터디를 조회합니다.") + @GetMapping("/me") + public ResponseEntity> getStudiesInCharge() { + List response = mentorStudyService.getStudiesInCharge(); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java new file mode 100644 index 000000000..061338e1b --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java @@ -0,0 +1,26 @@ +package com.gdschongik.gdsc.domain.study.application; + +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.study.dao.StudyRepository; +import com.gdschongik.gdsc.domain.study.domain.Study; +import com.gdschongik.gdsc.domain.study.dto.response.MentorStudyResponse; +import com.gdschongik.gdsc.global.util.MemberUtil; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class MentorStudyService { + + private final MemberUtil memberUtil; + private final StudyRepository studyRepository; + + @Transactional(readOnly = true) + public List getStudiesInCharge() { + Member currentMember = memberUtil.getCurrentMember(); + List myStudies = studyRepository.findAllByMentor(currentMember); + return myStudies.stream().map(MentorStudyResponse::from).toList(); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyRepository.java index 0f77c5045..f5c671240 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyRepository.java @@ -1,6 +1,11 @@ package com.gdschongik.gdsc.domain.study.dao; +import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.study.domain.Study; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -public interface StudyRepository extends JpaRepository {} +public interface StudyRepository extends JpaRepository { + + List findAllByMentor(Member mentor); +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/MentorStudyResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/MentorStudyResponse.java new file mode 100644 index 000000000..7b0e710ec --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/MentorStudyResponse.java @@ -0,0 +1,25 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import com.gdschongik.gdsc.domain.study.domain.Study; +import com.gdschongik.gdsc.domain.study.domain.StudyType; +import com.gdschongik.gdsc.global.util.formatter.SemesterFormatter; +import io.swagger.v3.oas.annotations.media.Schema; + +public record MentorStudyResponse( + Long studyId, + @Schema(description = "활동 학기") String semester, + @Schema(description = "이름") String title, + @Schema(description = "종류") StudyType studyType, + @Schema(description = "상세설명 노션 링크") String notionLink, + @Schema(description = "멘토 이름") String mentorName) { + + public static MentorStudyResponse from(Study study) { + return new MentorStudyResponse( + study.getId(), + SemesterFormatter.format(study.getAcademicYear(), study.getSemesterType()), + study.getTitle(), + study.getStudyType(), + study.getNotionLink(), + study.getMentor().getName()); + } +} From b76dcc0d62983bcb522cc65ae9495e1e5f2c05f7 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: Tue, 13 Aug 2024 19:41:59 +0900 Subject: [PATCH 03/40] =?UTF-8?q?feat:=20=EB=A9=98=ED=86=A0=20=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=20=EC=BB=A4=EB=A6=AC=ED=81=98=EB=9F=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20(#606)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 멘토 스터디 커리큘럼 조회 * feat: format변환해서 응답 반환해주기 * feat: 네이밍 단수-> 복수로 변경 * feat: 노션 링크 삭제 --- .../api/MentorStudyDetailController.java | 9 +++++++++ .../application/MentorStudyDetailService.java | 7 +++++++ .../dto/response/StudySessionResponse.java | 19 +++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudySessionResponse.java 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 e74145482..bc74e9a7b 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 @@ -3,6 +3,7 @@ import com.gdschongik.gdsc.domain.study.application.MentorStudyDetailService; import com.gdschongik.gdsc.domain.study.dto.request.AssignmentCreateUpdateRequest; import com.gdschongik.gdsc.domain.study.dto.response.AssignmentResponse; +import com.gdschongik.gdsc.domain.study.dto.response.StudySessionResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -62,4 +63,12 @@ public ResponseEntity cancelStudyAssignment(@PathVariable Long studyDetail mentorStudyDetailService.cancelStudyAssignment(studyDetailId); return ResponseEntity.noContent().build(); } + + // TODO 스터디 세션 워딩을 커리큘럼으로 변경해야함 + @Operation(summary = "스터디 주차별 커리큘럼 목록 조회", description = "멘토가 자신의 스터디 커리큘럼 목록을 조회합니다") + @GetMapping("/sessions") + public ResponseEntity> getStudySessions(@RequestParam(name = "study") Long studyId) { + List response = mentorStudyDetailService.getSessions(studyId); + return ResponseEntity.ok(response); + } } 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 a6c3c5485..48fbd4622 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 @@ -8,6 +8,7 @@ import com.gdschongik.gdsc.domain.study.domain.StudyDetailValidator; import com.gdschongik.gdsc.domain.study.dto.request.AssignmentCreateUpdateRequest; import com.gdschongik.gdsc.domain.study.dto.response.AssignmentResponse; +import com.gdschongik.gdsc.domain.study.dto.response.StudySessionResponse; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.util.MemberUtil; import java.util.List; @@ -83,4 +84,10 @@ public void updateStudyAssignment(Long studyDetailId, AssignmentCreateUpdateRequ log.info("[MentorStudyDetailService] 과제 수정 완료: studyDetailId={}", studyDetailId); } + + @Transactional(readOnly = true) + public List getSessions(Long studyId) { + List studyDetails = studyDetailRepository.findAllByStudyId(studyId); + return studyDetails.stream().map(StudySessionResponse::from).toList(); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudySessionResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudySessionResponse.java new file mode 100644 index 000000000..5c40f046e --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudySessionResponse.java @@ -0,0 +1,19 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import com.gdschongik.gdsc.domain.recruitment.domain.vo.Period; +import com.gdschongik.gdsc.domain.study.domain.Difficulty; +import com.gdschongik.gdsc.domain.study.domain.StudyDetail; +import com.gdschongik.gdsc.domain.study.domain.vo.Session; + +public record StudySessionResponse(Long studyDetailId, Period period, Long week, String title, Difficulty difficulty) { + + public static StudySessionResponse from(StudyDetail studyDetail) { + Session session = studyDetail.getSession(); + return new StudySessionResponse( + studyDetail.getId(), + studyDetail.getPeriod(), + studyDetail.getWeek(), + session.getTitle(), + session.getDifficulty()); + } +} From 7488fd61460a4f91bdca40ef4972dcc1153d5c22 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Tue, 13 Aug 2024 19:45:52 +0900 Subject: [PATCH 04/40] =?UTF-8?q?feat:=20=ED=97=88=EC=9A=A9=EB=90=9C=20COR?= =?UTF-8?q?S=20=EB=A1=9C=EC=BB=AC=20=ED=94=84=EB=A1=9D=EC=8B=9C=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=97=90=20=EC=99=80=EC=9A=B0?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80=20(#615)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 기타 로컬 프록시 도메인 추가 및 그룹화 * feat: 로컬 프로파일 판정 메서드 추가 * refactor: 프로파일 별 origin 설정 로직 개선 --- .../global/common/constant/UrlConstant.java | 22 ++++++++++++++++++- .../gdsc/global/config/WebSecurityConfig.java | 11 ++++------ .../gdsc/global/util/EnvironmentUtil.java | 4 ++++ 3 files changed, 29 insertions(+), 8 deletions(-) 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 a2992c151..9e9989f3b 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,6 +1,7 @@ package com.gdschongik.gdsc.global.common.constant; import java.util.List; +import java.util.stream.Stream; public class UrlConstant { @@ -11,15 +12,21 @@ private UrlConstant() {} public static final String PROD_CLIENT_ADMIN_URL = "https://admin.gdschongik.com"; public static final String PROD_CLIENT_STUDY_URL = "https://study.gdschongik.com"; public static final String PROD_CLIENT_STUDY_MENTOR_URL = "https://mentor.study.gdschongik.com"; + public static final String DEV_CLIENT_ONBOARDING_URL = "https://dev-onboarding.gdschongik.com"; public static final String DEV_CLIENT_ADMIN_URL = "https://dev-admin.gdschongik.com"; public static final String DEV_CLIENT_STUDY_URL = "https://dev-study.gdschongik.com"; public static final String DEV_CLIENT_STUDY_MENTOR_URL = "https://dev-mentor.study.gdschongik.com"; + + public static final String LOCAL_CLIENT_ONBOARDING_URL = "https://local-onboarding.gdschongik.com"; + public static final String LOCAL_CLIENT_ADMIN_URL = "https://local-admin.gdschongik.com"; + public static final String LOCAL_CLIENT_STUDY_URL = "https://local-study.gdschongik.com"; + public static final String LOCAL_CLIENT_STUDY_MENTOR_URL = "https://local-mentor.study.gdschongik.com"; + public static final String LOCAL_REACT_CLIENT_URL = "http://localhost:3000"; public static final String LOCAL_REACT_CLIENT_SECURE_URL = "https://localhost:3000"; 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 PROD_CLIENT_URLS = List.of( PROD_CLIENT_ONBOARDING_URL, PROD_CLIENT_ADMIN_URL, PROD_CLIENT_STUDY_URL, PROD_CLIENT_STUDY_MENTOR_URL); @@ -27,6 +34,19 @@ private UrlConstant() {} public static final List DEV_CLIENT_URLS = List.of(DEV_CLIENT_ONBOARDING_URL, DEV_CLIENT_ADMIN_URL, DEV_CLIENT_STUDY_URL, DEV_CLIENT_STUDY_MENTOR_URL); + public static final List LOCAL_CLIENT_URLS = List.of( + LOCAL_CLIENT_ONBOARDING_URL, + LOCAL_CLIENT_ADMIN_URL, + LOCAL_CLIENT_STUDY_URL, + LOCAL_CLIENT_STUDY_MENTOR_URL, + LOCAL_REACT_CLIENT_URL, + LOCAL_REACT_CLIENT_SECURE_URL, + LOCAL_VITE_CLIENT_URL, + LOCAL_VITE_CLIENT_SECURE_URL); + + public static final List DEV_AND_LOCAL_CLIENT_URLS = + Stream.concat(DEV_CLIENT_URLS.stream(), LOCAL_CLIENT_URLS.stream()).toList(); + // 서버 URL public static final String PROD_SERVER_URL = "https://api.gdschongik.com"; public static final String DEV_SERVER_URL = "https://dev-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 d8ca35ec0..e6ec97dfa 100644 --- a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java +++ b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java @@ -167,15 +167,12 @@ public CorsConfigurationSource corsConfigurationSource() { } if (environmentUtil.isDevProfile()) { - configuration.setAllowedOriginPatterns(DEV_CLIENT_URLS); - configuration.addAllowedOriginPattern(LOCAL_REACT_CLIENT_URL); - configuration.addAllowedOriginPattern(LOCAL_REACT_CLIENT_SECURE_URL); - configuration.addAllowedOriginPattern(LOCAL_VITE_CLIENT_URL); - configuration.addAllowedOriginPattern(LOCAL_VITE_CLIENT_SECURE_URL); - configuration.addAllowedOriginPattern(DEV_SERVER_URL); + configuration.setAllowedOriginPatterns(DEV_AND_LOCAL_CLIENT_URLS); } - configuration.addAllowedOriginPattern(LOCAL_PROXY_CLIENT_ONBOARDING_URL); + if (environmentUtil.isLocalProfile()) { + configuration.setAllowedOriginPatterns(LOCAL_CLIENT_URLS); + } configuration.addAllowedHeader("*"); configuration.addAllowedMethod("*"); diff --git a/src/main/java/com/gdschongik/gdsc/global/util/EnvironmentUtil.java b/src/main/java/com/gdschongik/gdsc/global/util/EnvironmentUtil.java index 98cf71ca6..99383ad9f 100644 --- a/src/main/java/com/gdschongik/gdsc/global/util/EnvironmentUtil.java +++ b/src/main/java/com/gdschongik/gdsc/global/util/EnvironmentUtil.java @@ -36,6 +36,10 @@ public boolean isDevAndLocalProfile() { return getActiveProfiles().anyMatch(DEV_AND_LOCAL_ENV::contains); } + public boolean isLocalProfile() { + return getActiveProfiles().anyMatch(LOCAL_ENV::equals); + } + private Stream getActiveProfiles() { return Stream.of(environment.getActiveProfiles()); } From fabbb4ed919c0f2156eaca213eb751d1e4ecf9e8 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Tue, 13 Aug 2024 23:58:22 +0900 Subject: [PATCH 05/40] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20API=EC=97=90=EC=84=9C=20=EC=BF=A0=ED=82=A4=EA=B0=80?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20(#620)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: 삭제하려는 쿠키의 도메인을 루트 도메인으로 유지 --- src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java | 1 + 1 file changed, 1 insertion(+) 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 2c6d5fb0f..df65ba58b 100644 --- a/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java +++ b/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java @@ -36,6 +36,7 @@ public void deleteCookie(jakarta.servlet.http.Cookie cookie, HttpServletResponse cookie.setPath("/"); cookie.setValue(""); cookie.setMaxAge(0); + cookie.setDomain(ROOT_DOMAIN); response.addCookie(cookie); } } From 95eb998f107b3416cbc54e93ac3e36822545d779 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:28:13 +0900 Subject: [PATCH 06/40] =?UTF-8?q?feat:=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EC=9E=85=EB=A0=A5=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#594)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: StudyHistory에 레포지토리 링크 필드 추가 * feat: 레포지토리 입력 api 추가 * test: 레포지토리 입력 도메인 서비스 테스트 추가 * rename: 변수명 수정 * rename: 메서드명 수정 * rename: 메서드명 수정 * refactor: 쿼리 조건 수정 * fix: 엔드포인트 수정 * fix: 수강 이력 서비스 분리 * remove: 사용하지 않는 import 제거 * rename: 서비스 클래스명 수정 * docs: 로그에 입력된 레포지토리 링크 추가 * feat: todo 주석 추가 * feat: 레포지토리 소유자 확인 로직 추가 --- .../api/StudentStudyHistoryController.java | 32 ++++++++++ .../StudentStudyHistoryService.java | 62 +++++++++++++++++++ .../AssignmentHistoryCustomRepository.java | 9 +++ ...AssignmentHistoryCustomRepositoryImpl.java | 39 ++++++++++++ .../dao/AssignmentHistoryRepository.java | 7 +++ .../domain/study/domain/StudyHistory.java | 9 +++ .../study/domain/StudyHistoryValidator.java | 14 +++++ .../dto/request/RepositoryUpdateRequest.java | 5 ++ .../common/constant/GithubConstant.java | 8 +++ .../gdsc/global/exception/ErrorCode.java | 8 ++- .../infra/client/github/GithubClient.java | 12 ++++ .../domain/StudyHistoryValidatorTest.java | 28 +++++++++ 12 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyHistoryController.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryRepository.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/request/RepositoryUpdateRequest.java create mode 100644 src/main/java/com/gdschongik/gdsc/global/common/constant/GithubConstant.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyHistoryController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyHistoryController.java new file mode 100644 index 000000000..4d6c556ff --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyHistoryController.java @@ -0,0 +1,32 @@ +package com.gdschongik.gdsc.domain.study.api; + +import com.gdschongik.gdsc.domain.study.application.StudentStudyHistoryService; +import com.gdschongik.gdsc.domain.study.dto.request.RepositoryUpdateRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +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.RestController; + +@Tag(name = "Student Study History", description = "사용자 스터디 수강 이력 API입니다.") +@RestController +@RequestMapping("/study-history") +@RequiredArgsConstructor +public class StudentStudyHistoryController { + + private final StudentStudyHistoryService studentStudyHistoryService; + + @Operation(summary = "레포지토리 입력", description = "레포지토리를 입력합니다. 이미 제출한 과제가 있다면 수정할 수 없습니다.") + @PutMapping("/{studyHistoryId}/repository") + public ResponseEntity updateRepository( + @PathVariable Long studyHistoryId, @Valid @RequestBody RepositoryUpdateRequest request) throws IOException { + studentStudyHistoryService.updateRepository(studyHistoryId, request); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java new file mode 100644 index 000000000..df253f536 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java @@ -0,0 +1,62 @@ +package com.gdschongik.gdsc.domain.study.application; + +import static com.gdschongik.gdsc.global.common.constant.GithubConstant.*; +import static com.gdschongik.gdsc.global.exception.ErrorCode.*; + +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.study.dao.AssignmentHistoryRepository; +import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository; +import com.gdschongik.gdsc.domain.study.domain.Study; +import com.gdschongik.gdsc.domain.study.domain.StudyHistory; +import com.gdschongik.gdsc.domain.study.domain.StudyHistoryValidator; +import com.gdschongik.gdsc.domain.study.dto.request.RepositoryUpdateRequest; +import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.util.MemberUtil; +import com.gdschongik.gdsc.infra.client.github.GithubClient; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.kohsuke.github.GHRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class StudentStudyHistoryService { + + private final MemberUtil memberUtil; + private final GithubClient githubClient; + private final StudyHistoryRepository studyHistoryRepository; + private final AssignmentHistoryRepository assignmentHistoryRepository; + private final StudyHistoryValidator studyHistoryValidator; + + @Transactional + public void updateRepository(Long studyHistoryId, RepositoryUpdateRequest request) throws IOException { + Member currentMember = memberUtil.getCurrentMember(); + StudyHistory studyHistory = studyHistoryRepository + .findById(studyHistoryId) + .orElseThrow(() -> new CustomException(STUDY_HISTORY_NOT_FOUND)); + Study study = studyHistory.getStudy(); + + boolean isAnyAssignmentSubmitted = + assignmentHistoryRepository.existsSubmittedAssignmentByMemberAndStudy(currentMember, study); + String ownerRepo = getOwnerRepo(request.repositoryLink()); + GHRepository repository = githubClient.getRepository(ownerRepo); + studyHistoryValidator.validateUpdateRepository( + isAnyAssignmentSubmitted, String.valueOf(repository.getOwner().getId()), currentMember.getOauthId()); + + studyHistory.updateRepositoryLink(request.repositoryLink()); + studyHistoryRepository.save(studyHistory); + + log.info( + "[StudyHistoryService] 레포지토리 입력: studyHistoryId={}, repositoryLink={}", + studyHistory.getId(), + request.repositoryLink()); + } + + private String getOwnerRepo(String repositoryLink) { + int startIndex = repositoryLink.indexOf(GITHUB_DOMAIN) + GITHUB_DOMAIN.length(); + return repositoryLink.substring(startIndex); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java new file mode 100644 index 000000000..89766c38a --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java @@ -0,0 +1,9 @@ +package com.gdschongik.gdsc.domain.study.dao; + +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.study.domain.Study; + +public interface AssignmentHistoryCustomRepository { + + boolean existsSubmittedAssignmentByMemberAndStudy(Member member, Study study); +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java new file mode 100644 index 000000000..c212e0f50 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java @@ -0,0 +1,39 @@ +package com.gdschongik.gdsc.domain.study.dao; + +import static com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus.*; +import static com.gdschongik.gdsc.domain.study.domain.QAssignmentHistory.*; + +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.study.domain.Study; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class AssignmentHistoryCustomRepositoryImpl implements AssignmentHistoryCustomRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public boolean existsSubmittedAssignmentByMemberAndStudy(Member member, Study study) { + Integer fetchOne = queryFactory + .selectOne() + .from(assignmentHistory) + .where(eqMember(member), eqStudy(study), isSubmitted()) + .fetchFirst(); + + return fetchOne != null; + } + + private BooleanExpression eqMember(Member member) { + return member == null ? null : assignmentHistory.member.eq(member); + } + + private BooleanExpression eqStudy(Study study) { + return study == null ? null : assignmentHistory.studyDetail.study.eq(study); + } + + private BooleanExpression isSubmitted() { + return assignmentHistory.submissionStatus.in(FAILURE, SUCCESS); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryRepository.java new file mode 100644 index 000000000..89ed0c714 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryRepository.java @@ -0,0 +1,7 @@ +package com.gdschongik.gdsc.domain.study.dao; + +import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AssignmentHistoryRepository + extends JpaRepository, AssignmentHistoryCustomRepository {} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java index a77ad9dd3..52689f528 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java @@ -33,6 +33,8 @@ public class StudyHistory extends BaseEntity { @JoinColumn(name = "study_id") private Study study; + private String repositoryLink; + @Builder(access = AccessLevel.PRIVATE) private StudyHistory(Member mentee, Study study) { this.mentee = mentee; @@ -43,6 +45,13 @@ public static StudyHistory create(Member mentee, Study study) { return StudyHistory.builder().mentee(mentee).study(study).build(); } + /** + * 레포지토리 링크를 업데이트합니다. + */ + public void updateRepositoryLink(String repositoryLink) { + this.repositoryLink = repositoryLink; + } + // 데이터 전달 로직 public boolean isStudyOngoing() { return study.isStudyOngoing(); diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistoryValidator.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistoryValidator.java index aed358292..2e2d19d82 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistoryValidator.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistoryValidator.java @@ -24,6 +24,7 @@ public void validateApplyStudy(Study study, List currentMemberStud } // 이미 듣고 있는 스터디가 있는 경우 + // todo: StudyHistory가 아닌 Study의 isOngoning 호출하도록 수정 boolean isInOngoingStudy = currentMemberStudyHistories.stream().anyMatch(StudyHistory::isStudyOngoing); if (isInOngoingStudy) { @@ -37,4 +38,17 @@ public void validateCancelStudyApply(Study study) { throw new CustomException(STUDY_NOT_CANCELABLE_APPLICATION_PERIOD); } } + + public void validateUpdateRepository( + boolean isAnyAssignmentSubmitted, String repositoryOwnerOauthId, String currentMemberOauthId) { + // 이미 제출한 과제가 있는 경우 + if (isAnyAssignmentSubmitted) { + throw new CustomException(STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_ASSIGNMENT_ALREADY_SUBMITTED); + } + + // 레포지토리 소유자가 현 멤버가 아닌 경우 + if (!repositoryOwnerOauthId.equals(currentMemberOauthId)) { + throw new CustomException(STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_OWNER_MISMATCH); + } + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/RepositoryUpdateRequest.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/RepositoryUpdateRequest.java new file mode 100644 index 000000000..7f311f37b --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/RepositoryUpdateRequest.java @@ -0,0 +1,5 @@ +package com.gdschongik.gdsc.domain.study.dto.request; + +import jakarta.validation.constraints.NotBlank; + +public record RepositoryUpdateRequest(@NotBlank String repositoryLink) {} diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/GithubConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/GithubConstant.java new file mode 100644 index 000000000..fc593abc6 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/GithubConstant.java @@ -0,0 +1,8 @@ +package com.gdschongik.gdsc.global.common.constant; + +public class GithubConstant { + + public static final String GITHUB_DOMAIN = "github.com/"; + + private GithubConstant() {} +} 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 17686780c..b17d81072 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -117,6 +117,9 @@ public enum ErrorCode { STUDY_HISTORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 수강 기록입니다."), STUDY_HISTORY_DUPLICATE(HttpStatus.CONFLICT, "이미 해당 스터디를 신청했습니다."), STUDY_HISTORY_ONGOING_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 진행중인 스터디가 있습니다."), + STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_ASSIGNMENT_ALREADY_SUBMITTED( + HttpStatus.CONFLICT, "이미 제출한 과제가 있으므로 레포지토리를 수정할 수 없습니다."), + STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_OWNER_MISMATCH(HttpStatus.CONFLICT, "레포지토리 소유자가 현재 멤버와 다릅니다."), // Order ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "주문이 존재하지 않습니다."), @@ -142,7 +145,10 @@ public enum ErrorCode { // Assignment ASSIGNMENT_CAN_NOT_BE_UPDATED(HttpStatus.CONFLICT, "휴강인 과제는 수정할 수 없습니다."), - ASSIGNMENT_DEADLINE_INVALID(HttpStatus.CONFLICT, "과제 마감 기한이 현재보다 빠릅니다."); + ASSIGNMENT_DEADLINE_INVALID(HttpStatus.CONFLICT, "과제 마감 기한이 현재보다 빠릅니다."), + + // Github + GITHUB_REPOSITORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 레포지토리입니다."); private final HttpStatus status; private final String message; 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 index e3235dfb7..6085c2c03 100644 --- a/src/main/java/com/gdschongik/gdsc/infra/client/github/GithubClient.java +++ b/src/main/java/com/gdschongik/gdsc/infra/client/github/GithubClient.java @@ -1,6 +1,10 @@ package com.gdschongik.gdsc.infra.client.github; +import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.exception.ErrorCode; +import java.io.IOException; import lombok.RequiredArgsConstructor; +import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; import org.springframework.stereotype.Component; @@ -9,4 +13,12 @@ public class GithubClient { private final GitHub github; + + public GHRepository getRepository(String ownerRepo) { + try { + return github.getRepository(ownerRepo); + } catch (IOException e) { + throw new CustomException(ErrorCode.GITHUB_REPOSITORY_NOT_FOUND); + } + } } diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyHistoryValidatorTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyHistoryValidatorTest.java index 4631ab5da..f72b97dba 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyHistoryValidatorTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyHistoryValidatorTest.java @@ -1,5 +1,6 @@ package com.gdschongik.gdsc.domain.study.domain; +import static com.gdschongik.gdsc.global.common.constant.MemberConstant.*; import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import static org.assertj.core.api.Assertions.*; @@ -96,4 +97,31 @@ class 스터디_수강신청_취소시 { .hasMessage(STUDY_NOT_CANCELABLE_APPLICATION_PERIOD.getMessage()); } } + + @Nested + class 레포지토리_입력시 { + + @Test + void 이미_제출한_과제가_있다면_실패한다() { + // given + boolean isAnyAssignmentSubmitted = true; + + // when & then + assertThatThrownBy(() -> studyHistoryValidator.validateUpdateRepository( + isAnyAssignmentSubmitted, OAUTH_ID, OAUTH_ID)) + .isInstanceOf(CustomException.class) + .hasMessage(STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_ASSIGNMENT_ALREADY_SUBMITTED.getMessage()); + } + + @Test + void 레포지토리의_소유자와_현재_멤버가_일치하지_않는다면_실패한다() { + // given + String wrongOauthId = "1234567"; + + // when & then + assertThatThrownBy(() -> studyHistoryValidator.validateUpdateRepository(false, wrongOauthId, OAUTH_ID)) + .isInstanceOf(CustomException.class) + .hasMessage(STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_OWNER_MISMATCH.getMessage()); + } + } } From 036e12da31f33ccc1a3bcbee5b90a4601d87a04e 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, 14 Aug 2024 19:20:58 +0900 Subject: [PATCH 07/40] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20(#608)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 스터디 기본 정보 조회 API 스펙 구현 * feat: 스터디 기본 조회 공통 API 구현 및 스터디 포매터 유틸로 분리 * feat: string 포매터 구현 제거 * feat: 트랜잭셔널 클래스 레벨에서 제거 * feat: formatting 되어 있는 변수 수정 --- .../study/api/CommonStudyController.java | 28 +++++++++++++ .../study/application/CommonStudyService.java | 26 ++++++++++++ .../dto/response/AssignmentResponse.java | 5 ++- .../dto/response/CommonStudyResponse.java | 40 +++++++++++++++++++ .../study/dto/response/StudyResponse.java | 34 +++++----------- 5 files changed, 106 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/api/CommonStudyController.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/application/CommonStudyService.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/CommonStudyResponse.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/CommonStudyController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/CommonStudyController.java new file mode 100644 index 000000000..af1b58517 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/CommonStudyController.java @@ -0,0 +1,28 @@ +package com.gdschongik.gdsc.domain.study.api; + +import com.gdschongik.gdsc.domain.study.application.CommonStudyService; +import com.gdschongik.gdsc.domain.study.dto.response.CommonStudyResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Common Study", description = "공통 스터디 API입니다.") +@RestController +@RequestMapping("/common/studies") +@RequiredArgsConstructor +public class CommonStudyController { + + private final CommonStudyService commonStudyService; + + @Operation(summary = "스터디 기본 정보 조회", description = "스터디 기본 정보를 조회합니다.") + @GetMapping("/{studyId}") + public ResponseEntity getStudyInformation(@PathVariable Long studyId) { + CommonStudyResponse response = commonStudyService.getStudyInformation(studyId); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/CommonStudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/CommonStudyService.java new file mode 100644 index 000000000..4561160e8 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/CommonStudyService.java @@ -0,0 +1,26 @@ +package com.gdschongik.gdsc.domain.study.application; + +import static com.gdschongik.gdsc.global.exception.ErrorCode.STUDY_NOT_FOUND; + +import com.gdschongik.gdsc.domain.study.dao.StudyRepository; +import com.gdschongik.gdsc.domain.study.domain.Study; +import com.gdschongik.gdsc.domain.study.dto.response.CommonStudyResponse; +import com.gdschongik.gdsc.global.exception.CustomException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CommonStudyService { + + private final StudyRepository studyRepository; + + @Transactional(readOnly = true) + public CommonStudyResponse getStudyInformation(Long studyId) { + Study study = studyRepository.findById(studyId).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND)); + return CommonStudyResponse.from(study); + } +} 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 index 33414fed9..5967f53f7 100644 --- 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 @@ -4,11 +4,12 @@ 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; +import java.time.LocalDateTime; public record AssignmentResponse( Long studyDetailId, @Schema(description = "과제 제목") String title, - @Schema(description = "마감 기한") String deadline, + @Schema(description = "마감 기한") LocalDateTime deadline, @Schema(description = "과제 명세 링크") String descriptionLink, @Schema(description = "과제 상태") StudyStatus assignmentStatus) { public static AssignmentResponse from(StudyDetail studyDetail) { @@ -16,7 +17,7 @@ public static AssignmentResponse from(StudyDetail studyDetail) { return new AssignmentResponse( studyDetail.getId(), assignment.getTitle(), - assignment.getDeadline().toString(), + assignment.getDeadline(), assignment.getDescriptionLink(), assignment.getStatus()); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/CommonStudyResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/CommonStudyResponse.java new file mode 100644 index 000000000..096838a9d --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/CommonStudyResponse.java @@ -0,0 +1,40 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import com.gdschongik.gdsc.domain.common.model.SemesterType; +import com.gdschongik.gdsc.domain.recruitment.domain.vo.Period; +import com.gdschongik.gdsc.domain.study.domain.Study; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.DayOfWeek; +import java.time.LocalTime; + +public record CommonStudyResponse( + Long studyId, + @Schema(description = "이름") String title, + @Schema(description = "활동 년도") Integer academicYear, + @Schema(description = "활동 학기") SemesterType semester, + @Schema(description = "종류") String studyType, + @Schema(description = "상세설명 노션 링크") String notionLink, + @Schema(description = "한 줄 소개") String introduction, + @Schema(description = "멘토 이름") String mentorName, + @Schema(description = "스터디 요일") DayOfWeek dayOfWeek, + @Schema(description = "스터디 시작 시간") LocalTime startTime, + @Schema(description = "스터디 종료 시간") LocalTime endTime, + @Schema(description = "총 주차수") Long totalWeek, + @Schema(description = "총 기간") Period period) { + public static CommonStudyResponse from(Study study) { + return new CommonStudyResponse( + study.getId(), + study.getTitle(), + study.getAcademicYear(), + study.getSemesterType(), + study.getStudyType().getValue(), + study.getNotionLink(), + study.getIntroduction(), + study.getMentor().getName(), + study.getDayOfWeek(), + study.getStartTime(), + study.getEndTime(), + study.getTotalWeek(), + study.getPeriod()); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyResponse.java index 56b531e81..e04ccdd8e 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyResponse.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyResponse.java @@ -3,8 +3,8 @@ import com.gdschongik.gdsc.domain.study.domain.Study; import io.swagger.v3.oas.annotations.media.Schema; import java.time.DayOfWeek; +import java.time.LocalDateTime; import java.time.LocalTime; -import java.time.format.DateTimeFormatter; public record StudyResponse( Long studyId, @@ -13,12 +13,12 @@ public record StudyResponse( @Schema(description = "상세설명 노션 링크") String notionLink, @Schema(description = "한 줄 소개") String introduction, @Schema(description = "멘토 이름") String mentorName, - @Schema(description = "스터디 시간") String schedule, - @Schema(description = "총 주차수") String totalWeek, - @Schema(description = "개강일") String openingDate) { + @Schema(description = "스터디 요일") DayOfWeek dayOfWeek, + @Schema(description = "스터디 시작 시간") LocalTime startTime, + @Schema(description = "총 주차수") Long totalWeek, + @Schema(description = "개강일") LocalDateTime openingDate) { public static StudyResponse from(Study study) { - // todo: 포맷터로 분리 return new StudyResponse( study.getId(), study.getTitle(), @@ -26,25 +26,9 @@ public static StudyResponse from(Study study) { study.getNotionLink(), study.getIntroduction(), study.getMentor().getName(), - getSchedule(study.getDayOfWeek(), study.getStartTime()), - study.getTotalWeek().toString() + "주 코스", - DateTimeFormatter.ofPattern("MM.dd").format(study.getPeriod().getStartDate()) + " 개강"); - } - - private static String getSchedule(DayOfWeek dayOfWeek, LocalTime startTime) { - return getKoreanDayOfWeek(dayOfWeek) + startTime.format(DateTimeFormatter.ofPattern("HH")) + "시"; - } - - private static String getKoreanDayOfWeek(DayOfWeek dayOfWeek) { - return switch (dayOfWeek) { - case MONDAY -> "월"; - case TUESDAY -> "화"; - case WEDNESDAY -> "수"; - case THURSDAY -> "목"; - case FRIDAY -> "금"; - case SATURDAY -> "토"; - case SUNDAY -> "일"; - default -> ""; - }; + study.getDayOfWeek(), + study.getStartTime(), + study.getTotalWeek(), + study.getPeriod().getStartDate()); } } From f5587bc8a61059f8b5a3d0abd052f5eed9ded68b 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, 14 Aug 2024 21:27:39 +0900 Subject: [PATCH 08/40] =?UTF-8?q?feat:=20=EB=82=98=EC=9D=98=20=EA=B3=BC?= =?UTF-8?q?=EC=A0=9C=20=ED=9E=88=EC=8A=A4=ED=86=A0=EB=A6=AC=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20(#610)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 과제 히스토리 조회 API * feat: 코드 네이밍 수정 * feat: fetch join 추가 * feat: 투두 주석 추가 및 네이밍 수정 * feat: 과제 제출 링크(레포지토리 링크) 추가 * feat: 쿼리 메소드 가독성 높이기 * feat: develop과 클래스 구조 일치 시키기 * feat: 쿼리 메서드 매개변수 위치 및 변수명 수정 --- .../api/StudentStudyHistoryController.java | 16 ++++++++---- .../StudentStudyHistoryService.java | 12 +++++++++ .../study/application/StudyService.java | 4 +-- .../AssignmentHistoryCustomRepository.java | 4 +++ ...AssignmentHistoryCustomRepositoryImpl.java | 17 ++++++++++++ .../study/dao/StudyHistoryRepository.java | 1 + .../response/AssignmentHistoryResponse.java | 26 +++++++++++++++++++ 7 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryResponse.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyHistoryController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyHistoryController.java index 4d6c556ff..0894221fa 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyHistoryController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyHistoryController.java @@ -2,17 +2,15 @@ import com.gdschongik.gdsc.domain.study.application.StudentStudyHistoryService; import com.gdschongik.gdsc.domain.study.dto.request.RepositoryUpdateRequest; +import com.gdschongik.gdsc.domain.study.dto.response.AssignmentHistoryResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.io.IOException; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -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.RestController; +import org.springframework.web.bind.annotation.*; @Tag(name = "Student Study History", description = "사용자 스터디 수강 이력 API입니다.") @RestController @@ -29,4 +27,12 @@ public ResponseEntity updateRepository( studentStudyHistoryService.updateRepository(studyHistoryId, request); return ResponseEntity.ok().build(); } + + @Operation(summary = "스터디 과제 히스토리 목록 조회", description = "스터디 과제 제출 내역을 조회합니다.") + @GetMapping("/assignments") + public ResponseEntity> getAllAssignmentHistories( + @RequestParam(name = "studyId") Long studyId) { + List response = studentStudyHistoryService.getAllAssignmentHistories(studyId); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java index df253f536..c38e9a2a0 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java @@ -10,10 +10,12 @@ import com.gdschongik.gdsc.domain.study.domain.StudyHistory; import com.gdschongik.gdsc.domain.study.domain.StudyHistoryValidator; import com.gdschongik.gdsc.domain.study.dto.request.RepositoryUpdateRequest; +import com.gdschongik.gdsc.domain.study.dto.response.AssignmentHistoryResponse; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.util.MemberUtil; import com.gdschongik.gdsc.infra.client.github.GithubClient; import java.io.IOException; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.kohsuke.github.GHRepository; @@ -59,4 +61,14 @@ private String getOwnerRepo(String repositoryLink) { int startIndex = repositoryLink.indexOf(GITHUB_DOMAIN) + GITHUB_DOMAIN.length(); return repositoryLink.substring(startIndex); } + + // TODO mentee -> study 변환 작업 필요 + @Transactional(readOnly = true) + public List getAllAssignmentHistories(Long studyId) { + Member currentMember = memberUtil.getCurrentMember(); + + return assignmentHistoryRepository.findAssignmentHistoriesByMenteeAndStudy(currentMember, studyId).stream() + .map(AssignmentHistoryResponse::from) + .toList(); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyService.java index c0ed98955..4bbe85b04 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyService.java @@ -5,9 +5,7 @@ import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository; import com.gdschongik.gdsc.domain.study.dao.StudyRepository; -import com.gdschongik.gdsc.domain.study.domain.Study; -import com.gdschongik.gdsc.domain.study.domain.StudyHistory; -import com.gdschongik.gdsc.domain.study.domain.StudyHistoryValidator; +import com.gdschongik.gdsc.domain.study.domain.*; import com.gdschongik.gdsc.domain.study.dto.response.StudyResponse; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.util.MemberUtil; diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java index 89766c38a..c0edd00ec 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java @@ -1,9 +1,13 @@ package com.gdschongik.gdsc.domain.study.dao; import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; import com.gdschongik.gdsc.domain.study.domain.Study; +import java.util.List; public interface AssignmentHistoryCustomRepository { boolean existsSubmittedAssignmentByMemberAndStudy(Member member, Study study); + + List findAssignmentHistoriesByMenteeAndStudy(Member member, Long studyId); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java index c212e0f50..02d3b57df 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java @@ -2,11 +2,14 @@ import static com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus.*; import static com.gdschongik.gdsc.domain.study.domain.QAssignmentHistory.*; +import static com.gdschongik.gdsc.domain.study.domain.QStudyDetail.studyDetail; import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; import com.gdschongik.gdsc.domain.study.domain.Study; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @@ -36,4 +39,18 @@ private BooleanExpression eqStudy(Study study) { private BooleanExpression isSubmitted() { return assignmentHistory.submissionStatus.in(FAILURE, SUCCESS); } + + @Override + public List findAssignmentHistoriesByMenteeAndStudy(Member currentMember, Long studyId) { + return queryFactory + .selectFrom(assignmentHistory) + .join(assignmentHistory.studyDetail, studyDetail) + .fetchJoin() + .where(eqStudyId(studyId).and(eqMember(currentMember))) + .fetch(); + } + + private BooleanExpression eqStudyId(Long studyId) { + return studyId != null ? studyDetail.study.id.eq(studyId) : null; + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java index f706aea27..a45f62e4a 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java @@ -9,6 +9,7 @@ public interface StudyHistoryRepository extends JpaRepository { + // TODO mentee -> student로 변경 List findAllByMentee(Member member); Optional findByMenteeAndStudy(Member member, Study study); diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryResponse.java new file mode 100644 index 000000000..5152004e8 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryResponse.java @@ -0,0 +1,26 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; +import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +public record AssignmentHistoryResponse( + Long assignmentHistoryId, + @Schema(description = "과제 제목") String title, + @Schema(description = "마감 기한") LocalDateTime deadline, + @Schema(description = "과제 명세 링크") String descriptionLink, + @Schema(description = "과제 제출 링크") String submissionLink, + @Schema(description = "과제 제출 상태") AssignmentSubmissionStatus assignmentSubmissionStatus, + @Schema(description = "주차") Long week) { + public static AssignmentHistoryResponse from(AssignmentHistory assignmentHistory) { + return new AssignmentHistoryResponse( + assignmentHistory.getId(), + assignmentHistory.getStudyDetail().getAssignment().getTitle(), + assignmentHistory.getStudyDetail().getAssignment().getDeadline(), + assignmentHistory.getStudyDetail().getAssignment().getDescriptionLink(), + assignmentHistory.getSubmissionLink(), + assignmentHistory.getSubmissionStatus(), + assignmentHistory.getStudyDetail().getWeek()); + } +} From 8ce2d0994476a4293bd72c3758cf8e714d23d03a Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Wed, 14 Aug 2024 23:42:23 +0900 Subject: [PATCH 09/40] =?UTF-8?q?feat:=20=EC=BD=94=EC=96=B4=EB=A9=A4?= =?UTF-8?q?=EB=B2=84=20=EC=A0=84=EC=B2=B4=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=ED=95=98=EA=B8=B0=20API=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(#624)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: 전체 스터디 조회하기 API 구현 --- .../gdsc/domain/study/api/AdminStudyController.java | 10 ++++++++++ .../domain/study/application/AdminStudyService.java | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/AdminStudyController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/AdminStudyController.java index 730bc7ffd..70a2b0438 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/AdminStudyController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/AdminStudyController.java @@ -2,11 +2,14 @@ import com.gdschongik.gdsc.domain.study.application.AdminStudyService; import com.gdschongik.gdsc.domain.study.dto.request.StudyCreateRequest; +import com.gdschongik.gdsc.domain.study.dto.response.StudyResponse; 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.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -26,4 +29,11 @@ public ResponseEntity createStudy(@Valid @RequestBody StudyCreateRequest r adminStudyService.createStudyAndStudyDetail(request); return ResponseEntity.ok().build(); } + + @Operation(summary = "전체 스터디 조회", description = "모든 스터디를 조회합니다. 코어멤버만 접근 가능합니다.") + @GetMapping + public ResponseEntity> getStudies() { + List response = adminStudyService.getAllStudies(); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/AdminStudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/AdminStudyService.java index 76b9e01e3..91143fa72 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/AdminStudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/AdminStudyService.java @@ -7,6 +7,7 @@ import com.gdschongik.gdsc.domain.study.domain.Study; import com.gdschongik.gdsc.domain.study.domain.StudyDetail; import com.gdschongik.gdsc.domain.study.dto.request.StudyCreateRequest; +import com.gdschongik.gdsc.domain.study.dto.response.StudyResponse; import com.gdschongik.gdsc.domain.study.factory.StudyDomainFactory; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.exception.ErrorCode; @@ -20,7 +21,6 @@ @Slf4j @Service @RequiredArgsConstructor -@Transactional(readOnly = true) public class AdminStudyService { private final StudyRepository studyRepository; @@ -55,4 +55,9 @@ private List createNoneStudyDetail(Study study) { private Member getMemberById(Long memberId) { return memberRepository.findById(memberId).orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)); } + + @Transactional(readOnly = true) + public List getAllStudies() { + return studyRepository.findAll().stream().map(StudyResponse::from).toList(); + } } From da08b2b1ea07566363b33eec3a866f9d9d14e241 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Thu, 15 Aug 2024 17:09:24 +0900 Subject: [PATCH 10/40] =?UTF-8?q?feat:=20=EA=B0=80=EC=9E=A5=20=EC=B5=9C?= =?UTF-8?q?=EC=8B=A0=20=EA=B9=83=ED=97=88=EB=B8=8C=20=EA=B3=BC=EC=A0=9C=20?= =?UTF-8?q?=EC=A0=9C=EC=B6=9C=EC=9D=B4=EB=A0=A5=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#626)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: 투두 추가 * chore: 로컬 환경에서 깃허브 라이브러리 로깅 설정 * refactor: 깃허브 클라이언트 패키지 변경 * feat: 깃허브 과제 경로 상수 추가 * feat: 깃허브 과제 없는 경우 예외 에러코드 추가 * feat: 가장 최신 제출이력 조회 로직 구현 * docs: 주석 추가 * feat: 응답에 커밋 해시 추가 --- .../StudentStudyHistoryService.java | 2 +- .../domain/AssignmentSubmissionStatus.java | 2 +- .../common/constant/GithubConstant.java | 1 + .../gdsc/global/exception/ErrorCode.java | 4 +- .../infra/client/github/GithubClient.java | 24 -------- .../infra/github/client/GithubClient.java | 59 +++++++++++++++++++ .../GithubAssignmentSubmissionResponse.java | 5 ++ src/main/resources/application-local.yml | 1 + 8 files changed, 71 insertions(+), 27 deletions(-) delete mode 100644 src/main/java/com/gdschongik/gdsc/infra/client/github/GithubClient.java create mode 100644 src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java create mode 100644 src/main/java/com/gdschongik/gdsc/infra/github/dto/response/GithubAssignmentSubmissionResponse.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java index c38e9a2a0..ca87ab75c 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java @@ -13,7 +13,7 @@ import com.gdschongik.gdsc.domain.study.dto.response.AssignmentHistoryResponse; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.util.MemberUtil; -import com.gdschongik.gdsc.infra.client.github.GithubClient; +import com.gdschongik.gdsc.infra.github.client.GithubClient; import java.io.IOException; import java.util.List; import lombok.RequiredArgsConstructor; 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 index 4ac62cd11..3865abb98 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionStatus.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionStatus.java @@ -9,7 +9,7 @@ public enum AssignmentSubmissionStatus { PENDING("제출 전"), FAILURE("제출 실패"), SUCCESS("제출 성공"), - CANCELLED("과제 휴강"); + CANCELLED("과제 휴강"); // TODO: 제거 및 DB에서 삭제 private final String value; } diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/GithubConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/GithubConstant.java index fc593abc6..d524cb9f4 100644 --- a/src/main/java/com/gdschongik/gdsc/global/common/constant/GithubConstant.java +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/GithubConstant.java @@ -3,6 +3,7 @@ public class GithubConstant { public static final String GITHUB_DOMAIN = "github.com/"; + public static final String GITHUB_ASSIGNMENT_PATH = "week%d/WIL.md"; private GithubConstant() {} } 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 b17d81072..9940bb24e 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -148,7 +148,9 @@ public enum ErrorCode { ASSIGNMENT_DEADLINE_INVALID(HttpStatus.CONFLICT, "과제 마감 기한이 현재보다 빠릅니다."), // Github - GITHUB_REPOSITORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 레포지토리입니다."); + GITHUB_REPOSITORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 레포지토리입니다."), + GITHUB_ASSIGNMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 과제 파일입니다."), + ; private final HttpStatus status; private final String message; 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 deleted file mode 100644 index 6085c2c03..000000000 --- a/src/main/java/com/gdschongik/gdsc/infra/client/github/GithubClient.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.gdschongik.gdsc.infra.client.github; - -import com.gdschongik.gdsc.global.exception.CustomException; -import com.gdschongik.gdsc.global.exception.ErrorCode; -import java.io.IOException; -import lombok.RequiredArgsConstructor; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.GitHub; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class GithubClient { - - private final GitHub github; - - public GHRepository getRepository(String ownerRepo) { - try { - return github.getRepository(ownerRepo); - } catch (IOException e) { - throw new CustomException(ErrorCode.GITHUB_REPOSITORY_NOT_FOUND); - } - } -} diff --git a/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java b/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java new file mode 100644 index 000000000..d5b94a740 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java @@ -0,0 +1,59 @@ +package com.gdschongik.gdsc.infra.github.client; + +import static com.gdschongik.gdsc.global.common.constant.GithubConstant.*; + +import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.exception.ErrorCode; +import com.gdschongik.gdsc.infra.github.dto.response.GithubAssignmentSubmissionResponse; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; +import lombok.RequiredArgsConstructor; +import org.kohsuke.github.GHCommit; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class GithubClient { + + private final GitHub github; + + public GHRepository getRepository(String ownerRepo) { + try { + return github.getRepository(ownerRepo); + } catch (IOException e) { + throw new CustomException(ErrorCode.GITHUB_REPOSITORY_NOT_FOUND); + } + } + + public GithubAssignmentSubmissionResponse getLatestAssignmentSubmission(String repo, int week) { + try { + GHRepository ghRepository = getRepository(repo); + String assignmentPath = GITHUB_ASSIGNMENT_PATH.formatted(week); + + // GHContent#getSize() 의 경우 한글 문자열을 byte 단위로 계산하기 때문에, 직접 content를 읽어서 길이를 계산 + GHContent ghContent = ghRepository.getFileContent(assignmentPath); + String content = new String(ghContent.read().readAllBytes()); + + GHCommit ghLatestCommit = ghRepository + .queryCommits() + .path(assignmentPath) + .list() + .toList() + .get(0); + + LocalDateTime committedAt = ghLatestCommit + .getCommitDate() + .toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + + return new GithubAssignmentSubmissionResponse(ghLatestCommit.getSHA1(), content.length(), committedAt); + } catch (IOException e) { + throw new CustomException(ErrorCode.GITHUB_ASSIGNMENT_NOT_FOUND); + } + } +} diff --git a/src/main/java/com/gdschongik/gdsc/infra/github/dto/response/GithubAssignmentSubmissionResponse.java b/src/main/java/com/gdschongik/gdsc/infra/github/dto/response/GithubAssignmentSubmissionResponse.java new file mode 100644 index 000000000..da64c9994 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/infra/github/dto/response/GithubAssignmentSubmissionResponse.java @@ -0,0 +1,5 @@ +package com.gdschongik.gdsc.infra.github.dto.response; + +import java.time.LocalDateTime; + +public record GithubAssignmentSubmissionResponse(String commitHash, Integer size, LocalDateTime committedAt) {} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index f86a1d3c2..32b629bc1 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -17,3 +17,4 @@ logging: level: org.springframework.orm.jpa: DEBUG org.springframework.transaction: DEBUG + org.kohsuke.github: debug From 9f905710453826a77c5edebf204433cd0647b011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=8A=AC=EA=B8=B0?= <84620016+seulgi99@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:59:36 +0900 Subject: [PATCH 11/40] =?UTF-8?q?refactor:=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A4=EC=8B=9C=20=EC=A0=9C=EB=AA=A9=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#632)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 스터디 개설시 제목필드 추가 및 스터디 시간 초 데이터 제거 * refactor: 스터디 시간, 분 범위 추가 * refactor: 스터디 개설 시작,종료시간 LocalTime으로 롤백 * refactor: implement제거 --- .../gdschongik/gdsc/domain/study/domain/Study.java | 4 ++++ .../domain/study/dto/request/StudyCreateRequest.java | 12 ++++++------ .../domain/study/factory/StudyDomainFactory.java | 1 + .../gdsc/domain/study/domain/StudyTest.java | 5 +++++ .../gdsc/global/common/constant/StudyConstant.java | 1 + .../com/gdschongik/gdsc/helper/FixtureHelper.java | 1 + .../com/gdschongik/gdsc/helper/IntegrationTest.java | 1 + 7 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java index 0d6639b31..8ea68eeb8 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java @@ -80,6 +80,7 @@ public class Study extends BaseSemesterEntity { private Study( Integer academicYear, SemesterType semesterType, + String title, Member mentor, Period period, Period applicationPeriod, @@ -89,6 +90,7 @@ private Study( LocalTime startTime, LocalTime endTime) { super(academicYear, semesterType); + this.title = title; this.mentor = mentor; this.period = period; this.applicationPeriod = applicationPeriod; @@ -102,6 +104,7 @@ private Study( public static Study createStudy( Integer academicYear, SemesterType semesterType, + String title, Member mentor, Period period, Period applicationPeriod, @@ -116,6 +119,7 @@ public static Study createStudy( return Study.builder() .academicYear(academicYear) .semesterType(semesterType) + .title(title) .mentor(mentor) .period(period) .applicationPeriod(applicationPeriod) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyCreateRequest.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyCreateRequest.java index cb5153e3c..a6e859f48 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyCreateRequest.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyCreateRequest.java @@ -6,6 +6,7 @@ import com.gdschongik.gdsc.domain.study.domain.StudyType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import java.time.DayOfWeek; @@ -17,6 +18,7 @@ public record StudyCreateRequest( @NotNull(message = "학년도는 null이 될 수 없습니다.") @Schema(description = "학년도", pattern = ACADEMIC_YEAR) Integer academicYear, @NotNull(message = "학기는 null이 될 수 없습니다.") @Schema(description = "학기") SemesterType semesterType, + @NotBlank(message = "스터디 제목을 입력해 주세요.") @Schema(description = "제목") String title, @NotNull(message = "신청기간 시작일은 null이 될 수 없습니다.") @Schema(description = "신청기간 시작일", pattern = DATE) LocalDate applicationStartDate, @Future @NotNull(message = "신청기간 종료일은 null이 될 수 없습니다.") @Schema(description = "신청기간 종료일", pattern = DATE) @@ -24,9 +26,7 @@ public record StudyCreateRequest( @Positive @NotNull(message = "총 주차수는 null이 될 수 없습니다.") @Schema(description = "총 주차수") Long totalWeek, @Future @NotNull(message = "스터디 시작일은 null이 될 수 없습니다.") @Schema(description = "스터디 시작일", pattern = DATE) LocalDate startDate, - @NotNull(message = "스터디 요일은 null이 될 수 없습니다.") @Schema(description = "스터디 요일", implementation = DayOfWeek.class) - DayOfWeek dayOfWeek, - @NotNull @Schema(description = "스터디 시작 시간", implementation = LocalTime.class) LocalTime studyStartTime, - @NotNull @Schema(description = "스터디 종료 시간", implementation = LocalTime.class) LocalTime studyEndTime, - @NotNull(message = "스터디 타입은 null이 될 수 없습니다.") @Schema(description = "스터디 타입", implementation = StudyType.class) - StudyType studyType) {} + @NotNull(message = "스터디 요일은 null이 될 수 없습니다.") @Schema(description = "스터디 요일") DayOfWeek dayOfWeek, + @NotNull @Schema(description = "스터디 시작 시간") LocalTime studyStartTime, + @NotNull @Schema(description = "스터디 종료 시간") LocalTime studyEndTime, + @NotNull(message = "스터디 타입은 null이 될 수 없습니다.") @Schema(description = "스터디 타입") StudyType studyType) {} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/factory/StudyDomainFactory.java b/src/main/java/com/gdschongik/gdsc/domain/study/factory/StudyDomainFactory.java index 3c45acb51..a8a610410 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/factory/StudyDomainFactory.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/factory/StudyDomainFactory.java @@ -20,6 +20,7 @@ public Study createNewStudy(StudyCreateRequest request, Member mentor) { return Study.createStudy( request.academicYear(), request.semesterType(), + request.title(), mentor, Period.createPeriod(request.startDate().atStartOfDay(), endDate.atTime(LocalTime.MAX)), Period.createPeriod( diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyTest.java index 131a58783..bd254bbaa 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyTest.java @@ -43,6 +43,7 @@ class 스터디_개설시 { assertThatThrownBy(() -> Study.createStudy( ACADEMIC_YEAR, SEMESTER_TYPE, + STUDY_TITLE, guestMember, period, applicationPeriod, @@ -66,6 +67,7 @@ class 스터디_개설시 { assertThatThrownBy(() -> Study.createStudy( ACADEMIC_YEAR, SEMESTER_TYPE, + STUDY_TITLE, member, period, applicationPeriod, @@ -89,6 +91,7 @@ class 스터디_개설시 { assertThatThrownBy(() -> Study.createStudy( ACADEMIC_YEAR, SEMESTER_TYPE, + STUDY_TITLE, member, period, applicationPeriod, @@ -114,6 +117,7 @@ class 스터디_개설시 { assertThatThrownBy(() -> Study.createStudy( ACADEMIC_YEAR, SEMESTER_TYPE, + STUDY_TITLE, member, period, applicationPeriod, @@ -139,6 +143,7 @@ class 스터디_개설시 { assertThatThrownBy(() -> Study.createStudy( ACADEMIC_YEAR, SEMESTER_TYPE, + STUDY_TITLE, member, period, applicationPeriod, 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 b9839811b..71c19f31f 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 @@ -7,6 +7,7 @@ public class StudyConstant { private StudyConstant() {} + public static final String STUDY_TITLE = "스터디 제목"; public static final Long TOTAL_WEEK = 8L; public static final StudyType ONLINE_STUDY = StudyType.ONLINE; public static final StudyType ASSIGNMENT_STUDY = StudyType.ASSIGNMENT; diff --git a/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java b/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java index f6a4c1cdf..a3b308835 100644 --- a/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java +++ b/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java @@ -71,6 +71,7 @@ public Study createStudy(Member mentor, Period period, Period applicationPeriod) return Study.createStudy( ACADEMIC_YEAR, SEMESTER_TYPE, + STUDY_TITLE, mentor, period, applicationPeriod, diff --git a/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java b/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java index 3401a349a..92f63c521 100644 --- a/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java +++ b/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java @@ -210,6 +210,7 @@ protected Study createStudy(Member mentor, Period period, Period applicationPeri Study study = Study.createStudy( ACADEMIC_YEAR, SEMESTER_TYPE, + STUDY_TITLE, mentor, period, applicationPeriod, From 21b32085cd39a59eba1fe769add2666481200899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=8A=AC=EA=B8=B0?= <84620016+seulgi99@users.noreply.github.com> Date: Fri, 16 Aug 2024 19:08:37 +0900 Subject: [PATCH 12/40] =?UTF-8?q?feat:=20=EC=B6=9C=EC=84=9D=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20API=20=EC=B6=94=EA=B0=80=20(#622)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 출석체크 API 추가 * refactor: spotless적용 * refactor: 오타수정 * refactor: 사용하지않는 상수 제거 * refactor: 출석하기 메소드명 및 mentee에서 student로 필드명 변경 * refactor: 출석체크 save후 재할당 코드 제거 * refactor: 출석체크 일자 유효성 검증 실패 메시지 변경 * refactor: 스터디 상세에 출석일자 구하는 함수 추가 * refactor: 출석체크 API 경로 변경 --- .../domain/study/api/StudyController.java | 11 ++++ .../study/application/StudyService.java | 28 ++++++++ .../study/dao/AttendanceRepository.java | 6 ++ .../gdsc/domain/study/domain/Attendance.java | 48 ++++++++++++++ .../study/domain/AttendanceValidator.java | 23 +++++++ .../gdsc/domain/study/domain/Study.java | 5 ++ .../gdsc/domain/study/domain/StudyDetail.java | 9 +++ .../domain/study/domain/StudyHistory.java | 3 + .../study/dto/request/StudyAttendRequest.java | 13 ++++ .../global/common/constant/RegexConstant.java | 2 + .../gdsc/global/exception/ErrorCode.java | 4 ++ .../study/domain/AttendanceValidatorTest.java | 64 +++++++++++++++++++ 12 files changed, 216 insertions(+) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/domain/Attendance.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidator.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyAttendRequest.java create mode 100644 src/test/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidatorTest.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudyController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudyController.java index 79e46d71d..b490e55bb 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudyController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudyController.java @@ -1,9 +1,11 @@ package com.gdschongik.gdsc.domain.study.api; import com.gdschongik.gdsc.domain.study.application.StudyService; +import com.gdschongik.gdsc.domain.study.dto.request.StudyAttendRequest; import com.gdschongik.gdsc.domain.study.dto.response.StudyResponse; 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; @@ -11,6 +13,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -42,4 +45,12 @@ public ResponseEntity cancelStudyApply(@PathVariable Long studyId) { studyService.cancelStudyApply(studyId); return ResponseEntity.noContent().build(); } + + @Operation(summary = "스터디 출석체크", description = "스터디에 출석체크합니다. 현재 진행중인 스터디 회차에 출석체크해야 하며, 중복출석체크할 수 없습니다.") + @PostMapping("/study-details/{studyDetailId}/attend") + public ResponseEntity attend( + @PathVariable Long studyDetailId, @Valid @RequestBody StudyAttendRequest request) { + studyService.attend(studyDetailId, request); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyService.java index 4bbe85b04..f82634d2b 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyService.java @@ -3,12 +3,18 @@ import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.study.dao.AttendanceRepository; +import com.gdschongik.gdsc.domain.study.dao.StudyDetailRepository; import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository; import com.gdschongik.gdsc.domain.study.dao.StudyRepository; import com.gdschongik.gdsc.domain.study.domain.*; +import com.gdschongik.gdsc.domain.study.domain.Attendance; +import com.gdschongik.gdsc.domain.study.domain.AttendanceValidator; +import com.gdschongik.gdsc.domain.study.dto.request.StudyAttendRequest; import com.gdschongik.gdsc.domain.study.dto.response.StudyResponse; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.util.MemberUtil; +import java.time.LocalDate; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -23,8 +29,11 @@ public class StudyService { private final MemberUtil memberUtil; private final StudyRepository studyRepository; + private final StudyDetailRepository studyDetailRepository; private final StudyHistoryRepository studyHistoryRepository; private final StudyHistoryValidator studyHistoryValidator; + private final AttendanceRepository attendanceRepository; + private final AttendanceValidator attendanceValidator; public List getAllApplicableStudies() { return studyRepository.findAll().stream() @@ -62,4 +71,23 @@ public void cancelStudyApply(Long studyId) { log.info("[StudyService] 스터디 수강신청 취소: studyId={}, memberId={}", study.getId(), currentMember.getId()); } + + @Transactional + public void attend(Long studyDetailId, StudyAttendRequest request) { + final StudyDetail studyDetail = studyDetailRepository + .findById(studyDetailId) + .orElseThrow(() -> new CustomException(STUDY_DETAIL_NOT_FOUND)); + final Member currentMember = memberUtil.getCurrentMember(); + final Study study = studyDetail.getStudy(); + final StudyHistory studyHistory = studyHistoryRepository + .findByMenteeAndStudy(currentMember, study) + .orElseThrow(() -> new CustomException(STUDY_HISTORY_NOT_FOUND)); + + attendanceValidator.validateAttendance(studyDetail, request.attendanceNumber(), LocalDate.now()); + + Attendance attendance = Attendance.create(currentMember, studyDetail); + attendanceRepository.save(attendance); + + log.info("[StudyService] 스터디 출석: attendanceId={}", attendance.getId()); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java new file mode 100644 index 000000000..1b31a4429 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java @@ -0,0 +1,6 @@ +package com.gdschongik.gdsc.domain.study.dao; + +import com.gdschongik.gdsc.domain.study.domain.Attendance; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AttendanceRepository extends JpaRepository {} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/Attendance.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/Attendance.java new file mode 100644 index 000000000..7556fd8be --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/Attendance.java @@ -0,0 +1,48 @@ +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.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"member_id", "study_detail_id"})}) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Attendance extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "attendance_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member student; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "study_detail_id") + private StudyDetail studyDetail; + + @Builder(access = AccessLevel.PRIVATE) + private Attendance(Member student, StudyDetail studyDetail) { + this.student = student; + this.studyDetail = studyDetail; + } + + public static Attendance create(Member student, StudyDetail studyDetail) { + return Attendance.builder().student(student).studyDetail(studyDetail).build(); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidator.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidator.java new file mode 100644 index 000000000..2637fd41a --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidator.java @@ -0,0 +1,23 @@ +package com.gdschongik.gdsc.domain.study.domain; + +import static com.gdschongik.gdsc.global.exception.ErrorCode.*; + +import com.gdschongik.gdsc.global.annotation.DomainService; +import com.gdschongik.gdsc.global.exception.CustomException; +import java.time.LocalDate; + +@DomainService +public class AttendanceValidator { + public void validateAttendance(StudyDetail studyDetail, String attendanceNumber, LocalDate date) { + // 출석체크 날짜 검증 + LocalDate attendanceDay = studyDetail.getAttendanceDay(); + if (!attendanceDay.equals(date)) { + throw new CustomException(ATTENDANCE_DATE_INVALID); + } + + // 출석체크 번호 검증 + if (!studyDetail.getAttendanceNumber().equals(attendanceNumber)) { + throw new CustomException(ATTENDANCE_NUMBER_MISMATCH); + } + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java index 8ea68eeb8..373e0c306 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java @@ -21,6 +21,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import java.time.DayOfWeek; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import lombok.AccessLevel; @@ -175,4 +176,8 @@ public boolean isApplicable() { public boolean isStudyOngoing() { return period.isOpen(); } + + public LocalDate getStartDate() { + return period.getStartDate().toLocalDate(); + } } 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 6057fa5ea..8ab50cfb2 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.LocalDate; import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; @@ -82,4 +83,12 @@ public void publishAssignment(String title, LocalDateTime deadLine, String descr public void updateAssignment(String title, LocalDateTime deadLine, String descriptionNotionLink) { assignment = Assignment.generateAssignment(title, deadLine, descriptionNotionLink); } + + // 스터디 시작일자 + 현재 주차 * 7 + (스터디 요일 - 스터디 기간 시작 요일) + public LocalDate getAttendanceDay() { + return study.getStartDate() + .plusDays(week * 7 + + study.getDayOfWeek().getValue() + - study.getStartDate().getDayOfWeek().getValue()); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java index 52689f528..bd1a06b2a 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java @@ -10,6 +10,8 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -18,6 +20,7 @@ @Getter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"member_id", "study_id"})}) public class StudyHistory extends BaseEntity { @Id diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyAttendRequest.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyAttendRequest.java new file mode 100644 index 000000000..051b1d5a9 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyAttendRequest.java @@ -0,0 +1,13 @@ +package com.gdschongik.gdsc.domain.study.dto.request; + +import static com.gdschongik.gdsc.global.common.constant.RegexConstant.ATTENDANCE_NUMBER; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +public record StudyAttendRequest( + @NotBlank + @Pattern(regexp = ATTENDANCE_NUMBER, message = "출석번호는 " + ATTENDANCE_NUMBER + " 형식이어야 합니다.") + @Schema(description = "출석번호") + String attendanceNumber) {} diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/RegexConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/RegexConstant.java index 6e7d42c90..afc38dc0b 100644 --- a/src/main/java/com/gdschongik/gdsc/global/common/constant/RegexConstant.java +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/RegexConstant.java @@ -12,5 +12,7 @@ public class RegexConstant { public static final String DATE = "yyyy-MM-dd"; public static final String ACADEMIC_YEAR = "^[0-9]{4}$"; + public static final String ATTENDANCE_NUMBER = "^[0-9]{4}$"; + private RegexConstant() {} } 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 9940bb24e..802e9cf8f 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -121,6 +121,10 @@ public enum ErrorCode { HttpStatus.CONFLICT, "이미 제출한 과제가 있으므로 레포지토리를 수정할 수 없습니다."), STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_OWNER_MISMATCH(HttpStatus.CONFLICT, "레포지토리 소유자가 현재 멤버와 다릅니다."), + // Attendance + ATTENDANCE_DATE_INVALID(HttpStatus.CONFLICT, "강의일이 아니면 출석체크할 수 없습니다."), + ATTENDANCE_NUMBER_MISMATCH(HttpStatus.CONFLICT, "출석번호가 일치하지 않습니다."), + // Order ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "주문이 존재하지 않습니다."), ORDER_MEMBERSHIP_MEMBER_MISMATCH(HttpStatus.CONFLICT, "주문 대상 멤버십의 멤버와 현재 로그인한 멤버가 일치하지 않습니다."), diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidatorTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidatorTest.java new file mode 100644 index 000000000..b39091045 --- /dev/null +++ b/src/test/java/com/gdschongik/gdsc/domain/study/domain/AttendanceValidatorTest.java @@ -0,0 +1,64 @@ +package com.gdschongik.gdsc.domain.study.domain; + +import static com.gdschongik.gdsc.global.common.constant.StudyConstant.ATTENDANCE_NUMBER; +import static com.gdschongik.gdsc.global.exception.ErrorCode.ATTENDANCE_DATE_INVALID; +import static com.gdschongik.gdsc.global.exception.ErrorCode.ATTENDANCE_NUMBER_MISMATCH; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +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.LocalDate; +import java.time.LocalDateTime; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class AttendanceValidatorTest { + FixtureHelper fixtureHelper = new FixtureHelper(); + AttendanceValidator attendanceValidator = new AttendanceValidator(); + + @Nested + class 스터디_출석체크시 { + + @Test + void 출석일자가_아니면_실패한다() { + // given + Member mentor = fixtureHelper.createAssociateMember(1L); + + LocalDateTime now = LocalDateTime.now(); + Period period = Period.createPeriod(now.plusDays(10), now.plusDays(65)); + Period applicationPeriod = Period.createPeriod(now.minusDays(10), now.plusDays(5)); + Study study = fixtureHelper.createStudy(mentor, period, applicationPeriod); + StudyDetail studyDetail = fixtureHelper.createStudyDetail(study, now, now.plusDays(7)); + + LocalDate attendanceDay = studyDetail.getAttendanceDay(); + + // when & then + assertThatThrownBy(() -> attendanceValidator.validateAttendance( + studyDetail, ATTENDANCE_NUMBER, attendanceDay.plusDays(1))) + .isInstanceOf(CustomException.class) + .hasMessage(ATTENDANCE_DATE_INVALID.getMessage()); + } + + @Test + void 출석번호가_다르면_실패한다() { + // given + Member mentor = fixtureHelper.createAssociateMember(1L); + + LocalDateTime now = LocalDateTime.now(); + Period period = Period.createPeriod(now.plusDays(10), now.plusDays(65)); + Period applicationPeriod = Period.createPeriod(now.minusDays(10), now.plusDays(5)); + Study study = fixtureHelper.createStudy(mentor, period, applicationPeriod); + StudyDetail studyDetail = fixtureHelper.createStudyDetail(study, now, now.plusDays(7)); + + LocalDate attendanceDay = studyDetail.getAttendanceDay(); + + // when & then + assertThatThrownBy(() -> + attendanceValidator.validateAttendance(studyDetail, ATTENDANCE_NUMBER + 1, attendanceDay)) + .isInstanceOf(CustomException.class) + .hasMessage(ATTENDANCE_NUMBER_MISMATCH.getMessage()); + } + } +} From f60a45711e6929e96b133afbf4d87f5e20c051b8 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:26:43 +0900 Subject: [PATCH 13/40] =?UTF-8?q?feat:=20=EA=B3=BC=EC=A0=9C=20=EC=A0=9C?= =?UTF-8?q?=EC=B6=9C=ED=95=98=EA=B8=B0=201=EC=B0=A8=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#635)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 스터디상세에 과제 제출가능여부 검증 메서드 추가 * feat: 멤버 및 스터디상세로 과제이력 조회 메서드 추가 * feat: 빈 과제이력 생성 로직 변경 * feat: 빈 과제이력 생성 검증로직 구현 * feat: 과제 제출 검증로직 구현 * feat: 과제 제출 및 검증 로직 구현 * refactor: 수강신청 여부를 boolean으로 변환 및 검증기에서 검증 * refactor: 과제 제출 검증 메서드 이름 변경 * docs: 투두 추가 * style: 개행 변경 * test: 스터디 관련 시간 상수 추가 * test: 과제와 함께 스터디상세 생성하는 픽스처 추가 * test: 과제 히스토리 검증기 관련 테스트 추가 * feat: 과제 시작기간 이전에 제출하는 경우 검증 * test: 과제 시작기간 이전 제출 검증 테스트 추가 * test: 테스트 이름 변경 * refactor: 빈 과제 히스토리 생성 시에는 검증 수행하지 않도록 변경 * test: 테스트가 잘못되어 있던 문제 수정 * refactor: 에러코드 이름 변경 * refactor: 에러코드 이름 변경 반영 * refactor: 시작시간 이전 제출 검증로직을 스터디상세 엔티티 내부로 이동 * refactor: 멘토와 함께 스터디 생성하는 픽스처 로직 변경 * refactor: 필요한 인자만을 받도록 수정 --- .../StudentStudyHistoryService.java | 31 ++++++ .../dao/AssignmentHistoryRepository.java | 7 +- .../study/dao/StudyHistoryRepository.java | 2 + .../study/domain/AssignmentHistory.java | 6 +- .../StudyAssignmentHistoryValidator.java | 22 +++++ .../gdsc/domain/study/domain/StudyDetail.java | 10 ++ .../domain/study/domain/vo/Assignment.java | 15 +++ .../gdsc/global/exception/ErrorCode.java | 5 + .../StudyAssignmentHistoryValidatorTest.java | 95 +++++++++++++++++++ .../global/common/constant/StudyConstant.java | 10 ++ .../gdschongik/gdsc/helper/FixtureHelper.java | 13 ++- 11 files changed, 209 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyAssignmentHistoryValidator.java create mode 100644 src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyAssignmentHistoryValidatorTest.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java index ca87ab75c..3712640ac 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java @@ -5,8 +5,12 @@ import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.study.dao.AssignmentHistoryRepository; +import com.gdschongik.gdsc.domain.study.dao.StudyDetailRepository; import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository; +import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; import com.gdschongik.gdsc.domain.study.domain.Study; +import com.gdschongik.gdsc.domain.study.domain.StudyAssignmentHistoryValidator; +import com.gdschongik.gdsc.domain.study.domain.StudyDetail; import com.gdschongik.gdsc.domain.study.domain.StudyHistory; import com.gdschongik.gdsc.domain.study.domain.StudyHistoryValidator; import com.gdschongik.gdsc.domain.study.dto.request.RepositoryUpdateRequest; @@ -15,6 +19,7 @@ import com.gdschongik.gdsc.global.util.MemberUtil; import com.gdschongik.gdsc.infra.github.client.GithubClient; import java.io.IOException; +import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -29,9 +34,11 @@ public class StudentStudyHistoryService { private final MemberUtil memberUtil; private final GithubClient githubClient; + private final StudyDetailRepository studyDetailRepository; private final StudyHistoryRepository studyHistoryRepository; private final AssignmentHistoryRepository assignmentHistoryRepository; private final StudyHistoryValidator studyHistoryValidator; + private final StudyAssignmentHistoryValidator studyAssignmentHistoryValidator; @Transactional public void updateRepository(Long studyHistoryId, RepositoryUpdateRequest request) throws IOException { @@ -71,4 +78,28 @@ public List getAllAssignmentHistories(Long studyId) { .map(AssignmentHistoryResponse::from) .toList(); } + + @Transactional + public void submitAssignment(Long studyDetailId) { + Member currentMember = memberUtil.getCurrentMember(); + StudyDetail studyDetail = studyDetailRepository + .findById(studyDetailId) + .orElseThrow(() -> new CustomException(STUDY_DETAIL_NOT_FOUND)); + boolean isAppliedToStudy = studyHistoryRepository.existsByMenteeAndStudy(currentMember, studyDetail.getStudy()); + LocalDateTime now = LocalDateTime.now(); + + AssignmentHistory assignmentHistory = findOrCreate(currentMember, studyDetail); + + studyAssignmentHistoryValidator.validateSubmitAvailable(isAppliedToStudy, now, studyDetail); + + // TODO: 과제 채점 및 과제이력 업데이트 로직 추가 + + assignmentHistoryRepository.save(assignmentHistory); + } + + private AssignmentHistory findOrCreate(Member currentMember, StudyDetail studyDetail) { + return assignmentHistoryRepository + .findByMemberAndStudyDetail(currentMember, studyDetail) + .orElseGet(() -> AssignmentHistory.create(studyDetail, currentMember)); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryRepository.java index 89ed0c714..1c882f5ee 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryRepository.java @@ -1,7 +1,12 @@ package com.gdschongik.gdsc.domain.study.dao; +import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; +import com.gdschongik.gdsc.domain.study.domain.StudyDetail; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface AssignmentHistoryRepository - extends JpaRepository, AssignmentHistoryCustomRepository {} + extends JpaRepository, AssignmentHistoryCustomRepository { + public Optional findByMemberAndStudyDetail(Member member, StudyDetail studyDetail); +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java index a45f62e4a..f597aee42 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java @@ -13,4 +13,6 @@ public interface StudyHistoryRepository extends JpaRepository findAllByMentee(Member member); Optional findByMenteeAndStudy(Member member, Study study); + + boolean existsByMenteeAndStudy(Member member, Study study); } 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 index e340b458e..8d5f651cf 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistory.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistory.java @@ -64,14 +64,10 @@ private AssignmentHistory( this.submissionStatus = submissionStatus; } - public static AssignmentHistory create( - StudyDetail studyDetail, Member member, String submissionLink, String commitHash, Long contentLength) { + public static AssignmentHistory create(StudyDetail studyDetail, Member member) { 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/StudyAssignmentHistoryValidator.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyAssignmentHistoryValidator.java new file mode 100644 index 000000000..7bda5b7ff --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyAssignmentHistoryValidator.java @@ -0,0 +1,22 @@ +package com.gdschongik.gdsc.domain.study.domain; + +import static com.gdschongik.gdsc.global.exception.ErrorCode.*; + +import com.gdschongik.gdsc.global.annotation.DomainService; +import com.gdschongik.gdsc.global.exception.CustomException; +import java.time.LocalDateTime; + +@DomainService +public class StudyAssignmentHistoryValidator { + + /** + * 채점을 수행하기 전, 과제 제출이 가능한지 검증합니다. + */ + public void validateSubmitAvailable(boolean isAppliedToStudy, LocalDateTime now, StudyDetail studyDetail) { + if (!isAppliedToStudy) { + throw new CustomException(ASSIGNMENT_STUDY_NOT_APPLIED); + } + + studyDetail.validateAssignmentSubmittable(now); + } +} 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 8ab50cfb2..a6630ddc0 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 @@ -1,9 +1,12 @@ package com.gdschongik.gdsc.domain.study.domain; +import static com.gdschongik.gdsc.global.exception.ErrorCode.*; + import com.gdschongik.gdsc.domain.common.model.BaseEntity; import com.gdschongik.gdsc.domain.recruitment.domain.vo.Period; import com.gdschongik.gdsc.domain.study.domain.vo.Assignment; import com.gdschongik.gdsc.domain.study.domain.vo.Session; +import com.gdschongik.gdsc.global.exception.CustomException; import jakarta.persistence.*; import java.time.LocalDate; import java.time.LocalDateTime; @@ -91,4 +94,11 @@ public LocalDate getAttendanceDay() { + study.getDayOfWeek().getValue() - study.getStartDate().getDayOfWeek().getValue()); } + + public void validateAssignmentSubmittable(LocalDateTime now) { + if (now.isBefore(period.getStartDate())) { + throw new CustomException(ASSIGNMENT_SUBMIT_NOT_STARTED); + } + assignment.validateSubmittable(now); + } } 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 ea480bed8..699820114 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 @@ -6,6 +6,7 @@ import com.gdschongik.gdsc.domain.study.domain.Difficulty; import com.gdschongik.gdsc.domain.study.domain.StudyStatus; +import com.gdschongik.gdsc.global.exception.CustomException; import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import jakarta.persistence.EnumType; @@ -65,4 +66,18 @@ public static Assignment generateAssignment(String title, LocalDateTime deadline .status(StudyStatus.OPEN) .build(); } + + public void validateSubmittable(LocalDateTime now) { + if (status == NONE) { + throw new CustomException(ASSIGNMENT_SUBMIT_NOT_PUBLISHED); + } + + if (status == CANCELLED) { + throw new CustomException(ASSIGNMENT_SUBMIT_CANCELLED); + } + + if (now.isAfter(deadline)) { + throw new CustomException(ASSIGNMENT_SUBMIT_DEADLINE_PASSED); + } + } } 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 802e9cf8f..1ec0744ef 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -150,6 +150,11 @@ public enum ErrorCode { // Assignment ASSIGNMENT_CAN_NOT_BE_UPDATED(HttpStatus.CONFLICT, "휴강인 과제는 수정할 수 없습니다."), ASSIGNMENT_DEADLINE_INVALID(HttpStatus.CONFLICT, "과제 마감 기한이 현재보다 빠릅니다."), + ASSIGNMENT_STUDY_NOT_APPLIED(HttpStatus.CONFLICT, "해당 스터디에 대한 수강신청 기록이 존재하지 않습니다."), + ASSIGNMENT_SUBMIT_NOT_STARTED(HttpStatus.CONFLICT, "아직 과제가 시작되지 않았습니다."), + ASSIGNMENT_SUBMIT_NOT_PUBLISHED(HttpStatus.CONFLICT, "아직 과제가 등록되지 않았습니다."), + ASSIGNMENT_SUBMIT_CANCELLED(HttpStatus.CONFLICT, "과제 휴강 주간에는 과제를 제출할 수 없습니다."), + ASSIGNMENT_SUBMIT_DEADLINE_PASSED(HttpStatus.CONFLICT, "과제 마감 기한이 지났습니다."), // Github GITHUB_REPOSITORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 레포지토리입니다."), diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyAssignmentHistoryValidatorTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyAssignmentHistoryValidatorTest.java new file mode 100644 index 000000000..5af2b9699 --- /dev/null +++ b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyAssignmentHistoryValidatorTest.java @@ -0,0 +1,95 @@ +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.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; + +class StudyAssignmentHistoryValidatorTest { + + private final FixtureHelper fixtureHelper = new FixtureHelper(); + private final StudyAssignmentHistoryValidator validator = new StudyAssignmentHistoryValidator(); + + // FixtureHelper 래핑 메서드 + private Member createMember(Long id) { + return fixtureHelper.createAssociateMember(id); + } + + private Study createStudyWithMentor(Long mentorId) { + Period period = Period.createPeriod(STUDY_START_DATETIME, STUDY_END_DATETIME); + Period applicationPeriod = + Period.createPeriod(STUDY_START_DATETIME.minusDays(7), STUDY_START_DATETIME.minusDays(1)); + return fixtureHelper.createStudyWithMentor(mentorId, period, applicationPeriod); + } + + private StudyDetail createStudyDetailWithAssignment(Study study) { + return fixtureHelper.createStudyDetailWithAssignment( + study, STUDY_DETAIL_START_DATETIME, STUDY_DETAIL_END_DATETIME, STUDY_ASSIGNMENT_DEADLINE_DATETIME); + } + + @Nested + class 과제_제출가능_여부_검증할때 { + + @Test + void 스터디_수강신청_기록이_없다면_실패한다() { + // given + Study study = createStudyWithMentor(1L); + StudyDetail studyDetail = createStudyDetailWithAssignment(study); + boolean isAppliedToStudy = false; + + // when & then + assertThatThrownBy(() -> validator.validateSubmitAvailable( + isAppliedToStudy, STUDY_DETAIL_START_DATETIME, studyDetail)) + .isInstanceOf(CustomException.class) + .hasMessage(ASSIGNMENT_STUDY_NOT_APPLIED.getMessage()); + } + + @Test + void 과제가_시작되지_않았다면_실패한다() { + // given + Study study = createStudyWithMentor(1L); + StudyDetail studyDetail = createStudyDetailWithAssignment(study); + boolean isAppliedToStudy = true; + LocalDateTime beforeStart = STUDY_DETAIL_START_DATETIME.minusDays(1); + + // when & then + assertThatThrownBy(() -> validator.validateSubmitAvailable(isAppliedToStudy, beforeStart, studyDetail)) + .isInstanceOf(CustomException.class) + .hasMessage(ASSIGNMENT_SUBMIT_NOT_STARTED.getMessage()); + } + + @Test + void 과제_마감기한이_지났다면_실패한다() { + // given + Study study = createStudyWithMentor(1L); + StudyDetail studyDetail = createStudyDetailWithAssignment(study); + boolean isAppliedToStudy = true; + LocalDateTime afterDeadline = STUDY_ASSIGNMENT_DEADLINE_DATETIME.plusDays(1); + + // when & then + assertThatThrownBy(() -> validator.validateSubmitAvailable(isAppliedToStudy, afterDeadline, studyDetail)) + .isInstanceOf(CustomException.class) + .hasMessage(ASSIGNMENT_SUBMIT_DEADLINE_PASSED.getMessage()); + } + + @Test + void 모든_조건을_만족하는_경우_성공한다() { + // given + Study study = createStudyWithMentor(1L); + StudyDetail studyDetail = createStudyDetailWithAssignment(study); + boolean isAppliedToStudy = true; + + // when & then + assertThatCode(() -> validator.validateSubmitAvailable( + isAppliedToStudy, STUDY_DETAIL_START_DATETIME, studyDetail)) + .doesNotThrowAnyException(); + } + } +} 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 71c19f31f..0e9e26afc 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 @@ -2,6 +2,7 @@ import com.gdschongik.gdsc.domain.study.domain.StudyType; import java.time.DayOfWeek; +import java.time.LocalDateTime; import java.time.LocalTime; public class StudyConstant { @@ -21,4 +22,13 @@ private StudyConstant() {} // Assignment public static final String ASSIGNMENT_TITLE = "testTitle"; public static final String DESCRIPTION_LINK = "www.link.com"; + + // Study (2024-09-01 ~ 2024-10-27) + public static final LocalDateTime STUDY_START_DATETIME = LocalDateTime.of(2024, 9, 1, 0, 0); + public static final LocalDateTime STUDY_END_DATETIME = STUDY_START_DATETIME.plusWeeks(8); + + // StudyDetail (1주차: 2024-09-01 ~ 2024-09-08) + public static final LocalDateTime STUDY_DETAIL_START_DATETIME = STUDY_START_DATETIME; + public static final LocalDateTime STUDY_DETAIL_END_DATETIME = STUDY_DETAIL_START_DATETIME.plusWeeks(1); + public static final LocalDateTime STUDY_ASSIGNMENT_DEADLINE_DATETIME = STUDY_DETAIL_END_DATETIME; } diff --git a/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java b/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java index a3b308835..eb5706461 100644 --- a/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java +++ b/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java @@ -1,7 +1,6 @@ package com.gdschongik.gdsc.helper; import static com.gdschongik.gdsc.domain.member.domain.Department.*; -import static com.gdschongik.gdsc.domain.member.domain.Member.*; 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.*; @@ -82,7 +81,19 @@ public Study createStudy(Member mentor, Period period, Period applicationPeriod) STUDY_END_TIME); } + public Study createStudyWithMentor(Long mentorId, Period period, Period applicationPeriod) { + Member mentor = createAssociateMember(mentorId); + return createStudy(mentor, period, applicationPeriod); + } + public StudyDetail createStudyDetail(Study study, LocalDateTime startDate, LocalDateTime endDate) { return StudyDetail.createStudyDetail(study, 1L, ATTENDANCE_NUMBER, Period.createPeriod(startDate, endDate)); } + + public StudyDetail createStudyDetailWithAssignment( + Study study, LocalDateTime startDate, LocalDateTime endDate, LocalDateTime deadline) { + StudyDetail studyDetail = createStudyDetail(study, startDate, endDate); + studyDetail.publishAssignment(ASSIGNMENT_TITLE, deadline, DESCRIPTION_LINK); + return studyDetail; + } } From 60dd1fbd089cbf4adc2e5428800c942324401a68 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:37:41 +0900 Subject: [PATCH 14/40] =?UTF-8?q?feat:=20=EC=A0=9C=EC=B6=9C=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=9C=20=EA=B3=BC=EC=A0=9C=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80=20(#575)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 제출 가능한 과제 조회 api 추가 * rename: 클래스명 컨벤션에 맞게 수정 * refactor: repository에서 제출 가능 여부를 필터링 하도록 수정 * docs: 스웨거 설명 수정 * rename: 메서드명 수정 * refactor: 포맷터 도입 * feat: 과제 대시보드 조회 api 추가 * remove: 사용하지 않는 포매터 제거 * remove: 사용하지 않는 레포지토리 메서드 제거 * style: 개행 추가 * remove: 제출한 과제 필드 제거 * refactor: 정적 팩토리 메서드로 분리 --- ...oller.java => StudentStudyController.java} | 16 +++--- .../api/StudentStudyDetailController.java | 29 +++++++++++ .../StudentStudyDetailService.java | 43 ++++++++++++++++ ...yService.java => StudentStudyService.java} | 2 +- .../study/dao/StudyHistoryRepository.java | 2 + .../study/domain/AssignmentHistory.java | 6 +++ .../gdsc/domain/study/domain/StudyDetail.java | 6 +++ .../domain/study/domain/vo/Assignment.java | 11 +++++ .../response/AssignmentDashboardResponse.java | 14 ++++++ .../dto/response/AssignmentResponse.java | 2 + .../response/AssignmentSubmittableDto.java | 49 +++++++++++++++++++ ...Test.java => StudentStudyServiceTest.java} | 6 +-- 12 files changed, 174 insertions(+), 12 deletions(-) rename src/main/java/com/gdschongik/gdsc/domain/study/api/{StudyController.java => StudentStudyController.java} (82%) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java rename src/main/java/com/gdschongik/gdsc/domain/study/application/{StudyService.java => StudentStudyService.java} (99%) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentDashboardResponse.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentSubmittableDto.java rename src/test/java/com/gdschongik/gdsc/domain/study/application/{StudyServiceTest.java => StudentStudyServiceTest.java} (79%) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudyController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyController.java similarity index 82% rename from src/main/java/com/gdschongik/gdsc/domain/study/api/StudyController.java rename to src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyController.java index b490e55bb..4b32c8a01 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudyController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyController.java @@ -1,6 +1,6 @@ package com.gdschongik.gdsc.domain.study.api; -import com.gdschongik.gdsc.domain.study.application.StudyService; +import com.gdschongik.gdsc.domain.study.application.StudentStudyService; import com.gdschongik.gdsc.domain.study.dto.request.StudyAttendRequest; import com.gdschongik.gdsc.domain.study.dto.response.StudyResponse; import io.swagger.v3.oas.annotations.Operation; @@ -17,32 +17,32 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "Study", description = "사용자 스터디 API입니다.") +@Tag(name = "Student Study", description = "사용자 스터디 API입니다.") @RestController @RequestMapping("/studies") @RequiredArgsConstructor -public class StudyController { +public class StudentStudyController { - private final StudyService studyService; + private final StudentStudyService studentStudyService; @Operation(summary = "신청 가능한 스터디 조회", description = "모집 기간 중에 있는 스터디를 조회합니다.") @GetMapping("/apply") public ResponseEntity> getAllApplicableStudies() { - List response = studyService.getAllApplicableStudies(); + List response = studentStudyService.getAllApplicableStudies(); return ResponseEntity.ok().body(response); } @Operation(summary = "스터디 수강신청", description = "스터디에 수강신청 합니다. 모집 기간 중이어야 하고, 이미 수강 중인 스터디가 없어야 합니다.") @PostMapping("/apply/{studyId}") public ResponseEntity applyStudy(@PathVariable Long studyId) { - studyService.applyStudy(studyId); + studentStudyService.applyStudy(studyId); return ResponseEntity.ok().build(); } @Operation(summary = "스터디 수강신청 취소", description = "수강신청을 취소합니다. 스터디 수강신청 기간 중에만 취소할 수 있습니다.") @DeleteMapping("/apply/{studyId}") public ResponseEntity cancelStudyApply(@PathVariable Long studyId) { - studyService.cancelStudyApply(studyId); + studentStudyService.cancelStudyApply(studyId); return ResponseEntity.noContent().build(); } @@ -50,7 +50,7 @@ public ResponseEntity cancelStudyApply(@PathVariable Long studyId) { @PostMapping("/study-details/{studyDetailId}/attend") public ResponseEntity attend( @PathVariable Long studyDetailId, @Valid @RequestBody StudyAttendRequest request) { - studyService.attend(studyDetailId, request); + studentStudyService.attend(studyDetailId, request); return ResponseEntity.ok().build(); } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java new file mode 100644 index 000000000..1f8eeed52 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java @@ -0,0 +1,29 @@ +package com.gdschongik.gdsc.domain.study.api; + +import com.gdschongik.gdsc.domain.study.application.StudentStudyDetailService; +import com.gdschongik.gdsc.domain.study.dto.response.AssignmentDashboardResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Student Study Detail", description = "수강자 스터디 상세 API입니다.") +@RestController +@RequestMapping("/study-details") +@RequiredArgsConstructor +public class StudentStudyDetailController { + + private final StudentStudyDetailService studentStudyDetailService; + + @Operation(summary = "내 제출 가능한 과제 조회", description = "나의 제출 가능한 과제를 조회합니다.") + @GetMapping("/assignments/dashboard") + public ResponseEntity getSubmittableAssignments( + @RequestParam(name = "studyId") Long studyId) { + AssignmentDashboardResponse response = studentStudyDetailService.getSubmittableAssignments(studyId); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java new file mode 100644 index 000000000..36fac587c --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java @@ -0,0 +1,43 @@ +package com.gdschongik.gdsc.domain.study.application; + +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.study.dao.AssignmentHistoryRepository; +import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository; +import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; +import com.gdschongik.gdsc.domain.study.domain.StudyHistory; +import com.gdschongik.gdsc.domain.study.dto.response.AssignmentDashboardResponse; +import com.gdschongik.gdsc.domain.study.dto.response.AssignmentSubmittableDto; +import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.exception.ErrorCode; +import com.gdschongik.gdsc.global.util.MemberUtil; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class StudentStudyDetailService { + + private final MemberUtil memberUtil; + private final StudyHistoryRepository studyHistoryRepository; + private final AssignmentHistoryRepository assignmentHistoryRepository; + + @Transactional(readOnly = true) + public AssignmentDashboardResponse getSubmittableAssignments(Long studyId) { + Member currentMember = memberUtil.getCurrentMember(); + StudyHistory studyHistory = studyHistoryRepository + .findByMenteeAndStudyId(currentMember, studyId) + .orElseThrow(() -> new CustomException(ErrorCode.STUDY_HISTORY_NOT_FOUND)); + + List assignmentHistories = + assignmentHistoryRepository.findAssignmentHistoriesByMenteeAndStudy(currentMember, studyId); + boolean isAnySubmitted = assignmentHistories.stream().anyMatch(AssignmentHistory::isSubmitted); + List submittableAssignments = assignmentHistories.stream() + .filter(assignmentHistory -> assignmentHistory.getStudyDetail().isAssignmentDeadlineRemaining()) + .map(AssignmentSubmittableDto::from) + .toList(); + + return AssignmentDashboardResponse.of(studyHistory.getRepositoryLink(), isAnySubmitted, submittableAssignments); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java similarity index 99% rename from src/main/java/com/gdschongik/gdsc/domain/study/application/StudyService.java rename to src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java index f82634d2b..42c36001a 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java @@ -25,7 +25,7 @@ @Service @RequiredArgsConstructor @Transactional(readOnly = true) -public class StudyService { +public class StudentStudyService { private final MemberUtil memberUtil; private final StudyRepository studyRepository; diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java index f597aee42..5dd0a9cf4 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java @@ -15,4 +15,6 @@ public interface StudyHistoryRepository extends JpaRepository findByMenteeAndStudy(Member member, Study study); boolean existsByMenteeAndStudy(Member member, Study study); + + Optional findByMenteeAndStudyId(Member member, Long studyId); } 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 index 8d5f651cf..7c5d1edcb 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistory.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistory.java @@ -1,5 +1,7 @@ package com.gdschongik.gdsc.domain.study.domain; +import static com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus.*; + import com.gdschongik.gdsc.domain.common.model.BaseEntity; import com.gdschongik.gdsc.domain.member.domain.Member; import jakarta.persistence.Column; @@ -71,4 +73,8 @@ public static AssignmentHistory create(StudyDetail studyDetail, Member member) { .submissionStatus(AssignmentSubmissionStatus.PENDING) .build(); } + + public boolean isSubmitted() { + return submissionStatus == SUCCESS || submissionStatus == FAILURE; + } } 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 a6630ddc0..860282183 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 @@ -87,6 +87,12 @@ public void updateAssignment(String title, LocalDateTime deadLine, String descri assignment = Assignment.generateAssignment(title, deadLine, descriptionNotionLink); } + // 데이터 전달 로직 + + public boolean isAssignmentDeadlineRemaining() { + return assignment.isDeadlineRemaining(); + } + // 스터디 시작일자 + 현재 주차 * 7 + (스터디 요일 - 스터디 기간 시작 요일) public LocalDate getAttendanceDay() { return study.getStartDate() 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 699820114..c64cacfec 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 @@ -80,4 +80,15 @@ public void validateSubmittable(LocalDateTime now) { throw new CustomException(ASSIGNMENT_SUBMIT_DEADLINE_PASSED); } } + + // 데이터 전달 로직 + + public boolean isCancelled() { + return status == CANCELLED; + } + + public boolean isDeadlineRemaining() { + LocalDateTime now = LocalDateTime.now(); + return now.isBefore(deadline); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentDashboardResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentDashboardResponse.java new file mode 100644 index 000000000..12474bb19 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentDashboardResponse.java @@ -0,0 +1,14 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public record AssignmentDashboardResponse( + @Schema(description = "레포지토리 링크") String repositoryLink, + @Schema(description = "링크 수정 가능 여부") boolean isLinkEditable, + @Schema(description = "제출 가능한 과제") List submittableAssignments) { + public static AssignmentDashboardResponse of( + String repositoryLink, boolean isAnySubmitted, List submittableAssignments) { + return new AssignmentDashboardResponse(repositoryLink, !isAnySubmitted, submittableAssignments); + } +} 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 index 5967f53f7..3f0656984 100644 --- 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 @@ -10,6 +10,7 @@ public record AssignmentResponse( Long studyDetailId, @Schema(description = "과제 제목") String title, @Schema(description = "마감 기한") LocalDateTime deadline, + @Schema(description = "주차") Long week, @Schema(description = "과제 명세 링크") String descriptionLink, @Schema(description = "과제 상태") StudyStatus assignmentStatus) { public static AssignmentResponse from(StudyDetail studyDetail) { @@ -18,6 +19,7 @@ public static AssignmentResponse from(StudyDetail studyDetail) { studyDetail.getId(), assignment.getTitle(), assignment.getDeadline(), + studyDetail.getWeek(), assignment.getDescriptionLink(), assignment.getStatus()); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentSubmittableDto.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentSubmittableDto.java new file mode 100644 index 000000000..ce379f0de --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentSubmittableDto.java @@ -0,0 +1,49 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; +import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus; +import com.gdschongik.gdsc.domain.study.domain.StudyDetail; +import com.gdschongik.gdsc.domain.study.domain.StudyStatus; +import com.gdschongik.gdsc.domain.study.domain.SubmissionFailureType; +import com.gdschongik.gdsc.domain.study.domain.vo.Assignment; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.annotation.Nullable; +import java.time.LocalDateTime; + +public record AssignmentSubmittableDto( + Long studyDetailId, + @Schema(description = "과제 상태") StudyStatus assignmentStatus, + @Schema(description = "주차") Long week, + @Nullable @Schema(description = "과제 제목") String title, + @Nullable @Schema(description = "과제 제출 상태") AssignmentSubmissionStatus assignmentSubmissionStatus, + @Nullable @Schema(description = "과제 명세 링크") String descriptionLink, + @Nullable @Schema(description = "마감 기한") LocalDateTime deadline, + @Nullable @Schema(description = "과제 제출 링크") String submissionLink, + @Nullable @Schema(description = "과제 제출 실패 사유") SubmissionFailureType submissionFailureType) { + public static AssignmentSubmittableDto from(AssignmentHistory assignmentHistory) { + StudyDetail studyDetail = assignmentHistory.getStudyDetail(); + Assignment assignment = studyDetail.getAssignment(); + + if (assignment.isCancelled()) { + return cancelledAssignment(studyDetail, assignment); + } + + return new AssignmentSubmittableDto( + studyDetail.getId(), + assignment.getStatus(), + studyDetail.getWeek(), + assignment.getTitle(), + assignmentHistory.getSubmissionStatus(), + assignment.getDescriptionLink(), + assignment.getDeadline(), + assignmentHistory.getSubmissionLink(), + assignmentHistory.getSubmissionFailureType() == null + ? null + : assignmentHistory.getSubmissionFailureType()); + } + + private static AssignmentSubmittableDto cancelledAssignment(StudyDetail studyDetail, Assignment assignment) { + return new AssignmentSubmittableDto( + studyDetail.getId(), assignment.getStatus(), studyDetail.getWeek(), null, null, null, null, null, null); + } +} diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/application/StudyServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/application/StudentStudyServiceTest.java similarity index 79% rename from src/test/java/com/gdschongik/gdsc/domain/study/application/StudyServiceTest.java rename to src/test/java/com/gdschongik/gdsc/domain/study/application/StudentStudyServiceTest.java index 64177d3a4..62ce8c0bf 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/study/application/StudyServiceTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/study/application/StudentStudyServiceTest.java @@ -9,10 +9,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -public class StudyServiceTest extends IntegrationTest { +public class StudentStudyServiceTest extends IntegrationTest { @Autowired - private StudyService studyService; + private StudentStudyService studentStudyService; @Nested class 스터디_수강신청시 { @@ -20,7 +20,7 @@ class 스터디_수강신청시 { @Test void 존재하지_않는_스터디라면_실패한다() { // when & then - assertThatThrownBy(() -> studyService.applyStudy(1L)) + assertThatThrownBy(() -> studentStudyService.applyStudy(1L)) .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.STUDY_NOT_FOUND.getMessage()); } From 1dc3f2d491b5fd213e76ae46a310f8445ca0ce3e 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: Sun, 18 Aug 2024 13:24:26 +0900 Subject: [PATCH 15/40] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=88=98=EA=B0=95=EC=83=9D=20=EB=AA=85=EB=8B=A8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=20(#623)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 스터디원 조회 api 스펙 * feat: 멘토 특정 스터디 수강생 명단 조회 구현 * feat: 해당 스터디의 멘토인지 검증하는 로직 추가 * feat: 자잘한 컨벤션 지키기 * feat: endpoint수정하기 * feat: 외부 도메인 서비스에 의해 참조 엔티티에 대한 검증 책임 구현 * feat: 디스코드 관련 명칭 수정 * feat: 어드민, 멘토, 즉 게스트인 회원에 대한 접근을 검증하는 로직 추가 * feat: 어드민도 스터디의 수강생 명단 볼 수 있게 구성 * feat: 오타 수정 * feat: 테스트 추가 * feat: 테스트 추가 및 로직 수정 * feat: 스터디 validator확장성 고려해서 수정 * feat: 변경된 validator 매개변수 테스트코드에 반영 * feat: 변경된 매개변수 코드에 적용하기 * feat: 복잡한 예외 케이스 분리하기 * feat: 스터디 validator 테스트 오류 수정 * feat: description 수정, 불필요한 로직 수정 * feat: 코드 컨벤션 지키기 * feat: 오타 수정 * fix: 머지 컨플릭트 수정하기 * feat: develop과 merge --- .../gdsc/domain/member/domain/Member.java | 8 ++ .../study/api/MentorStudyController.java | 9 ++ .../study/application/MentorStudyService.java | 20 ++++ .../study/dao/StudyHistoryRepository.java | 2 + .../gdsc/domain/study/domain/Study.java | 2 + .../domain/study/domain/StudyValidator.java | 27 ++++++ .../dto/response/StudyStudentResponse.java | 22 +++++ .../gdsc/global/exception/ErrorCode.java | 2 + .../study/domain/StudyValidatorTest.java | 91 +++++++++++++++++++ .../gdschongik/gdsc/helper/FixtureHelper.java | 15 +++ 10 files changed, 198 insertions(+) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyValidator.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentResponse.java create mode 100644 src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyValidatorTest.java 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 2270544d3..da0a9124f 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 @@ -345,4 +345,12 @@ public boolean isAssociate() { public boolean isRegular() { return role.equals(REGULAR); } + + public boolean isAdmin() { + return manageRole.equals(ADMIN); + } + + public boolean isMentor() { + return studyRole.equals(MENTOR); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java index 8ce49f9bc..0ff6c784f 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java @@ -2,12 +2,14 @@ import com.gdschongik.gdsc.domain.study.application.MentorStudyService; import com.gdschongik.gdsc.domain.study.dto.response.MentorStudyResponse; +import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -25,4 +27,11 @@ public ResponseEntity> getStudiesInCharge() { List response = mentorStudyService.getStudiesInCharge(); return ResponseEntity.ok(response); } + + @Operation(summary = "스터디 수강생 명단 조회", description = "해당 스터디의 수강생 명단을 조회합니다") + @GetMapping("/{studyId}/students") + public ResponseEntity> getStudyStudents(@PathVariable Long studyId) { + List response = mentorStudyService.getStudyStudents(studyId); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java index 061338e1b..c2227117b 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java @@ -1,9 +1,15 @@ package com.gdschongik.gdsc.domain.study.application; import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository; import com.gdschongik.gdsc.domain.study.dao.StudyRepository; import com.gdschongik.gdsc.domain.study.domain.Study; +import com.gdschongik.gdsc.domain.study.domain.StudyHistory; +import com.gdschongik.gdsc.domain.study.domain.StudyValidator; import com.gdschongik.gdsc.domain.study.dto.response.MentorStudyResponse; +import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentResponse; +import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.exception.ErrorCode; import com.gdschongik.gdsc.global.util.MemberUtil; import java.util.List; import lombok.RequiredArgsConstructor; @@ -16,6 +22,8 @@ public class MentorStudyService { private final MemberUtil memberUtil; private final StudyRepository studyRepository; + private final StudyHistoryRepository studyHistoryRepository; + private final StudyValidator studyValidator; @Transactional(readOnly = true) public List getStudiesInCharge() { @@ -23,4 +31,16 @@ public List getStudiesInCharge() { List myStudies = studyRepository.findAllByMentor(currentMember); return myStudies.stream().map(MentorStudyResponse::from).toList(); } + + @Transactional(readOnly = true) + public List getStudyStudents(Long studyId) { + Member currentMember = memberUtil.getCurrentMember(); + Study study = + studyRepository.findById(studyId).orElseThrow(() -> new CustomException(ErrorCode.STUDY_NOT_FOUND)); + + studyValidator.validateStudyMentor(currentMember, study); + List studyHistories = studyHistoryRepository.findByStudyId(studyId); + + return studyHistories.stream().map(StudyStudentResponse::from).toList(); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java index 5dd0a9cf4..c35b95004 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java @@ -9,6 +9,8 @@ public interface StudyHistoryRepository extends JpaRepository { + List findByStudyId(Long studyId); + // TODO mentee -> student로 변경 List findAllByMentee(Member member); diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java index 373e0c306..fe2ebc20b 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java @@ -132,6 +132,8 @@ public static Study createStudy( .build(); } + // 검증 로직 + private static void validateApplicationStartDateBeforeSessionStartDate( LocalDateTime applicationStartDate, LocalDateTime startDate) { if (!applicationStartDate.isBefore(startDate)) { diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyValidator.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyValidator.java new file mode 100644 index 000000000..ce22c17e5 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyValidator.java @@ -0,0 +1,27 @@ +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.global.annotation.DomainService; +import com.gdschongik.gdsc.global.exception.CustomException; + +@DomainService +public class StudyValidator { + public void validateStudyMentor(Member currentMember, Study study) { + // 어드민인 경우 검증 통과 + if (currentMember.isAdmin()) { + return; + } + + // 어드민이 아니고 멘토 역할도 아니면 예외가 밸생합니다. + if (!currentMember.isMentor()) { + throw new CustomException(STUDY_ACCESS_NOT_ALLOWED); + } + + // 해당 스터디의 담당 멘토인지 검증 + if (!currentMember.getId().equals(study.getMentor().getId())) { + throw new CustomException(STUDY_MENTOR_INVALID); + } + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentResponse.java new file mode 100644 index 000000000..8f35cc2c7 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentResponse.java @@ -0,0 +1,22 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import com.gdschongik.gdsc.domain.study.domain.StudyHistory; +import io.swagger.v3.oas.annotations.media.Schema; + +public record StudyStudentResponse( + @Schema(description = "멤버 아이디") Long memberId, + @Schema(description = "학생 이름") String name, + @Schema(description = "학번") String studentId, + @Schema(description = "디스코드 사용자명") String discordUsername, + @Schema(description = "디스코드 닉네임") String nickname, + @Schema(description = "깃허브 링크") String githubLink) { + public static StudyStudentResponse from(StudyHistory studyHistory) { + return new StudyStudentResponse( + studyHistory.getMentee().getId(), + studyHistory.getMentee().getName(), + studyHistory.getMentee().getStudentId(), + studyHistory.getMentee().getDiscordUsername(), + studyHistory.getMentee().getNickname(), + studyHistory.getRepositoryLink()); + } +} 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 1ec0744ef..13cbe6fa3 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -100,6 +100,8 @@ public enum ErrorCode { // Study STUDY_APPLICATION_START_DATE_INVALID(HttpStatus.CONFLICT, "스터디 신청기간 시작일이 스터디 시작일보다 빠릅니다."), STUDY_MENTOR_IS_UNAUTHORIZED(HttpStatus.CONFLICT, "게스트인 회원은 멘토로 지정할 수 없습니다."), + STUDY_ACCESS_NOT_ALLOWED(HttpStatus.FORBIDDEN, "관리자 또는 멘토 역할이 아닌 회원은 이 작업을 수행할 수 없습니다."), + STUDY_MENTOR_INVALID(HttpStatus.CONFLICT, "사용자가 해당 스터디의 멘토가 아닙니다."), ON_OFF_LINE_STUDY_TIME_IS_ESSENTIAL(HttpStatus.CONFLICT, "온오프라인 스터디는 스터디 시간이 필요합니다."), STUDY_TIME_INVALID(HttpStatus.CONFLICT, "스터디종료 시각이 스터디시작 시각보다 빠릅니다."), ASSIGNMENT_STUDY_CAN_NOT_INPUT_STUDY_TIME(HttpStatus.CONFLICT, "과제 스터디는 스터디 시간을 입력할 수 없습니다."), diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyValidatorTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyValidatorTest.java new file mode 100644 index 000000000..6507a1fef --- /dev/null +++ b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyValidatorTest.java @@ -0,0 +1,91 @@ +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 StudyValidatorTest { + + FixtureHelper fixtureHelper = new FixtureHelper(); + StudyValidator studyValidator = new StudyValidator(); + + // FixtureHelper 래핑 메서드 + private Member createMentor(Long id) { + return fixtureHelper.createMentor(id); + } + + private Member createMember(Long id) { + return fixtureHelper.createAssociateMember(id); + } + + private Member createAdmin(Long id) { + return fixtureHelper.createAdmin(id); + } + + @Nested + class 스터디_검증시 { + + @Test + void 멘토역할이_아니라면_실패한다() { + // given + Member currentMember = createMember(1L); + Member mentor = createMentor(2L); + 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)); + + // when & then + assertThatThrownBy(() -> studyValidator.validateStudyMentor(currentMember, study)) + .isInstanceOf(CustomException.class) + .hasMessage(STUDY_ACCESS_NOT_ALLOWED.getMessage()); + } + + @Test + void 멘토이지만_자신이_맡은_스터디가_아니라면_실패한다() { + // given + Member currentMember = createMentor(1L); + Member mentor = createMentor(2L); + 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)); + + Study currentMentorStudy = fixtureHelper.createStudy( + currentMember, + Period.createPeriod(assignmentCreatedDate.plusDays(5), assignmentCreatedDate.plusDays(10)), + Period.createPeriod(assignmentCreatedDate.minusDays(5), assignmentCreatedDate)); + + // when & then + assertThat(currentMentorStudy.getMentor().getId()).isEqualTo(currentMember.getId()); + assertThatThrownBy(() -> studyValidator.validateStudyMentor(currentMember, study)) + .isInstanceOf(CustomException.class) + .hasMessage(STUDY_MENTOR_INVALID.getMessage()); + } + + @Test + void 어드민이라면_성공한다() { + // given + Member admin = createAdmin(1L); + Member mentor = createMentor(2L); + 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)); + + // when & then + assertThatCode(() -> studyValidator.validateStudyMentor(admin, study)) + .doesNotThrowAnyException(); + } + } +} diff --git a/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java b/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java index eb5706461..5874f7e71 100644 --- a/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java +++ b/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java @@ -1,6 +1,8 @@ package com.gdschongik.gdsc.helper; import static com.gdschongik.gdsc.domain.member.domain.Department.*; +import static com.gdschongik.gdsc.domain.member.domain.MemberManageRole.ADMIN; +import static com.gdschongik.gdsc.domain.member.domain.MemberStudyRole.MENTOR; 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.*; @@ -45,6 +47,19 @@ public Member createRegularMember(Long id) { return member; } + public Member createAdmin(Long id) { + Member member = createRegularMember(id); + ReflectionTestUtils.setField(member, "manageRole", ADMIN); + return member; + } + + public Member createMentor(Long id) { + Member member = createRegularMember(id); + member.assignToMentor(); + ReflectionTestUtils.setField(member, "studyRole", MENTOR); + return member; + } + public RecruitmentRound createRecruitmentRound( LocalDateTime startDate, LocalDateTime endDate, From e48c72df5f81e758e1308f8fb9e6d8206f3cf46c Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Sun, 18 Aug 2024 14:29:34 +0900 Subject: [PATCH 16/40] =?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=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20=EA=B9=83=ED=97=88=EB=B8=8C=20?= =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B3=80=EA=B2=BD=20(#639)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 정적 임포트 및 임포트 추가 * refactor: 각 예외에 대응되는 에러코드를 할당하도록 변경 * feat: 과제 성공 및 실패 처리 로직 추가 * feat: 과제 휴강 상태 제거 * feat: 제출상태에서 PENDING 제거 * feat: 생성자에서 제출정보 파라미터 제거 * feat: 초기 제출상태를 FAILURE로 시작하도록 변경 * feat: 초기 실패사유를 미제출 상태로 지정 * refactor: 제출여부 검증 로직에 실패사유를 사용하도록 변경 * docs: 투두 추가 * refactor: 순서 변경 * feat: 제출성공 시 설정할 실패사유 추가 * feat: 미사용 에러코드 제거 * feat: 실패처리 시 실패사유 검증로직 및 기존 제출정보 비우기 * test: 과제 히스토리 테스트 추가 --- .../study/domain/AssignmentHistory.java | 47 ++++- .../domain/AssignmentSubmissionStatus.java | 5 +- .../study/domain/SubmissionFailureType.java | 3 +- .../gdsc/global/exception/ErrorCode.java | 5 +- .../infra/github/client/GithubClient.java | 70 +++++--- .../study/domain/AssignmentHistoryTest.java | 165 ++++++++++++++++++ .../global/common/constant/StudyConstant.java | 6 + 7 files changed, 263 insertions(+), 38 deletions(-) create mode 100644 src/test/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistoryTest.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 index 7c5d1edcb..0316c6652 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistory.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistory.java @@ -1,9 +1,12 @@ package com.gdschongik.gdsc.domain.study.domain; import static com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus.*; +import static com.gdschongik.gdsc.domain.study.domain.SubmissionFailureType.*; +import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import com.gdschongik.gdsc.domain.common.model.BaseEntity; import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.global.exception.CustomException; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -14,6 +17,7 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -44,6 +48,8 @@ public class AssignmentHistory extends BaseEntity { private Long contentLength; + private LocalDateTime committedAt; + @Enumerated(EnumType.STRING) private AssignmentSubmissionStatus submissionStatus; @@ -54,27 +60,50 @@ public class AssignmentHistory extends BaseEntity { private AssignmentHistory( StudyDetail studyDetail, Member member, - String submissionLink, - String commitHash, - Long contentLength, - AssignmentSubmissionStatus submissionStatus) { + AssignmentSubmissionStatus submissionStatus, + SubmissionFailureType submissionFailureType) { this.studyDetail = studyDetail; this.member = member; - this.submissionLink = submissionLink; - this.commitHash = commitHash; - this.contentLength = contentLength; this.submissionStatus = submissionStatus; + this.submissionFailureType = submissionFailureType; } public static AssignmentHistory create(StudyDetail studyDetail, Member member) { return AssignmentHistory.builder() .studyDetail(studyDetail) .member(member) - .submissionStatus(AssignmentSubmissionStatus.PENDING) + .submissionStatus(FAILURE) + .submissionFailureType(NOT_SUBMITTED) .build(); } + // 데이터 조회 로직 + public boolean isSubmitted() { - return submissionStatus == SUCCESS || submissionStatus == FAILURE; + return submissionFailureType != NOT_SUBMITTED; + } + + // 데이터 변경 로직 + + public void success(String submissionLink, String commitHash, Long contentLength, LocalDateTime committedAt) { + this.submissionLink = submissionLink; + this.commitHash = commitHash; + this.contentLength = contentLength; + this.committedAt = committedAt; + this.submissionStatus = SUCCESS; + this.submissionFailureType = NONE; + } + + public void fail(SubmissionFailureType submissionFailureType) { + if (submissionFailureType == NOT_SUBMITTED || submissionFailureType == NONE) { + throw new CustomException(ASSIGNMENT_INVALID_FAILURE_TYPE); + } + + this.submissionLink = null; + this.commitHash = null; + this.contentLength = null; + this.committedAt = null; + this.submissionStatus = FAILURE; + this.submissionFailureType = submissionFailureType; } } 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 index 3865abb98..28fcf864a 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionStatus.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionStatus.java @@ -6,10 +6,9 @@ @Getter @RequiredArgsConstructor public enum AssignmentSubmissionStatus { - PENDING("제출 전"), + // TODO: 클라이언트 응답에는 PENDING 상태 필요하므로, 추후 응답용 enum 클래스 생성 구현 FAILURE("제출 실패"), - SUCCESS("제출 성공"), - CANCELLED("과제 휴강"); // TODO: 제거 및 DB에서 삭제 + SUCCESS("제출 성공"); 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 index 252b556e9..bd875f25f 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/SubmissionFailureType.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/SubmissionFailureType.java @@ -6,7 +6,8 @@ @Getter @RequiredArgsConstructor public enum SubmissionFailureType { - NOT_SUBMITTED("미제출"), + NONE("실패 없음"), // 제출상태 성공 시 사용 + NOT_SUBMITTED("미제출"), // 기본값 WORD_COUNT_INSUFFICIENT("글자수 부족"), LOCATION_UNIDENTIFIABLE("위치 확인불가"); 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 13cbe6fa3..7677ecfc9 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -150,7 +150,7 @@ public enum ErrorCode { ORDER_FINAL_PAYMENT_AMOUNT_MISMATCH(HttpStatus.CONFLICT, "주문 최종결제금액은 주문총액에서 할인금액을 뺀 값이어야 합니다."), // Assignment - ASSIGNMENT_CAN_NOT_BE_UPDATED(HttpStatus.CONFLICT, "휴강인 과제는 수정할 수 없습니다."), + ASSIGNMENT_INVALID_FAILURE_TYPE(HttpStatus.CONFLICT, "유효하지 않은 제출 실패사유입니다."), ASSIGNMENT_DEADLINE_INVALID(HttpStatus.CONFLICT, "과제 마감 기한이 현재보다 빠릅니다."), ASSIGNMENT_STUDY_NOT_APPLIED(HttpStatus.CONFLICT, "해당 스터디에 대한 수강신청 기록이 존재하지 않습니다."), ASSIGNMENT_SUBMIT_NOT_STARTED(HttpStatus.CONFLICT, "아직 과제가 시작되지 않았습니다."), @@ -160,6 +160,9 @@ public enum ErrorCode { // Github GITHUB_REPOSITORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 레포지토리입니다."), + GITHUB_CONTENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 파일입니다."), + GITHUB_FILE_READ_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "깃허브 파일 읽기에 실패했습니다."), + GITHUB_COMMIT_DATE_FETCH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "깃허브 커밋 날짜 조회에 실패했습니다."), GITHUB_ASSIGNMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 과제 파일입니다."), ; diff --git a/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java b/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java index d5b94a740..34dd73f9c 100644 --- a/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java +++ b/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java @@ -1,13 +1,15 @@ package com.gdschongik.gdsc.infra.github.client; import static com.gdschongik.gdsc.global.common.constant.GithubConstant.*; +import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import com.gdschongik.gdsc.global.exception.CustomException; -import com.gdschongik.gdsc.global.exception.ErrorCode; import com.gdschongik.gdsc.infra.github.dto.response.GithubAssignmentSubmissionResponse; import java.io.IOException; +import java.io.InputStream; import java.time.LocalDateTime; import java.time.ZoneId; +import java.util.Date; import lombok.RequiredArgsConstructor; import org.kohsuke.github.GHCommit; import org.kohsuke.github.GHContent; @@ -25,35 +27,55 @@ public GHRepository getRepository(String ownerRepo) { try { return github.getRepository(ownerRepo); } catch (IOException e) { - throw new CustomException(ErrorCode.GITHUB_REPOSITORY_NOT_FOUND); + throw new CustomException(GITHUB_REPOSITORY_NOT_FOUND); } } public GithubAssignmentSubmissionResponse getLatestAssignmentSubmission(String repo, int week) { + GHRepository ghRepository = getRepository(repo); + String assignmentPath = GITHUB_ASSIGNMENT_PATH.formatted(week); + + // GHContent#getSize() 의 경우 한글 문자열을 byte 단위로 계산하기 때문에, 직접 content를 읽어서 길이를 계산 + GHContent ghContent = getFileContent(ghRepository, assignmentPath); + String content = readFileContent(ghContent); + + GHCommit ghLatestCommit = ghRepository + .queryCommits() + .path(assignmentPath) + .list() + .withPageSize(1) + .iterator() + .next(); + + LocalDateTime committedAt = getCommitDate(ghLatestCommit) + .toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + + return new GithubAssignmentSubmissionResponse(ghLatestCommit.getSHA1(), content.length(), committedAt); + } + + private GHContent getFileContent(GHRepository ghRepository, String filePath) { + try { + return ghRepository.getFileContent(filePath); + } catch (IOException e) { + throw new CustomException(GITHUB_CONTENT_NOT_FOUND); + } + } + + private String readFileContent(GHContent ghContent) { + try (InputStream inputStream = ghContent.read()) { + return new String(inputStream.readAllBytes()); + } catch (IOException e) { + throw new CustomException(GITHUB_FILE_READ_FAILED); + } + } + + private Date getCommitDate(GHCommit ghLatestCommit) { try { - GHRepository ghRepository = getRepository(repo); - String assignmentPath = GITHUB_ASSIGNMENT_PATH.formatted(week); - - // GHContent#getSize() 의 경우 한글 문자열을 byte 단위로 계산하기 때문에, 직접 content를 읽어서 길이를 계산 - GHContent ghContent = ghRepository.getFileContent(assignmentPath); - String content = new String(ghContent.read().readAllBytes()); - - GHCommit ghLatestCommit = ghRepository - .queryCommits() - .path(assignmentPath) - .list() - .toList() - .get(0); - - LocalDateTime committedAt = ghLatestCommit - .getCommitDate() - .toInstant() - .atZone(ZoneId.systemDefault()) - .toLocalDateTime(); - - return new GithubAssignmentSubmissionResponse(ghLatestCommit.getSHA1(), content.length(), committedAt); + return ghLatestCommit.getCommitDate(); } catch (IOException e) { - throw new CustomException(ErrorCode.GITHUB_ASSIGNMENT_NOT_FOUND); + throw new CustomException(GITHUB_COMMIT_DATE_FETCH_FAILED); } } } diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistoryTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistoryTest.java new file mode 100644 index 000000000..3c45e14d1 --- /dev/null +++ b/src/test/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistoryTest.java @@ -0,0 +1,165 @@ +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.global.exception.CustomException; +import com.gdschongik.gdsc.helper.FixtureHelper; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class AssignmentHistoryTest { + + FixtureHelper fixtureHelper = new FixtureHelper(); + + private Member createMember(Long id) { + return fixtureHelper.createAssociateMember(id); + } + + private Study createStudyWithMentor(Long mentorId) { + Period period = Period.createPeriod(STUDY_START_DATETIME, STUDY_END_DATETIME); + Period applicationPeriod = + Period.createPeriod(STUDY_START_DATETIME.minusDays(7), STUDY_START_DATETIME.minusDays(1)); + return fixtureHelper.createStudyWithMentor(mentorId, period, applicationPeriod); + } + + private StudyDetail createStudyDetailWithAssignment(Study study) { + return fixtureHelper.createStudyDetailWithAssignment( + study, STUDY_DETAIL_START_DATETIME, STUDY_DETAIL_END_DATETIME, STUDY_ASSIGNMENT_DEADLINE_DATETIME); + } + + @Nested + class 빈_과제이력_생성할때 { + + @Test + void 제출상태는_FAILURE이다() { + // given + Member member = createMember(1L); + Study study = createStudyWithMentor(1L); + StudyDetail studyDetail = createStudyDetailWithAssignment(study); + + // when + AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member); + + // then + assertThat(assignmentHistory.getSubmissionStatus()).isEqualTo(AssignmentSubmissionStatus.FAILURE); + } + + @Test + void 실패사유는_NOT_SUBMITTED이다() { + // given + Member member = createMember(1L); + Study study = createStudyWithMentor(1L); + StudyDetail studyDetail = createStudyDetailWithAssignment(study); + + // when + AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member); + + // then + assertThat(assignmentHistory.getSubmissionFailureType()).isEqualTo(SubmissionFailureType.NOT_SUBMITTED); + } + } + + @Nested + class 과제이력_제출_성공할때 { + + @Test + void 제출상태는_SUCCESS이다() { + // given + Member member = createMember(1L); + Study study = createStudyWithMentor(1L); + StudyDetail studyDetail = createStudyDetailWithAssignment(study); + AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member); + + // when + assignmentHistory.success(SUBMISSION_LINK, COMMIT_HASH, CONTENT_LENGTH, COMMITTED_AT); + } + + @Test + void 실패사유는_NONE이다() { + // given + Member member = createMember(1L); + Study study = createStudyWithMentor(1L); + StudyDetail studyDetail = createStudyDetailWithAssignment(study); + AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member); + + // when + assignmentHistory.success(SUBMISSION_LINK, COMMIT_HASH, CONTENT_LENGTH, COMMITTED_AT); + + // then + assertThat(assignmentHistory.getSubmissionFailureType()).isEqualTo(SubmissionFailureType.NONE); + } + } + + @Nested + class 과제이력_제출_실패할때 { + + @Test + void 제출상태는_FAILURE이다() { + // given + Member member = createMember(1L); + Study study = createStudyWithMentor(1L); + StudyDetail studyDetail = createStudyDetailWithAssignment(study); + AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member); + assignmentHistory.success(SUBMISSION_LINK, COMMIT_HASH, CONTENT_LENGTH, COMMITTED_AT); + + // when + assignmentHistory.fail(SubmissionFailureType.WORD_COUNT_INSUFFICIENT); + + // then + assertThat(assignmentHistory.getSubmissionStatus()).isEqualTo(AssignmentSubmissionStatus.FAILURE); + } + + @Test + void 실패사유는_NOT_SUBMITTED가_될수없다() { + // given + Member member = createMember(1L); + Study study = createStudyWithMentor(1L); + StudyDetail studyDetail = createStudyDetailWithAssignment(study); + AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member); + assignmentHistory.success(SUBMISSION_LINK, COMMIT_HASH, CONTENT_LENGTH, COMMITTED_AT); + + // when, then + assertThatThrownBy(() -> assignmentHistory.fail(SubmissionFailureType.NOT_SUBMITTED)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(ASSIGNMENT_INVALID_FAILURE_TYPE.getMessage()); + } + + @Test + void 실패사유는_NONE이_될수없다() { + // given + Member member = createMember(1L); + Study study = createStudyWithMentor(1L); + StudyDetail studyDetail = createStudyDetailWithAssignment(study); + AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member); + assignmentHistory.success(SUBMISSION_LINK, COMMIT_HASH, CONTENT_LENGTH, COMMITTED_AT); + + // when, then + assertThatThrownBy(() -> assignmentHistory.fail(SubmissionFailureType.NONE)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(ASSIGNMENT_INVALID_FAILURE_TYPE.getMessage()); + } + + @Test + void 기존_제출정보는_삭제된다() { + // given + Member member = createMember(1L); + Study study = createStudyWithMentor(1L); + StudyDetail studyDetail = createStudyDetailWithAssignment(study); + AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member); + assignmentHistory.success(SUBMISSION_LINK, COMMIT_HASH, CONTENT_LENGTH, COMMITTED_AT); + + // when + assignmentHistory.fail(SubmissionFailureType.WORD_COUNT_INSUFFICIENT); + + // then + assertThat(assignmentHistory.getSubmissionLink()).isNull(); + assertThat(assignmentHistory.getCommitHash()).isNull(); + assertThat(assignmentHistory.getContentLength()).isNull(); + assertThat(assignmentHistory.getCommittedAt()).isNull(); + } + } +} 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 0e9e26afc..031bf4fbd 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 @@ -31,4 +31,10 @@ private StudyConstant() {} public static final LocalDateTime STUDY_DETAIL_START_DATETIME = STUDY_START_DATETIME; public static final LocalDateTime STUDY_DETAIL_END_DATETIME = STUDY_DETAIL_START_DATETIME.plusWeeks(1); public static final LocalDateTime STUDY_ASSIGNMENT_DEADLINE_DATETIME = STUDY_DETAIL_END_DATETIME; + + // AssignmentHistory + public static final String SUBMISSION_LINK = "https://github.com/ownername/reponame/blob/main/week1/WIL.md"; + public static final String COMMIT_HASH = "aa11bb22cc33"; + public static final Long CONTENT_LENGTH = 2000L; + public static final LocalDateTime COMMITTED_AT = LocalDateTime.of(2024, 9, 8, 0, 0); } From e242f91616fac3e0cf2f917cc13f413048cbb020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=8A=AC=EA=B8=B0?= <84620016+seulgi99@users.noreply.github.com> Date: Sun, 18 Aug 2024 15:18:50 +0900 Subject: [PATCH 17/40] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EA=B3=B5=EC=A7=80=20=EC=83=9D=EC=84=B1,=EC=88=98=EC=A0=95,?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20API=20=EC=B6=94=EA=B0=80=20(#641)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 스터디 공지 생성,수정,삭제 API 추가 * refactor: spotless적용 * style: 스터디 명단조회 API와 스터디 공지 생성 API 코드 순서 변경 * refactor: 스터디 공지 테이블명 StudyNotification에서 StudyAnnouncement로 수정 * refactor: 스터디 공지 수정 PUT메소드로 변경 * refactor: 스터디 공지 생성 수정 리퀘스트명 변경 --- .../study/api/MentorStudyController.java | 29 ++++++++++++ .../study/api/StudentStudyController.java | 4 +- .../study/application/MentorStudyService.java | 47 +++++++++++++++++++ .../application/StudentStudyService.java | 4 +- .../dao/StudyAnnouncementRepository.java | 14 ++++++ .../domain/study/dao/StudyRepository.java | 7 +++ ...tification.java => StudyAnnouncement.java} | 21 ++++++++- .../StudyAnnouncementCreateUpdateRequest.java | 8 ++++ ...est.java => StudyAttendCreateRequest.java} | 2 +- .../gdsc/global/exception/ErrorCode.java | 3 ++ 10 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyAnnouncementRepository.java rename src/main/java/com/gdschongik/gdsc/domain/study/domain/{StudyNotification.java => StudyAnnouncement.java} (57%) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyAnnouncementCreateUpdateRequest.java rename src/main/java/com/gdschongik/gdsc/domain/study/dto/request/{StudyAttendRequest.java => StudyAttendCreateRequest.java} (93%) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java index 0ff6c784f..640327cd8 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java @@ -1,15 +1,21 @@ package com.gdschongik.gdsc.domain.study.api; import com.gdschongik.gdsc.domain.study.application.MentorStudyService; +import com.gdschongik.gdsc.domain.study.dto.request.StudyAnnouncementCreateUpdateRequest; import com.gdschongik.gdsc.domain.study.dto.response.MentorStudyResponse; import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentResponse; 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.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +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.RestController; @@ -34,4 +40,27 @@ public ResponseEntity> getStudyStudents(@PathVariable List response = mentorStudyService.getStudyStudents(studyId); return ResponseEntity.ok(response); } + + @Operation(summary = "스터디 공지 생성", description = "스터디의 공지사항을 생성합니다.") + @PostMapping("/{studyId}/announcements") + public ResponseEntity createStudyAnnouncement( + @PathVariable Long studyId, @Valid @RequestBody StudyAnnouncementCreateUpdateRequest request) { + mentorStudyService.createStudyAnnouncement(studyId, request); + return ResponseEntity.ok().build(); + } + + @Operation(summary = "스터디 공지 수정", description = "스터디의 공지사항을 수정합니다.") + @PutMapping("/announcements/{studyAnnouncementId}") + public ResponseEntity updateStudyAnnouncement( + @PathVariable Long studyAnnouncementId, @Valid @RequestBody StudyAnnouncementCreateUpdateRequest request) { + mentorStudyService.updateStudyAnnouncement(studyAnnouncementId, request); + return ResponseEntity.ok().build(); + } + + @Operation(summary = "스터디 공지 삭제", description = "스터디의 공지사항을 삭제합니다.") + @DeleteMapping("/announcements/{studyAnnouncementId}") + public ResponseEntity deleteStudyAnnouncement(@PathVariable Long studyAnnouncementId) { + mentorStudyService.deleteStudyAnnouncement(studyAnnouncementId); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyController.java index 4b32c8a01..88f796c2a 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyController.java @@ -1,7 +1,7 @@ package com.gdschongik.gdsc.domain.study.api; import com.gdschongik.gdsc.domain.study.application.StudentStudyService; -import com.gdschongik.gdsc.domain.study.dto.request.StudyAttendRequest; +import com.gdschongik.gdsc.domain.study.dto.request.StudyAttendCreateRequest; import com.gdschongik.gdsc.domain.study.dto.response.StudyResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -49,7 +49,7 @@ public ResponseEntity cancelStudyApply(@PathVariable Long studyId) { @Operation(summary = "스터디 출석체크", description = "스터디에 출석체크합니다. 현재 진행중인 스터디 회차에 출석체크해야 하며, 중복출석체크할 수 없습니다.") @PostMapping("/study-details/{studyDetailId}/attend") public ResponseEntity attend( - @PathVariable Long studyDetailId, @Valid @RequestBody StudyAttendRequest request) { + @PathVariable Long studyDetailId, @Valid @RequestBody StudyAttendCreateRequest request) { studentStudyService.attend(studyDetailId, request); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java index c2227117b..5aed434fd 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java @@ -1,11 +1,14 @@ package com.gdschongik.gdsc.domain.study.application; import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.study.dao.StudyAnnouncementRepository; import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository; import com.gdschongik.gdsc.domain.study.dao.StudyRepository; import com.gdschongik.gdsc.domain.study.domain.Study; +import com.gdschongik.gdsc.domain.study.domain.StudyAnnouncement; import com.gdschongik.gdsc.domain.study.domain.StudyHistory; import com.gdschongik.gdsc.domain.study.domain.StudyValidator; +import com.gdschongik.gdsc.domain.study.dto.request.StudyAnnouncementCreateUpdateRequest; import com.gdschongik.gdsc.domain.study.dto.response.MentorStudyResponse; import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentResponse; import com.gdschongik.gdsc.global.exception.CustomException; @@ -13,15 +16,18 @@ import com.gdschongik.gdsc.global.util.MemberUtil; import java.util.List; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service @RequiredArgsConstructor public class MentorStudyService { private final MemberUtil memberUtil; private final StudyRepository studyRepository; + private final StudyAnnouncementRepository studyAnnouncementRepository; private final StudyHistoryRepository studyHistoryRepository; private final StudyValidator studyValidator; @@ -43,4 +49,45 @@ public List getStudyStudents(Long studyId) { return studyHistories.stream().map(StudyStudentResponse::from).toList(); } + + @Transactional + public void createStudyAnnouncement(Long studyId, StudyAnnouncementCreateUpdateRequest request) { + Member currentMember = memberUtil.getCurrentMember(); + final Study study = studyRepository.getById(studyId); + + studyValidator.validateStudyMentor(currentMember, study); + + StudyAnnouncement studyAnnouncement = + StudyAnnouncement.createStudyAnnouncement(study, request.title(), request.link()); + studyAnnouncementRepository.save(studyAnnouncement); + + log.info("[MentorStudyService] 스터디 공지 생성: studyAnnouncementId={}", studyAnnouncement.getId()); + } + + @Transactional + public void updateStudyAnnouncement(Long studyAnnouncementId, StudyAnnouncementCreateUpdateRequest request) { + Member currentMember = memberUtil.getCurrentMember(); + final StudyAnnouncement studyAnnouncement = studyAnnouncementRepository.getById(studyAnnouncementId); + Study study = studyAnnouncement.getStudy(); + + studyValidator.validateStudyMentor(currentMember, study); + + studyAnnouncement.update(request.title(), request.link()); + studyAnnouncementRepository.save(studyAnnouncement); + + log.info("[MentorStudyService] 스터디 공지 수정 완료: studyAnnouncementId={}", studyAnnouncement.getId()); + } + + @Transactional + public void deleteStudyAnnouncement(Long studyAnnouncementId) { + Member currentMember = memberUtil.getCurrentMember(); + final StudyAnnouncement studyAnnouncement = studyAnnouncementRepository.getById(studyAnnouncementId); + Study study = studyAnnouncement.getStudy(); + + studyValidator.validateStudyMentor(currentMember, study); + + studyAnnouncementRepository.delete(studyAnnouncement); + + log.info("[MentorStudyService] 스터디 공지 삭제 완료: studyAnnouncementId={}", studyAnnouncement.getId()); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java index 42c36001a..e8828879f 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java @@ -10,7 +10,7 @@ import com.gdschongik.gdsc.domain.study.domain.*; import com.gdschongik.gdsc.domain.study.domain.Attendance; import com.gdschongik.gdsc.domain.study.domain.AttendanceValidator; -import com.gdschongik.gdsc.domain.study.dto.request.StudyAttendRequest; +import com.gdschongik.gdsc.domain.study.dto.request.StudyAttendCreateRequest; import com.gdschongik.gdsc.domain.study.dto.response.StudyResponse; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.util.MemberUtil; @@ -73,7 +73,7 @@ public void cancelStudyApply(Long studyId) { } @Transactional - public void attend(Long studyDetailId, StudyAttendRequest request) { + public void attend(Long studyDetailId, StudyAttendCreateRequest request) { final StudyDetail studyDetail = studyDetailRepository .findById(studyDetailId) .orElseThrow(() -> new CustomException(STUDY_DETAIL_NOT_FOUND)); diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyAnnouncementRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyAnnouncementRepository.java new file mode 100644 index 000000000..b741bf4e6 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyAnnouncementRepository.java @@ -0,0 +1,14 @@ +package com.gdschongik.gdsc.domain.study.dao; + +import static com.gdschongik.gdsc.global.exception.ErrorCode.STUDY_ANNOUNCEMENT_NOT_FOUND; + +import com.gdschongik.gdsc.domain.study.domain.StudyAnnouncement; +import com.gdschongik.gdsc.global.exception.CustomException; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StudyAnnouncementRepository extends JpaRepository { + + default StudyAnnouncement getById(Long id) { + return findById(id).orElseThrow(() -> new CustomException(STUDY_ANNOUNCEMENT_NOT_FOUND)); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyRepository.java index f5c671240..c050df530 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyRepository.java @@ -1,11 +1,18 @@ package com.gdschongik.gdsc.domain.study.dao; +import static com.gdschongik.gdsc.global.exception.ErrorCode.STUDY_NOT_FOUND; + import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.study.domain.Study; +import com.gdschongik.gdsc.global.exception.CustomException; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface StudyRepository extends JpaRepository { + default Study getById(Long id) { + return findById(id).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND)); + } + List findAllByMentor(Member mentor); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyNotification.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyAnnouncement.java similarity index 57% rename from src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyNotification.java rename to src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyAnnouncement.java index 125b88f55..f10470bc8 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyNotification.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyAnnouncement.java @@ -10,17 +10,18 @@ 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 StudyNotification extends BaseEntity { +public class StudyAnnouncement extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "study_notification_id") + @Column(name = "study_announcement_id") private Long id; @ManyToOne(fetch = FetchType.LAZY) @@ -31,4 +32,20 @@ public class StudyNotification extends BaseEntity { @Column(columnDefinition = "TEXT") private String link; + + @Builder(access = AccessLevel.PRIVATE) + public StudyAnnouncement(Study study, String title, String link) { + this.study = study; + this.title = title; + this.link = link; + } + + public static StudyAnnouncement createStudyAnnouncement(Study study, String title, String link) { + return StudyAnnouncement.builder().study(study).title(title).link(link).build(); + } + + public void update(String title, String link) { + this.title = title; + this.link = link; + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyAnnouncementCreateUpdateRequest.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyAnnouncementCreateUpdateRequest.java new file mode 100644 index 000000000..12bcff77b --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyAnnouncementCreateUpdateRequest.java @@ -0,0 +1,8 @@ +package com.gdschongik.gdsc.domain.study.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +public record StudyAnnouncementCreateUpdateRequest( + @NotBlank(message = "공지제목이 비었습니다.") @Schema(description = "공지제목") String title, + @Schema(description = "공지링크") String link) {} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyAttendRequest.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyAttendCreateRequest.java similarity index 93% rename from src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyAttendRequest.java rename to src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyAttendCreateRequest.java index 051b1d5a9..47c817d29 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyAttendRequest.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyAttendCreateRequest.java @@ -6,7 +6,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; -public record StudyAttendRequest( +public record StudyAttendCreateRequest( @NotBlank @Pattern(regexp = ATTENDANCE_NUMBER, message = "출석번호는 " + ATTENDANCE_NUMBER + " 형식이어야 합니다.") @Schema(description = "출석번호") 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 7677ecfc9..103b6a40b 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -123,6 +123,9 @@ public enum ErrorCode { HttpStatus.CONFLICT, "이미 제출한 과제가 있으므로 레포지토리를 수정할 수 없습니다."), STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_OWNER_MISMATCH(HttpStatus.CONFLICT, "레포지토리 소유자가 현재 멤버와 다릅니다."), + // StudyAnnouncement + STUDY_ANNOUNCEMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 공지입니다."), + // Attendance ATTENDANCE_DATE_INVALID(HttpStatus.CONFLICT, "강의일이 아니면 출석체크할 수 없습니다."), ATTENDANCE_NUMBER_MISMATCH(HttpStatus.CONFLICT, "출석번호가 일치하지 않습니다."), From eb9e3ca30fe557cc362228b3f03a96496fe0eb4f 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, 19 Aug 2024 19:38:53 +0900 Subject: [PATCH 18/40] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EC=A0=95=EB=B3=B4=EC=99=80=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=EC=9E=91=EC=84=B1=20API=20(#6?= =?UTF-8?q?42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 스터디 상세 작성 스펙 구현 * feat: 스터디 상세 정보 기본 로직 작성 * feat: 스터디 상세 정보 작성 API * feat: validate 개선 * feat: 테스트 케이스 추가 * feat: 전체 로직 구현 * feat: 전체 로직, validaotr 테스트코드 작성 * feat: log 추가 * feat: test 가독성 개선 * feat: 에러 코드 수정 * feat: 수정 로직 불필요한 주석 제거 * feat: 스터디 상세 정보 id 제약조건 추가 * feat: 스터디 상세 정보 비즈니스 로직에서 스터디 비즈니스 로직으로 변경 * feat: 스터디 요청 Dto 이름 변경 * feat: 스터디 상세 정보 -> 스터디 상세 & 기본으로 변경 * fix: pr 수정 요청 부분 수정 * feat: 고정된 날짜 사용으로 대체하기 * feat: this로 객체 메서드 오류 해결, 테스트 불필요한 fixture삭제 * feat: 올바른 테스트 데이터 작성 * feat: 스터디 시작 시간을 세션 시작시간으로 변경 --- .../study/api/MentorStudyController.java | 17 +++-- .../study/application/MentorStudyService.java | 55 ++++++++++++++ .../gdsc/domain/study/domain/Study.java | 5 ++ .../gdsc/domain/study/domain/StudyDetail.java | 6 ++ .../study/domain/StudyDetailValidator.java | 13 ++++ .../gdsc/domain/study/domain/vo/Session.java | 18 ++++- .../request/StudySessionCreateRequest.java | 13 ++++ .../study/dto/request/StudyUpdateRequest.java | 9 +++ .../gdsc/global/exception/ErrorCode.java | 4 +- .../application/MentorStudyServiceTest.java | 76 +++++++++++++++++++ .../domain/StudyDetailValidatorTest.java | 33 ++++++++ .../global/common/constant/StudyConstant.java | 6 ++ .../gdschongik/gdsc/helper/FixtureHelper.java | 8 ++ .../gdsc/helper/IntegrationTest.java | 29 +++++++ 14 files changed, 278 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudySessionCreateRequest.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyUpdateRequest.java create mode 100644 src/test/java/com/gdschongik/gdsc/domain/study/application/MentorStudyServiceTest.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java index 640327cd8..69a615e9d 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java @@ -2,6 +2,7 @@ import com.gdschongik.gdsc.domain.study.application.MentorStudyService; import com.gdschongik.gdsc.domain.study.dto.request.StudyAnnouncementCreateUpdateRequest; +import com.gdschongik.gdsc.domain.study.dto.request.StudyUpdateRequest; import com.gdschongik.gdsc.domain.study.dto.response.MentorStudyResponse; import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentResponse; import io.swagger.v3.oas.annotations.Operation; @@ -10,14 +11,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -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.RestController; +import org.springframework.web.bind.annotation.*; @Tag(name = "Mentor Study", description = "멘토 스터디 API입니다.") @RestController @@ -27,6 +21,13 @@ public class MentorStudyController { private final MentorStudyService mentorStudyService; + @Operation(summary = "스터디 정보 작성", description = "스터디 기본 정보와 상세 정보를 작성합니다.") + @PatchMapping("/{studyId}") + public ResponseEntity updateStudy(@PathVariable Long studyId, @RequestBody StudyUpdateRequest request) { + mentorStudyService.updateStudy(studyId, request); + return ResponseEntity.ok().build(); + } + @Operation(summary = "내 스터디 조회", description = "내가 멘토로 있는 스터디를 조회합니다.") @GetMapping("/me") public ResponseEntity> getStudiesInCharge() { diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java index 5aed434fd..67580e0bd 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java @@ -1,20 +1,28 @@ package com.gdschongik.gdsc.domain.study.application; +import static com.gdschongik.gdsc.global.exception.ErrorCode.STUDY_NOT_FOUND; + import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.study.dao.StudyAnnouncementRepository; +import com.gdschongik.gdsc.domain.study.dao.StudyDetailRepository; import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository; import com.gdschongik.gdsc.domain.study.dao.StudyRepository; +import com.gdschongik.gdsc.domain.study.domain.*; import com.gdschongik.gdsc.domain.study.domain.Study; import com.gdschongik.gdsc.domain.study.domain.StudyAnnouncement; import com.gdschongik.gdsc.domain.study.domain.StudyHistory; import com.gdschongik.gdsc.domain.study.domain.StudyValidator; import com.gdschongik.gdsc.domain.study.dto.request.StudyAnnouncementCreateUpdateRequest; +import com.gdschongik.gdsc.domain.study.dto.request.StudySessionCreateRequest; +import com.gdschongik.gdsc.domain.study.dto.request.StudyUpdateRequest; import com.gdschongik.gdsc.domain.study.dto.response.MentorStudyResponse; import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentResponse; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.exception.ErrorCode; import com.gdschongik.gdsc.global.util.MemberUtil; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -30,6 +38,8 @@ public class MentorStudyService { private final StudyAnnouncementRepository studyAnnouncementRepository; private final StudyHistoryRepository studyHistoryRepository; private final StudyValidator studyValidator; + private final StudyDetailRepository studyDetailRepository; + private final StudyDetailValidator studyDetailValidator; @Transactional(readOnly = true) public List getStudiesInCharge() { @@ -90,4 +100,49 @@ public void deleteStudyAnnouncement(Long studyAnnouncementId) { log.info("[MentorStudyService] 스터디 공지 삭제 완료: studyAnnouncementId={}", studyAnnouncement.getId()); } + + // TODO session -> curriculum 변경 + @Transactional + public void updateStudy(Long studyId, StudyUpdateRequest request) { + Member currentMember = memberUtil.getCurrentMember(); + Study study = studyRepository.findById(studyId).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND)); + studyValidator.validateStudyMentor(currentMember, study); + + List studyDetails = studyDetailRepository.findAllByStudyId(studyId); + // StudyDetail ID를 추출하여 Set으로 저장 + Set studyDetailIds = studyDetails.stream().map(StudyDetail::getId).collect(Collectors.toSet()); + + // 요청된 StudySessionCreateRequest의 StudyDetail ID를 추출하여 Set으로 저장 + Set requestIds = request.studySessions().stream() + .map(StudySessionCreateRequest::studyDetailId) + .collect(Collectors.toSet()); + + studyDetailValidator.validateUpdateStudyDetail(studyDetailIds, requestIds); + + study.update(request.notionLink(), request.introduction()); + studyRepository.save(study); + log.info("[MentorStudyService] 스터디 기본 정보 수정 완료: studyId={}", studyId); + + updateAllStudyDetailSession(studyDetails, request.studySessions()); + } + + private void updateAllStudyDetailSession( + List studyDetails, List studySessions) { + for (StudyDetail studyDetail : studyDetails) { + Long id = studyDetail.getId(); + StudySessionCreateRequest matchingSession = studySessions.stream() + .filter(session -> session.studyDetailId().equals(id)) + .findFirst() + .get(); + + studyDetail.updateSession( + studyDetail.getStudy().getStartTime(), + matchingSession.title(), + matchingSession.description(), + matchingSession.difficulty(), + matchingSession.status()); + } + studyDetailRepository.saveAll(studyDetails); + log.info("[MentorStudyService] 스터디 상세정보 커리큘럼 작성 완료: studyDetailId={}", studyDetails); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java index fe2ebc20b..6fe8152b3 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/Study.java @@ -182,4 +182,9 @@ public boolean isStudyOngoing() { public LocalDate getStartDate() { return period.getStartDate().toLocalDate(); } + + public void update(String link, String introduction) { + notionLink = link; + this.introduction = introduction; + } } 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 860282183..f8aa0244f 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 @@ -10,6 +10,7 @@ import jakarta.persistence.*; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -101,6 +102,11 @@ public LocalDate getAttendanceDay() { - study.getStartDate().getDayOfWeek().getValue()); } + public void updateSession( + LocalTime startAt, String title, String description, Difficulty difficulty, StudyStatus status) { + session = Session.generateSession(startAt, title, description, difficulty, status); + } + public void validateAssignmentSubmittable(LocalDateTime now) { if (now.isBefore(period.getStartDate())) { throw new CustomException(ASSIGNMENT_SUBMIT_NOT_STARTED); 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 59ab1afb8..161a1383f 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 @@ -7,6 +7,7 @@ import com.gdschongik.gdsc.global.annotation.DomainService; import com.gdschongik.gdsc.global.exception.CustomException; import java.time.LocalDateTime; +import java.util.Set; @DomainService public class StudyDetailValidator { @@ -54,4 +55,16 @@ private void validateUpdateDeadline( throw new CustomException(STUDY_DETAIL_ASSIGNMENT_INVALID_UPDATE_DEADLINE); } } + + public void validateUpdateStudyDetail(Set studyDetails, Set requests) { + // StudyDetail 목록과 요청된 StudySessionCreateRequest 목록의 크기를 먼저 비교 + if (studyDetails.size() != requests.size()) { + throw new CustomException(STUDY_DETAIL_SESSION_SIZE_MISMATCH); + } + + // 두 ID 집합이 동일한지 비교하여 ID 불일치 시 예외를 던짐 + if (!studyDetails.equals(requests)) { + throw new CustomException(STUDY_DETAIL_ID_INVALID); + } + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/vo/Session.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/vo/Session.java index d32aba4cc..1b26ff63b 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/vo/Session.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/vo/Session.java @@ -5,7 +5,7 @@ import jakarta.persistence.Embeddable; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; -import java.time.LocalDateTime; +import java.time.LocalTime; import lombok.AccessLevel; import lombok.Builder; import lombok.EqualsAndHashCode; @@ -19,7 +19,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Session { - private LocalDateTime startAt; + private LocalTime startAt; private String title; @@ -33,8 +33,7 @@ public class Session { private StudyStatus status; @Builder(access = AccessLevel.PRIVATE) - private Session( - LocalDateTime startAt, String title, String description, Difficulty difficulty, StudyStatus status) { + private Session(LocalTime startAt, String title, String description, Difficulty difficulty, StudyStatus status) { this.startAt = startAt; this.title = title; this.description = description; @@ -45,4 +44,15 @@ private Session( public static Session createEmptySession() { return Session.builder().status(StudyStatus.NONE).build(); } + + public static Session generateSession( + LocalTime startAt, String title, String description, Difficulty difficulty, StudyStatus status) { + return Session.builder() + .startAt(startAt) + .title(title) + .description(description) + .difficulty(difficulty) + .status(status) + .build(); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudySessionCreateRequest.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudySessionCreateRequest.java new file mode 100644 index 000000000..fd511dcd0 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudySessionCreateRequest.java @@ -0,0 +1,13 @@ +package com.gdschongik.gdsc.domain.study.dto.request; + +import com.gdschongik.gdsc.domain.study.domain.Difficulty; +import com.gdschongik.gdsc.domain.study.domain.StudyStatus; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record StudySessionCreateRequest( + @NotNull Long studyDetailId, + @Schema(description = "제목") String title, + @Schema(description = "설명") String description, + @Schema(description = "난이도") Difficulty difficulty, + @Schema(description = "휴강 여부") StudyStatus status) {} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyUpdateRequest.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyUpdateRequest.java new file mode 100644 index 000000000..326c05831 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/request/StudyUpdateRequest.java @@ -0,0 +1,9 @@ +package com.gdschongik.gdsc.domain.study.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public record StudyUpdateRequest( + @Schema(description = "스터디 소개 페이지 링크") String notionLink, + @Schema(description = "스터디 한 줄 소개") String introduction, + List studySessions) {} 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 103b6a40b..69777c0f7 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -114,7 +114,8 @@ public enum ErrorCode { STUDY_DETAIL_UPDATE_RESTRICTED_TO_MENTOR(HttpStatus.CONFLICT, "해당 스터디의 멘토만 수정할 수 있습니다."), STUDY_DETAIL_ASSIGNMENT_INVALID_DEADLINE(HttpStatus.CONFLICT, "마감기한이 지난 과제의 마감기한을 수정할 수 없습니다"), STUDY_DETAIL_ASSIGNMENT_INVALID_UPDATE_DEADLINE(HttpStatus.CONFLICT, "수정하려고 하는 과제의 마감기한은 기존의 마감기한보다 빠르면 안됩니다."), - + STUDY_DETAIL_ID_INVALID(HttpStatus.CONFLICT, "수정하려는 스터디 상세정보가 서버에 존재하지 않거나 유효하지 않습니다."), + STUDY_DETAIL_SESSION_SIZE_MISMATCH(HttpStatus.BAD_REQUEST, "스터디 커리큘럼의 총 개수가 일치하지 않습니다."), // StudyHistory STUDY_HISTORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 수강 기록입니다."), STUDY_HISTORY_DUPLICATE(HttpStatus.CONFLICT, "이미 해당 스터디를 신청했습니다."), @@ -168,7 +169,6 @@ public enum ErrorCode { GITHUB_COMMIT_DATE_FETCH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "깃허브 커밋 날짜 조회에 실패했습니다."), GITHUB_ASSIGNMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 과제 파일입니다."), ; - private final HttpStatus status; private final String message; } diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/application/MentorStudyServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/application/MentorStudyServiceTest.java new file mode 100644 index 000000000..0f0d0da22 --- /dev/null +++ b/src/test/java/com/gdschongik/gdsc/domain/study/application/MentorStudyServiceTest.java @@ -0,0 +1,76 @@ +package com.gdschongik.gdsc.domain.study.application; + +import static com.gdschongik.gdsc.global.common.constant.StudyConstant.*; +import static com.gdschongik.gdsc.global.common.constant.StudyConstant.SESSION_DESCRIPTION; +import static org.assertj.core.api.Assertions.assertThat; + +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.domain.Difficulty; +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.domain.study.dto.request.StudySessionCreateRequest; +import com.gdschongik.gdsc.domain.study.dto.request.StudyUpdateRequest; +import com.gdschongik.gdsc.helper.IntegrationTest; +import java.time.LocalDateTime; +import java.util.*; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; + +public class MentorStudyServiceTest extends IntegrationTest { + + @Autowired + private MentorStudyService mentorStudyService; + + @Nested + class 스터디_정보_작성시 { + + @Test + void 성공한다() { + // given + LocalDateTime now = STUDY_START_DATETIME; + Member mentor = createMentor(); + Study study = createNewStudy( + mentor, + 4L, + Period.createPeriod(now.plusDays(5), now.plusDays(10)), + Period.createPeriod(now.minusDays(5), now)); + for (int i = 1; i <= 4; i++) { + Long week = (long) i; + createNewStudyDetail(week, study, now, now.plusDays(7)); + now = now.plusDays(8); + } + logoutAndReloginAs(study.getMentor().getId(), MemberRole.ASSOCIATE); + + List sessionCreateRequests = new ArrayList<>(); + for (int i = 1; i <= study.getTotalWeek(); i++) { + Long id = (long) i; + StudySessionCreateRequest sessionCreateRequest = new StudySessionCreateRequest( + id, SESSION_TITLE + i, SESSION_DESCRIPTION + i, Difficulty.HIGH, StudyStatus.OPEN); + sessionCreateRequests.add(sessionCreateRequest); + } + + StudyUpdateRequest request = + new StudyUpdateRequest(STUDY_NOTION_LINK, STUDY_INTRODUCTION, sessionCreateRequests); + + // when + mentorStudyService.updateStudy(1L, request); + + // then + Study savedStudy = studyRepository.findById(study.getId()).get(); + assertThat(savedStudy.getNotionLink()).isEqualTo(request.notionLink()); + + List studyDetails = studyDetailRepository.findAllByStudyId(1L); + for (int i = 0; i < studyDetails.size(); i++) { + StudyDetail studyDetail = studyDetails.get(i); + Long expectedId = studyDetail.getId(); + + assertThat(studyDetail.getId()).isEqualTo(expectedId); + assertThat(studyDetail.getSession().getTitle()).isEqualTo(SESSION_TITLE + expectedId); + assertThat(studyDetail.getSession().getDescription()).isEqualTo(SESSION_DESCRIPTION + expectedId); + } + } + } +} 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 0c536814f..e0e8789f6 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 @@ -10,6 +10,9 @@ import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.helper.FixtureHelper; import java.time.LocalDateTime; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.LongStream; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -157,4 +160,34 @@ class 과제_수정시 { .hasMessage(STUDY_DETAIL_ASSIGNMENT_INVALID_DEADLINE.getMessage()); } } + + @Nested + class 스터디_상세정보_작성시 { + + @Test + void 존재하는_스터디상세정보_총개수와_요청된_스터디상세정보_총개수가_다르면_실패한다() { + // given + Set studyDetailIds = LongStream.rangeClosed(1, 4).boxed().collect(Collectors.toSet()); + + Set requestIds = LongStream.rangeClosed(1, 5).boxed().collect(Collectors.toSet()); + + // when & then + assertThatThrownBy(() -> studyDetailValidator.validateUpdateStudyDetail(studyDetailIds, requestIds)) + .isInstanceOf(CustomException.class) + .hasMessage(STUDY_DETAIL_SESSION_SIZE_MISMATCH.getMessage()); + } + + @Test + void 요청한_상세정보_id와_기존의_상세정보_id가_맞지_않으면_실패한다() { + // given + Set studyDetailIds = LongStream.rangeClosed(1, 4).boxed().collect(Collectors.toSet()); + + Set requestIds = LongStream.rangeClosed(2, 5).boxed().collect(Collectors.toSet()); + + // when & then + assertThatThrownBy(() -> studyDetailValidator.validateUpdateStudyDetail(studyDetailIds, requestIds)) + .isInstanceOf(CustomException.class) + .hasMessage(STUDY_DETAIL_ID_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 031bf4fbd..02588410d 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 @@ -26,12 +26,18 @@ private StudyConstant() {} // Study (2024-09-01 ~ 2024-10-27) public static final LocalDateTime STUDY_START_DATETIME = LocalDateTime.of(2024, 9, 1, 0, 0); public static final LocalDateTime STUDY_END_DATETIME = STUDY_START_DATETIME.plusWeeks(8); + public static final String STUDY_NOTION_LINK = "notionLink"; + public static final String STUDY_INTRODUCTION = "introduction"; // StudyDetail (1주차: 2024-09-01 ~ 2024-09-08) public static final LocalDateTime STUDY_DETAIL_START_DATETIME = STUDY_START_DATETIME; public static final LocalDateTime STUDY_DETAIL_END_DATETIME = STUDY_DETAIL_START_DATETIME.plusWeeks(1); public static final LocalDateTime STUDY_ASSIGNMENT_DEADLINE_DATETIME = STUDY_DETAIL_END_DATETIME; + // Session + public static final String SESSION_TITLE = "sessionTitle"; + public static final String SESSION_DESCRIPTION = "sessionDescription"; + // AssignmentHistory public static final String SUBMISSION_LINK = "https://github.com/ownername/reponame/blob/main/week1/WIL.md"; public static final String COMMIT_HASH = "aa11bb22cc33"; diff --git a/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java b/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java index 5874f7e71..184dec360 100644 --- a/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java +++ b/src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java @@ -105,6 +105,14 @@ public StudyDetail createStudyDetail(Study study, LocalDateTime startDate, Local return StudyDetail.createStudyDetail(study, 1L, ATTENDANCE_NUMBER, Period.createPeriod(startDate, endDate)); } + public StudyDetail createNewStudyDetail( + Long id, Study study, Long week, LocalDateTime startDate, LocalDateTime endDate) { + StudyDetail studyDetail = + StudyDetail.createStudyDetail(study, week, ATTENDANCE_NUMBER, Period.createPeriod(startDate, endDate)); + ReflectionTestUtils.setField(studyDetail, "id", id); + return studyDetail; + } + public StudyDetail createStudyDetailWithAssignment( Study study, LocalDateTime startDate, LocalDateTime endDate, LocalDateTime deadline) { StudyDetail studyDetail = createStudyDetail(study, 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 92f63c521..fc0e07f8d 100644 --- a/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java +++ b/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java @@ -165,6 +165,12 @@ protected Member createRegularMember() { return memberRepository.save(member); } + public Member createMentor() { + Member member = createAssociateMember(); + member.assignToMentor(); + return memberRepository.save(member); + } + protected RecruitmentRound createRecruitmentRound() { Recruitment recruitment = createRecruitment(ACADEMIC_YEAR, SEMESTER_TYPE, FEE); @@ -223,9 +229,32 @@ protected Study createStudy(Member mentor, Period period, Period applicationPeri return studyRepository.save(study); } + protected Study createNewStudy(Member mentor, Long totalWeek, Period period, Period applicationPeriod) { + Study study = Study.createStudy( + ACADEMIC_YEAR, + SEMESTER_TYPE, + STUDY_TITLE, + mentor, + period, + applicationPeriod, + totalWeek, + 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); } + + protected StudyDetail createNewStudyDetail(Long week, Study study, LocalDateTime startDate, LocalDateTime endDate) { + StudyDetail studyDetail = + StudyDetail.createStudyDetail(study, week, ATTENDANCE_NUMBER, Period.createPeriod(startDate, endDate)); + return studyDetailRepository.save(studyDetail); + } } From afdf94325c8fb20651266dbb5b4876f84980df28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=8A=AC=EA=B8=B0?= <84620016+seulgi99@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:20:08 +0900 Subject: [PATCH 19/40] =?UTF-8?q?fix:=20objectMapper=EC=97=90=20JavaTimeMo?= =?UTF-8?q?dule=20=EC=B6=94=EA=B0=80=20(#651)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gdsc/global/config/ObjectMapperConfig.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/main/java/com/gdschongik/gdsc/global/config/ObjectMapperConfig.java diff --git a/src/main/java/com/gdschongik/gdsc/global/config/ObjectMapperConfig.java b/src/main/java/com/gdschongik/gdsc/global/config/ObjectMapperConfig.java new file mode 100644 index 000000000..44fbbb5f0 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/config/ObjectMapperConfig.java @@ -0,0 +1,16 @@ +package com.gdschongik.gdsc.global.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ObjectMapperConfig { + @Bean + public ObjectMapper objectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + return mapper; + } +} From 4c7f42b07a5f241cbc453f1eac92344b581c7790 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:19:59 +0900 Subject: [PATCH 20/40] =?UTF-8?q?feat:=20StudyResponse=EC=97=90=20?= =?UTF-8?q?=ED=95=99=EB=85=84=EB=8F=84=EC=99=80=20=ED=95=99=EA=B8=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#654)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gdsc/domain/study/dto/response/StudyResponse.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyResponse.java index e04ccdd8e..d7274ad3e 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyResponse.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyResponse.java @@ -1,5 +1,6 @@ package com.gdschongik.gdsc.domain.study.dto.response; +import com.gdschongik.gdsc.domain.common.model.SemesterType; import com.gdschongik.gdsc.domain.study.domain.Study; import io.swagger.v3.oas.annotations.media.Schema; import java.time.DayOfWeek; @@ -8,6 +9,8 @@ public record StudyResponse( Long studyId, + @Schema(description = "학년도") Integer academicYear, + @Schema(description = "학기") SemesterType semesterType, @Schema(description = "이름") String title, @Schema(description = "종류") String studyType, @Schema(description = "상세설명 노션 링크") String notionLink, @@ -21,6 +24,8 @@ public record StudyResponse( public static StudyResponse from(Study study) { return new StudyResponse( study.getId(), + study.getAcademicYear(), + study.getSemesterType(), study.getTitle(), study.getStudyType().getValue(), study.getNotionLink(), From 072933ee8112119c725242da5fd7304ff6c5d171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=8A=AC=EA=B8=B0?= <84620016+seulgi99@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:45:58 +0900 Subject: [PATCH 21/40] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EA=B3=B5=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80=20(#644)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 스터디 공지 목록 조회 API 추가 * test: 스터디 멘토 혹은 수강생 검증 테스트 추가 * refactor: 오타수정 --- .../gdsc/domain/member/domain/Member.java | 4 ++ .../study/api/CommonStudyController.java | 9 ++++ .../study/application/CommonStudyService.java | 28 ++++++++++++ .../dao/StudyAnnouncementRepository.java | 3 ++ .../domain/study/domain/StudyValidator.java | 20 ++++++++- .../response/StudyAnnouncementResponse.java | 20 +++++++++ .../study/domain/StudyValidatorTest.java | 44 ++++++++++++++++++- 7 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyAnnouncementResponse.java 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 da0a9124f..e2c07099b 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 @@ -353,4 +353,8 @@ public boolean isAdmin() { public boolean isMentor() { return studyRole.equals(MENTOR); } + + public boolean isStudent() { + return studyRole.equals(STUDENT); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/CommonStudyController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/CommonStudyController.java index af1b58517..43c1b0ccd 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/CommonStudyController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/CommonStudyController.java @@ -2,8 +2,10 @@ import com.gdschongik.gdsc.domain.study.application.CommonStudyService; import com.gdschongik.gdsc.domain.study.dto.response.CommonStudyResponse; +import com.gdschongik.gdsc.domain.study.dto.response.StudyAnnouncementResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -25,4 +27,11 @@ public ResponseEntity getStudyInformation(@PathVariable Lon CommonStudyResponse response = commonStudyService.getStudyInformation(studyId); return ResponseEntity.ok(response); } + + @Operation(summary = "스터디 공지 목록 조회", description = "스터디 공지 목록을 조회합니다.") + @GetMapping("/{studyId}/announcements") + public ResponseEntity> getStudyAnnouncements(@PathVariable Long studyId) { + List response = commonStudyService.getStudyAnnouncements(studyId); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/CommonStudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/CommonStudyService.java index 4561160e8..ae45dbd70 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/CommonStudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/CommonStudyService.java @@ -2,10 +2,20 @@ import static com.gdschongik.gdsc.global.exception.ErrorCode.STUDY_NOT_FOUND; +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.study.dao.StudyAnnouncementRepository; +import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository; import com.gdschongik.gdsc.domain.study.dao.StudyRepository; import com.gdschongik.gdsc.domain.study.domain.Study; +import com.gdschongik.gdsc.domain.study.domain.StudyAnnouncement; +import com.gdschongik.gdsc.domain.study.domain.StudyHistory; +import com.gdschongik.gdsc.domain.study.domain.StudyValidator; import com.gdschongik.gdsc.domain.study.dto.response.CommonStudyResponse; +import com.gdschongik.gdsc.domain.study.dto.response.StudyAnnouncementResponse; import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.util.MemberUtil; +import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -17,10 +27,28 @@ public class CommonStudyService { private final StudyRepository studyRepository; + private final StudyHistoryRepository studyHistoryRepository; + private final StudyAnnouncementRepository studyAnnouncementRepository; + private final MemberUtil memberUtil; + private final StudyValidator studyValidator; @Transactional(readOnly = true) public CommonStudyResponse getStudyInformation(Long studyId) { Study study = studyRepository.findById(studyId).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND)); return CommonStudyResponse.from(study); } + + @Transactional(readOnly = true) + public List getStudyAnnouncements(Long studyId) { + Member currentMember = memberUtil.getCurrentMember(); + final Study study = studyRepository.findById(studyId).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND)); + Optional studyHistory = studyHistoryRepository.findByMenteeAndStudyId(currentMember, studyId); + + studyValidator.validateStudyMentorOrStudent(currentMember, study, studyHistory); + + final List studyAnnouncements = + studyAnnouncementRepository.findAllByStudyIdOrderByCreatedAtDesc(studyId); + + return studyAnnouncements.stream().map(StudyAnnouncementResponse::from).toList(); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyAnnouncementRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyAnnouncementRepository.java index b741bf4e6..3d81940e2 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyAnnouncementRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyAnnouncementRepository.java @@ -4,6 +4,7 @@ import com.gdschongik.gdsc.domain.study.domain.StudyAnnouncement; import com.gdschongik.gdsc.global.exception.CustomException; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface StudyAnnouncementRepository extends JpaRepository { @@ -11,4 +12,6 @@ public interface StudyAnnouncementRepository extends JpaRepository new CustomException(STUDY_ANNOUNCEMENT_NOT_FOUND)); } + + List findAllByStudyIdOrderByCreatedAtDesc(Long studyId); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyValidator.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyValidator.java index ce22c17e5..93a95deab 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyValidator.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyValidator.java @@ -5,6 +5,7 @@ import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.global.annotation.DomainService; import com.gdschongik.gdsc.global.exception.CustomException; +import java.util.Optional; @DomainService public class StudyValidator { @@ -14,7 +15,7 @@ public void validateStudyMentor(Member currentMember, Study study) { return; } - // 어드민이 아니고 멘토 역할도 아니면 예외가 밸생합니다. + // 멘토인지 검증 if (!currentMember.isMentor()) { throw new CustomException(STUDY_ACCESS_NOT_ALLOWED); } @@ -24,4 +25,21 @@ public void validateStudyMentor(Member currentMember, Study study) { throw new CustomException(STUDY_MENTOR_INVALID); } } + + public void validateStudyMentorOrStudent(Member currentMember, Study study, Optional studyHistory) { + // 어드민인 경우 검증 통과 + if (currentMember.isAdmin()) { + return; + } + + // 해당 스터디의 수강생인지 검증 + if (currentMember.isStudent() && studyHistory.isEmpty()) { + throw new CustomException(STUDY_ACCESS_NOT_ALLOWED); + } + + // 해당 스터디의 담당 멘토인지 검증 + if (!currentMember.getId().equals(study.getMentor().getId())) { + throw new CustomException(STUDY_MENTOR_INVALID); + } + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyAnnouncementResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyAnnouncementResponse.java new file mode 100644 index 000000000..22a95f0d8 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyAnnouncementResponse.java @@ -0,0 +1,20 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import com.gdschongik.gdsc.domain.study.domain.StudyAnnouncement; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; + +public record StudyAnnouncementResponse( + Long studyAnnounceId, + @Schema(description = "제목") String title, + @Schema(description = "링크") String link, + @Schema(description = "생성 일자") LocalDate createdDate) { + + public static StudyAnnouncementResponse from(StudyAnnouncement studyAnnouncement) { + return new StudyAnnouncementResponse( + studyAnnouncement.getId(), + studyAnnouncement.getTitle(), + studyAnnouncement.getLink(), + studyAnnouncement.getCreatedAt().toLocalDate()); + } +} diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyValidatorTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyValidatorTest.java index 6507a1fef..602aefaa8 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyValidatorTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyValidatorTest.java @@ -8,6 +8,7 @@ import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.helper.FixtureHelper; import java.time.LocalDateTime; +import java.util.Optional; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -30,7 +31,7 @@ private Member createAdmin(Long id) { } @Nested - class 스터디_검증시 { + class 스터디_멘토역할_검증시 { @Test void 멘토역할이_아니라면_실패한다() { @@ -88,4 +89,45 @@ class 스터디_검증시 { .doesNotThrowAnyException(); } } + + @Nested + class 스터디_멘토_또는_학생역할_검증시 { + + @Test + void 수강하지않는_스터디가_아니라면_실패한다() { + // given + Member student = createMember(1L); + Member mentor = createMentor(2L); + 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)); + StudyHistory studyHistory = null; + + // when & then + assertThatThrownBy(() -> studyValidator.validateStudyMentorOrStudent( + student, study, Optional.ofNullable(studyHistory))) + .isInstanceOf(CustomException.class) + .hasMessage(STUDY_ACCESS_NOT_ALLOWED.getMessage()); + } + + @Test + void 멘토이지만_자신이_맡은_스터디가_아니라면_실패한다() { + // given + Member currentMember = createMentor(1L); + Member mentor = createMentor(2L); + 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)); + + // when & then + assertThatThrownBy( + () -> studyValidator.validateStudyMentorOrStudent(currentMember, study, Optional.empty())) + .isInstanceOf(CustomException.class) + .hasMessage(STUDY_MENTOR_INVALID.getMessage()); + } + } } From 4aa194cb91f5e2f6c4fbab5316d807403802ad47 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Tue, 20 Aug 2024 22:14:12 +0900 Subject: [PATCH 22/40] =?UTF-8?q?feat:=20=EC=8B=A0=EC=B2=AD=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=9C=20=EC=8A=A4=ED=84=B0=EB=94=94=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=EC=97=90=20=EC=9D=B4=EB=AF=B8=20=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=ED=95=9C=20=EC=8A=A4=ED=84=B0=EB=94=94=20ID=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#652)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 신청 가능한 스터디 조회 api에 이미 신청한 스터디 pk 필드 추가 * rename: 변수명 수정 * docs: 문서화 추가 --- .../domain/study/api/StudentStudyController.java | 7 +++---- .../study/application/StudentStudyService.java | 16 +++++++++++++--- .../dto/response/StudyApplicableResponse.java | 13 +++++++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyApplicableResponse.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyController.java index 88f796c2a..2d85082e2 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyController.java @@ -2,11 +2,10 @@ import com.gdschongik.gdsc.domain.study.application.StudentStudyService; import com.gdschongik.gdsc.domain.study.dto.request.StudyAttendCreateRequest; -import com.gdschongik.gdsc.domain.study.dto.response.StudyResponse; +import com.gdschongik.gdsc.domain.study.dto.response.StudyApplicableResponse; 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.DeleteMapping; @@ -27,8 +26,8 @@ public class StudentStudyController { @Operation(summary = "신청 가능한 스터디 조회", description = "모집 기간 중에 있는 스터디를 조회합니다.") @GetMapping("/apply") - public ResponseEntity> getAllApplicableStudies() { - List response = studentStudyService.getAllApplicableStudies(); + public ResponseEntity getAllApplicableStudies() { + StudyApplicableResponse response = studentStudyService.getAllApplicableStudies(); return ResponseEntity.ok().body(response); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java index e8828879f..4da76a819 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java @@ -11,11 +11,13 @@ import com.gdschongik.gdsc.domain.study.domain.Attendance; import com.gdschongik.gdsc.domain.study.domain.AttendanceValidator; import com.gdschongik.gdsc.domain.study.dto.request.StudyAttendCreateRequest; +import com.gdschongik.gdsc.domain.study.dto.response.StudyApplicableResponse; import com.gdschongik.gdsc.domain.study.dto.response.StudyResponse; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.util.MemberUtil; import java.time.LocalDate; import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -35,11 +37,19 @@ public class StudentStudyService { private final AttendanceRepository attendanceRepository; private final AttendanceValidator attendanceValidator; - public List getAllApplicableStudies() { - return studyRepository.findAll().stream() + public StudyApplicableResponse getAllApplicableStudies() { + Member currentMember = memberUtil.getCurrentMember(); + List studyHistories = studyHistoryRepository.findAllByMentee(currentMember); + Optional appliedStudy = studyHistories.stream() + .map(StudyHistory::getStudy) + .filter(Study::isStudyOngoing) + .findFirst(); + List studyResponses = studyRepository.findAll().stream() .filter(Study::isApplicable) .map(StudyResponse::from) .toList(); + + return StudyApplicableResponse.of(appliedStudy.orElse(null), studyResponses); } @Transactional @@ -69,7 +79,7 @@ public void cancelStudyApply(Long studyId) { .orElseThrow(() -> new CustomException(STUDY_HISTORY_NOT_FOUND)); studyHistoryRepository.delete(studyHistory); - log.info("[StudyService] 스터디 수강신청 취소: studyId={}, memberId={}", study.getId(), currentMember.getId()); + log.info("[StudyService] 스터디 수강신청 취소: appliedStudyId={}, memberId={}", study.getId(), currentMember.getId()); } @Transactional diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyApplicableResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyApplicableResponse.java new file mode 100644 index 000000000..6fcaf74d3 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyApplicableResponse.java @@ -0,0 +1,13 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import com.gdschongik.gdsc.domain.study.domain.Study; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.annotation.Nullable; +import java.util.List; + +public record StudyApplicableResponse( + @Nullable @Schema(description = "이미 신청한 스터디 id") Long appliedStudyId, List studyResponses) { + public static StudyApplicableResponse of(Study study, List studyResponses) { + return new StudyApplicableResponse(study == null ? null : study.getId(), studyResponses); + } +} From db5adc3f96949f06be971d785d538911b94f1494 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Wed, 21 Aug 2024 00:49:44 +0900 Subject: [PATCH 23/40] =?UTF-8?q?feat:=20=EA=B3=BC=EC=A0=9C=20=EC=A0=9C?= =?UTF-8?q?=EC=B6=9C=ED=95=98=EA=B8=B0=20=EC=B1=84=EC=A0=90=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84=20(#649)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 미사용 에러코드 제거 * refactor: 과제 길이를 integer로 수정 * feat: VO Supplier를 응답하도록 변경 * feat: 제출 실패사유에 알수없음 추가 * feat: 과제 채점 로직 구현 * refactor: 항상 LocalDateTime을 반환하도록 변경 * refactor: 과제 최소길이를 상수로 추출 * feat: 함수형 인터페이스인 AssignmentSubmissionFetcher 추가 * refactor: 패키지 위치 변경 * docs: 투두 추가 * fix: 지연 평가 작동하도록 로직 분리 * feat: 클로저가 되지 않도록 수정 * feat: 예외 시그니처 추가 * feat: 변경된 과제정보 페치 방식 반영 * docs: 구현 관련 주석 추가 * feat: 깃허브 요청 과정에서 발생한 예외는 전부 UNKNOWN으로 변환 * feat: 과제 길이 상수 integer로 변경 * feat: 과제 채점 로직 반영 * feat: 과제 제출 컨트롤러 추가 * test: 과제 채점기 테스트 추가 * feat: 스터디 상수 추가 * docs: 투두 추가 * test: GithubClient 모킹 * feat: 과제 발행 유틸 메서드 추가 * test: 과제 제출 통합 테스트 추가 * docs: 주요 로그 추가 * refactor: private 상수로 변경 --- .../api/StudentStudyHistoryController.java | 7 ++ .../StudentStudyHistoryService.java | 22 +++- .../study/domain/AssignmentHistory.java | 4 +- .../study/domain/AssignmentHistoryGrader.java | 52 +++++++++ .../study/domain/AssignmentSubmission.java | 5 + .../AssignmentSubmissionFetchExecutor.java | 8 ++ .../domain/AssignmentSubmissionFetcher.java | 9 ++ .../gdsc/domain/study/domain/StudyDetail.java | 2 +- .../study/domain/SubmissionFailureType.java | 4 +- .../gdsc/global/exception/ErrorCode.java | 1 - .../infra/github/client/GithubClient.java | 33 ++++-- .../GithubAssignmentSubmissionResponse.java | 5 - .../StudentStudyHistoryServiceTest.java | 88 +++++++++++++++ .../domain/AssignmentHistoryGraderTest.java | 100 ++++++++++++++++++ .../global/common/constant/StudyConstant.java | 5 +- .../gdsc/helper/IntegrationTest.java | 9 ++ 16 files changed, 330 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistoryGrader.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmission.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionFetchExecutor.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionFetcher.java delete mode 100644 src/main/java/com/gdschongik/gdsc/infra/github/dto/response/GithubAssignmentSubmissionResponse.java create mode 100644 src/test/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryServiceTest.java create mode 100644 src/test/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistoryGraderTest.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyHistoryController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyHistoryController.java index 0894221fa..48faedcdf 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyHistoryController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyHistoryController.java @@ -35,4 +35,11 @@ public ResponseEntity> getAllAssignmentHistories List response = studentStudyHistoryService.getAllAssignmentHistories(studyId); return ResponseEntity.ok(response); } + + @Operation(summary = "과제 제출하기", description = "과제를 제출합니다. 제출된 과제는 채점되어 제출내역에 반영됩니다.") + @PostMapping("/submit") + public ResponseEntity submitAssignment(@RequestParam(name = "studyDetailId") Long studyDetailId) { + studentStudyHistoryService.submitAssignment(studyDetailId); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java index 3712640ac..7d4455343 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java @@ -8,6 +8,8 @@ import com.gdschongik.gdsc.domain.study.dao.StudyDetailRepository; import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository; import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; +import com.gdschongik.gdsc.domain.study.domain.AssignmentHistoryGrader; +import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionFetcher; import com.gdschongik.gdsc.domain.study.domain.Study; import com.gdschongik.gdsc.domain.study.domain.StudyAssignmentHistoryValidator; import com.gdschongik.gdsc.domain.study.domain.StudyDetail; @@ -21,6 +23,7 @@ import java.io.IOException; import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.kohsuke.github.GHRepository; @@ -39,6 +42,7 @@ public class StudentStudyHistoryService { private final AssignmentHistoryRepository assignmentHistoryRepository; private final StudyHistoryValidator studyHistoryValidator; private final StudyAssignmentHistoryValidator studyAssignmentHistoryValidator; + private final AssignmentHistoryGrader assignmentHistoryGrader; @Transactional public void updateRepository(Long studyHistoryId, RepositoryUpdateRequest request) throws IOException { @@ -52,6 +56,7 @@ public void updateRepository(Long studyHistoryId, RepositoryUpdateRequest reques assignmentHistoryRepository.existsSubmittedAssignmentByMemberAndStudy(currentMember, study); String ownerRepo = getOwnerRepo(request.repositoryLink()); GHRepository repository = githubClient.getRepository(ownerRepo); + // TODO: GHRepository 등을 wrapper로 감싸서 테스트 가능하도록 변경 studyHistoryValidator.validateUpdateRepository( isAnyAssignmentSubmitted, String.valueOf(repository.getOwner().getId()), currentMember.getOauthId()); @@ -85,16 +90,27 @@ public void submitAssignment(Long studyDetailId) { StudyDetail studyDetail = studyDetailRepository .findById(studyDetailId) .orElseThrow(() -> new CustomException(STUDY_DETAIL_NOT_FOUND)); - boolean isAppliedToStudy = studyHistoryRepository.existsByMenteeAndStudy(currentMember, studyDetail.getStudy()); + Optional studyHistory = + studyHistoryRepository.findByMenteeAndStudy(currentMember, studyDetail.getStudy()); LocalDateTime now = LocalDateTime.now(); AssignmentHistory assignmentHistory = findOrCreate(currentMember, studyDetail); - studyAssignmentHistoryValidator.validateSubmitAvailable(isAppliedToStudy, now, studyDetail); + studyAssignmentHistoryValidator.validateSubmitAvailable(studyHistory.isPresent(), now, studyDetail); - // TODO: 과제 채점 및 과제이력 업데이트 로직 추가 + AssignmentSubmissionFetcher fetcher = githubClient.getLatestAssignmentSubmissionFetcher( + studyHistory.get().getRepositoryLink(), Math.toIntExact(studyDetail.getWeek())); + + assignmentHistoryGrader.judge(fetcher, assignmentHistory); assignmentHistoryRepository.save(assignmentHistory); + + log.info( + "[StudyHistoryService] 과제 제출: studyDetailId={}, menteeId={}, submissionStatus={}, submissionFailureType={}", + studyDetailId, + currentMember.getId(), + assignmentHistory.getSubmissionStatus(), + assignmentHistory.getSubmissionFailureType()); } private AssignmentHistory findOrCreate(Member currentMember, StudyDetail studyDetail) { 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 index 0316c6652..bc34ae1fd 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistory.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistory.java @@ -46,7 +46,7 @@ public class AssignmentHistory extends BaseEntity { private String commitHash; - private Long contentLength; + private Integer contentLength; private LocalDateTime committedAt; @@ -85,7 +85,7 @@ public boolean isSubmitted() { // 데이터 변경 로직 - public void success(String submissionLink, String commitHash, Long contentLength, LocalDateTime committedAt) { + public void success(String submissionLink, String commitHash, Integer contentLength, LocalDateTime committedAt) { this.submissionLink = submissionLink; this.commitHash = commitHash; this.contentLength = contentLength; diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistoryGrader.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistoryGrader.java new file mode 100644 index 000000000..78ab2c592 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistoryGrader.java @@ -0,0 +1,52 @@ +package com.gdschongik.gdsc.domain.study.domain; + +import static com.gdschongik.gdsc.domain.study.domain.SubmissionFailureType.*; +import static com.gdschongik.gdsc.global.exception.ErrorCode.*; + +import com.gdschongik.gdsc.global.annotation.DomainService; +import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.exception.ErrorCode; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@DomainService +public class AssignmentHistoryGrader { + + private static final int MINIMUM_ASSIGNMENT_CONTENT_LENGTH = 300; + + public void judge(AssignmentSubmissionFetcher assignmentSubmissionFetcher, AssignmentHistory assignmentHistory) { + try { + AssignmentSubmission assignmentSubmission = assignmentSubmissionFetcher.fetch(); + judgeAssignmentSubmission(assignmentSubmission, assignmentHistory); + } catch (CustomException e) { + SubmissionFailureType failureType = translateException(e); + assignmentHistory.fail(failureType); + } + } + + private void judgeAssignmentSubmission( + AssignmentSubmission assignmentSubmission, AssignmentHistory assignmentHistory) { + if (assignmentSubmission.contentLength() < MINIMUM_ASSIGNMENT_CONTENT_LENGTH) { + assignmentHistory.fail(WORD_COUNT_INSUFFICIENT); + return; + } + + assignmentHistory.success( + assignmentSubmission.url(), + assignmentSubmission.commitHash(), + assignmentSubmission.contentLength(), + assignmentSubmission.committedAt()); + } + + private SubmissionFailureType translateException(CustomException e) { + ErrorCode errorCode = e.getErrorCode(); + + if (errorCode == GITHUB_CONTENT_NOT_FOUND) { + return LOCATION_UNIDENTIFIABLE; + } + + log.warn("[AssignmentHistoryGrader] 과제 제출정보 조회 중 알 수 없는 오류 발생: {}", e.getMessage()); + + return UNKNOWN; + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmission.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmission.java new file mode 100644 index 000000000..307db0cbe --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmission.java @@ -0,0 +1,5 @@ +package com.gdschongik.gdsc.domain.study.domain; + +import java.time.LocalDateTime; + +public record AssignmentSubmission(String url, String commitHash, Integer contentLength, LocalDateTime committedAt) {} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionFetchExecutor.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionFetchExecutor.java new file mode 100644 index 000000000..32eab4e9c --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionFetchExecutor.java @@ -0,0 +1,8 @@ +package com.gdschongik.gdsc.domain.study.domain; + +import com.gdschongik.gdsc.global.exception.CustomException; + +@FunctionalInterface +public interface AssignmentSubmissionFetchExecutor { + AssignmentSubmission execute(String repo, int week) throws CustomException; +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionFetcher.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionFetcher.java new file mode 100644 index 000000000..3f19e5d12 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionFetcher.java @@ -0,0 +1,9 @@ +package com.gdschongik.gdsc.domain.study.domain; + +import com.gdschongik.gdsc.global.exception.CustomException; + +public record AssignmentSubmissionFetcher(String repo, int week, AssignmentSubmissionFetchExecutor fetchExecutor) { + public AssignmentSubmission fetch() throws CustomException { + return fetchExecutor.execute(repo, week); + } +} 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 f8aa0244f..020ee5dea 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 @@ -32,7 +32,7 @@ public class StudyDetail extends BaseEntity { private Study study; @Comment("현 주차수") - private Long week; + private Long week; // TODO: Integer로 변경 private String attendanceNumber; 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 index bd875f25f..74e38fb92 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/SubmissionFailureType.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/SubmissionFailureType.java @@ -9,7 +9,9 @@ public enum SubmissionFailureType { NONE("실패 없음"), // 제출상태 성공 시 사용 NOT_SUBMITTED("미제출"), // 기본값 WORD_COUNT_INSUFFICIENT("글자수 부족"), - LOCATION_UNIDENTIFIABLE("위치 확인불가"); + LOCATION_UNIDENTIFIABLE("위치 확인불가"), + UNKNOWN("알 수 없음"), + ; private final String value; } 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 69777c0f7..7982912ab 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -167,7 +167,6 @@ public enum ErrorCode { GITHUB_CONTENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 파일입니다."), GITHUB_FILE_READ_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "깃허브 파일 읽기에 실패했습니다."), GITHUB_COMMIT_DATE_FETCH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "깃허브 커밋 날짜 조회에 실패했습니다."), - GITHUB_ASSIGNMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 과제 파일입니다."), ; private final HttpStatus status; private final String message; diff --git a/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java b/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java index 34dd73f9c..869a2a6ec 100644 --- a/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java +++ b/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java @@ -3,13 +3,14 @@ import static com.gdschongik.gdsc.global.common.constant.GithubConstant.*; import static com.gdschongik.gdsc.global.exception.ErrorCode.*; +import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmission; +import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionFetchExecutor; +import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionFetcher; import com.gdschongik.gdsc.global.exception.CustomException; -import com.gdschongik.gdsc.infra.github.dto.response.GithubAssignmentSubmissionResponse; import java.io.IOException; import java.io.InputStream; import java.time.LocalDateTime; import java.time.ZoneId; -import java.util.Date; import lombok.RequiredArgsConstructor; import org.kohsuke.github.GHCommit; import org.kohsuke.github.GHContent; @@ -31,7 +32,17 @@ public GHRepository getRepository(String ownerRepo) { } } - public GithubAssignmentSubmissionResponse getLatestAssignmentSubmission(String repo, int week) { + /** + * 직접 요청을 수행하는 대신, fetcher를 통해 요청을 수행합니다. + * 요청 수행 시 발생하는 예외의 경우 과제 채점에 사용되므로, 실제 요청은 채점 로직 내부에서 수행되어야 합니다. + * 따라서 지연 평가가 가능하도록 {@link AssignmentSubmissionFetchExecutor}를 인자로 받습니다. + * 또한, 인자로 전달된 repo와 week가 closure로 캡쳐되지 않도록 fetcher 내부에 컨텍스트로 저장합니다. + */ + public AssignmentSubmissionFetcher getLatestAssignmentSubmissionFetcher(String repo, int week) { + return new AssignmentSubmissionFetcher(repo, week, this::getLatestAssignmentSubmission); + } + + private AssignmentSubmission getLatestAssignmentSubmission(String repo, int week) { GHRepository ghRepository = getRepository(repo); String assignmentPath = GITHUB_ASSIGNMENT_PATH.formatted(week); @@ -47,12 +58,10 @@ public GithubAssignmentSubmissionResponse getLatestAssignmentSubmission(String r .iterator() .next(); - LocalDateTime committedAt = getCommitDate(ghLatestCommit) - .toInstant() - .atZone(ZoneId.systemDefault()) - .toLocalDateTime(); + LocalDateTime committedAt = getCommitDate(ghLatestCommit); - return new GithubAssignmentSubmissionResponse(ghLatestCommit.getSHA1(), content.length(), committedAt); + return new AssignmentSubmission( + ghContent.getHtmlUrl(), ghLatestCommit.getSHA1(), content.length(), committedAt); } private GHContent getFileContent(GHRepository ghRepository, String filePath) { @@ -71,9 +80,13 @@ private String readFileContent(GHContent ghContent) { } } - private Date getCommitDate(GHCommit ghLatestCommit) { + private LocalDateTime getCommitDate(GHCommit ghLatestCommit) { try { - return ghLatestCommit.getCommitDate(); + return ghLatestCommit + .getCommitDate() + .toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); } catch (IOException e) { throw new CustomException(GITHUB_COMMIT_DATE_FETCH_FAILED); } diff --git a/src/main/java/com/gdschongik/gdsc/infra/github/dto/response/GithubAssignmentSubmissionResponse.java b/src/main/java/com/gdschongik/gdsc/infra/github/dto/response/GithubAssignmentSubmissionResponse.java deleted file mode 100644 index da64c9994..000000000 --- a/src/main/java/com/gdschongik/gdsc/infra/github/dto/response/GithubAssignmentSubmissionResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.gdschongik.gdsc.infra.github.dto.response; - -import java.time.LocalDateTime; - -public record GithubAssignmentSubmissionResponse(String commitHash, Integer size, LocalDateTime committedAt) {} diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryServiceTest.java new file mode 100644 index 000000000..9aa0ad9ca --- /dev/null +++ b/src/test/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryServiceTest.java @@ -0,0 +1,88 @@ +package com.gdschongik.gdsc.domain.study.application; + +import static com.gdschongik.gdsc.global.common.constant.StudyConstant.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +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.AssignmentHistoryRepository; +import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository; +import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; +import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmission; +import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionFetcher; +import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus; +import com.gdschongik.gdsc.domain.study.domain.Study; +import com.gdschongik.gdsc.domain.study.domain.StudyDetail; +import com.gdschongik.gdsc.domain.study.domain.StudyHistory; +import com.gdschongik.gdsc.helper.IntegrationTest; +import java.time.LocalDateTime; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +class StudentStudyHistoryServiceTest extends IntegrationTest { + + @Autowired + private StudentStudyHistoryService studentStudyHistoryService; + + @Autowired + private StudyHistoryRepository studyHistoryRepository; + + @Autowired + private AssignmentHistoryRepository assignmentHistoryRepository; + + private void setCurrentTime(LocalDateTime now) { + try (MockedStatic mock = Mockito.mockStatic(LocalDateTime.class, Mockito.CALLS_REAL_METHODS)) { + mock.when(LocalDateTime::now).thenReturn(now); + } + } + + @Nested + class 과제_제출할때 { + + @Test + void 성공한다() { + // given + Member mentor = createAssociateMember(); + // TODO: LocalDateTime.now() 관련 테스트 정책 논의 필요 + LocalDateTime now = LocalDateTime.now(); // 통합 테스트에서는 LocalDateTime.now()를 사용해야 함 + Study study = createStudy( + mentor, + Period.createPeriod(now.minusWeeks(1), now.plusWeeks(7)), // 스터디 기간: 1주 전 ~ 7주 후 + Period.createPeriod(now.minusWeeks(2), now.minusWeeks(1))); // 수강신청 기간: 2주 전 ~ 1주 전 + StudyDetail studyDetail = + createStudyDetail(study, now.minusDays(6), now.plusDays(1)); // 1주차 기간: 6일 전 ~ 1일 후 + publishAssignment(studyDetail); + + Member student = createRegularMember(); + logoutAndReloginAs(student.getId(), MemberRole.REGULAR); + + // 수강신청 valiadtion 로직이 LocalDateTime.now() 기준으로 동작하기 때문에 직접 수강신청 생성 + StudyHistory studyHistory = StudyHistory.create(student, study); + studyHistory.updateRepositoryLink(REPOSITORY_LINK); + studyHistoryRepository.save(studyHistory); + + // 제출정보 조회 fetcher stubbing + AssignmentSubmissionFetcher mockFetcher = mock(AssignmentSubmissionFetcher.class); + when(mockFetcher.fetch()) + .thenReturn(new AssignmentSubmission(REPOSITORY_LINK, COMMIT_HASH, 500, COMMITTED_AT)); + when(githubClient.getLatestAssignmentSubmissionFetcher(anyString(), anyInt())) + .thenReturn(mockFetcher); + + // when + studentStudyHistoryService.submitAssignment(studyDetail.getId()); + + // then + AssignmentHistory assignmentHistory = + assignmentHistoryRepository.findById(1L).orElseThrow(); + assertThat(assignmentHistory.getSubmissionStatus()).isEqualTo(AssignmentSubmissionStatus.SUCCESS); + assertThat(assignmentHistory.getSubmissionLink()).isEqualTo(REPOSITORY_LINK); + assertThat(assignmentHistory.getCommitHash()).isEqualTo(COMMIT_HASH); + assertThat(assignmentHistory.getContentLength()).isEqualTo(500); + } + } +} diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistoryGraderTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistoryGraderTest.java new file mode 100644 index 000000000..671488c27 --- /dev/null +++ b/src/test/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistoryGraderTest.java @@ -0,0 +1,100 @@ +package com.gdschongik.gdsc.domain.study.domain; + +import static com.gdschongik.gdsc.global.common.constant.StudyConstant.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.gdschongik.gdsc.domain.recruitment.domain.vo.Period; +import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.exception.ErrorCode; +import com.gdschongik.gdsc.helper.FixtureHelper; +import java.time.LocalDateTime; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class AssignmentHistoryGraderTest { + + FixtureHelper fixtureHelper = new FixtureHelper(); + AssignmentHistoryGrader grader = new AssignmentHistoryGrader(); + + AssignmentSubmissionFetcher mockFetcher = mock(AssignmentSubmissionFetcher.class); + + // FixtureHelper 래핑 메서드 + private AssignmentHistory createAssignmentHistory() { + Study study = createStudyWithMentor(1L); + StudyDetail studyDetail = fixtureHelper.createStudyDetail( + study, LocalDateTime.now(), LocalDateTime.now().plusDays(7)); + return AssignmentHistory.create(studyDetail, fixtureHelper.createAssociateMember(2L)); + } + + private Study createStudyWithMentor(Long mentorId) { + Period period = Period.createPeriod(STUDY_START_DATETIME, STUDY_END_DATETIME); + Period applicationPeriod = + Period.createPeriod(STUDY_START_DATETIME.minusDays(7), STUDY_START_DATETIME.minusDays(1)); + return fixtureHelper.createStudyWithMentor(mentorId, period, applicationPeriod); + } + + @Nested + class 과제_채점시 { + + @Test + void 과제내용이_최소길이_이상이면_성공_처리된다() { + // given + AssignmentHistory history = createAssignmentHistory(); + AssignmentSubmission validSubmission = new AssignmentSubmission("url", "hash", 500, LocalDateTime.now()); + when(mockFetcher.fetch()).thenReturn(validSubmission); + + // when + grader.judge(mockFetcher, history); + + // then + assertThat(history.getSubmissionStatus()).isEqualTo(AssignmentSubmissionStatus.SUCCESS); + assertThat(history.getSubmissionLink()).isEqualTo("url"); + assertThat(history.getCommitHash()).isEqualTo("hash"); + assertThat(history.getContentLength()).isEqualTo(500); + } + + @Test + void 과제내용이_최소길이_미만이면_실패_처리된다() { + // given + AssignmentHistory history = createAssignmentHistory(); + AssignmentSubmission shortSubmission = new AssignmentSubmission("url", "hash", 200, LocalDateTime.now()); + when(mockFetcher.fetch()).thenReturn(shortSubmission); + + // when + grader.judge(mockFetcher, history); + + // then + assertThat(history.getSubmissionStatus()).isEqualTo(AssignmentSubmissionStatus.FAILURE); + assertThat(history.getSubmissionFailureType()).isEqualTo(SubmissionFailureType.WORD_COUNT_INSUFFICIENT); + } + + @Test + void 해당_위치에_과제파일_미존재시_위치확인불가로_실패_처리된다() { + // given + AssignmentHistory history = createAssignmentHistory(); + when(mockFetcher.fetch()).thenThrow(new CustomException(ErrorCode.GITHUB_CONTENT_NOT_FOUND)); + + // when + grader.judge(mockFetcher, history); + + // then + assertThat(history.getSubmissionStatus()).isEqualTo(AssignmentSubmissionStatus.FAILURE); + assertThat(history.getSubmissionFailureType()).isEqualTo(SubmissionFailureType.LOCATION_UNIDENTIFIABLE); + } + + @Test + void 그외_Github_문제인경우_알수없는오류로_실패_처리된다() { + // given + AssignmentHistory history = createAssignmentHistory(); + when(mockFetcher.fetch()).thenThrow(new CustomException(ErrorCode.GITHUB_FILE_READ_FAILED)); + + // when + grader.judge(mockFetcher, history); + + // then + assertThat(history.getSubmissionStatus()).isEqualTo(AssignmentSubmissionStatus.FAILURE); + assertThat(history.getSubmissionFailureType()).isEqualTo(SubmissionFailureType.UNKNOWN); + } + } +} 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 02588410d..19714d487 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 @@ -41,6 +41,9 @@ private StudyConstant() {} // AssignmentHistory public static final String SUBMISSION_LINK = "https://github.com/ownername/reponame/blob/main/week1/WIL.md"; public static final String COMMIT_HASH = "aa11bb22cc33"; - public static final Long CONTENT_LENGTH = 2000L; + public static final Integer CONTENT_LENGTH = 2000; public static final LocalDateTime COMMITTED_AT = LocalDateTime.of(2024, 9, 8, 0, 0); + + // StudyHistory + public static final String REPOSITORY_LINK = "ownername/reponame"; } diff --git a/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java b/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java index fc0e07f8d..1bfbac94d 100644 --- a/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java +++ b/src/test/java/com/gdschongik/gdsc/helper/IntegrationTest.java @@ -36,6 +36,7 @@ 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 com.gdschongik.gdsc.infra.github.client.GithubClient; import java.time.LocalDateTime; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; @@ -86,6 +87,9 @@ public abstract class IntegrationTest { @MockBean protected PaymentClient paymentClient; + @MockBean + protected GithubClient githubClient; + @MockBean protected DelegateMemberDiscordEventHandler delegateMemberDiscordEventHandler; @@ -257,4 +261,9 @@ protected StudyDetail createNewStudyDetail(Long week, Study study, LocalDateTime StudyDetail.createStudyDetail(study, week, ATTENDANCE_NUMBER, Period.createPeriod(startDate, endDate)); return studyDetailRepository.save(studyDetail); } + + protected StudyDetail publishAssignment(StudyDetail studyDetail) { + studyDetail.publishAssignment(ASSIGNMENT_TITLE, studyDetail.getPeriod().getEndDate(), DESCRIPTION_LINK); + return studyDetailRepository.save(studyDetail); + } } From ae66f5ef21ca9fb34a1f5c8b01c6fc585aa79619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=8A=AC=EA=B8=B0?= <84620016+seulgi99@users.noreply.github.com> Date: Wed, 21 Aug 2024 08:23:12 +0900 Subject: [PATCH 24/40] =?UTF-8?q?fix:=20Time=EA=B4=80=EB=A0=A8=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20Deserializer=20=EB=A7=B5=ED=8D=BC=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#658)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Time관련 필드 Deserializer 맵퍼에 추가 * refactor: 주석 제거 * refactor: Serializer, Deserializer 클래스로 분리 --- .../global/config/ObjectMapperConfig.java | 98 ++++++++++++++++++- 1 file changed, 96 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/global/config/ObjectMapperConfig.java b/src/main/java/com/gdschongik/gdsc/global/config/ObjectMapperConfig.java index 44fbbb5f0..ce69b3e8a 100644 --- a/src/main/java/com/gdschongik/gdsc/global/config/ObjectMapperConfig.java +++ b/src/main/java/com/gdschongik/gdsc/global/config/ObjectMapperConfig.java @@ -1,7 +1,22 @@ package com.gdschongik.gdsc.global.config; +import static com.gdschongik.gdsc.global.common.constant.RegexConstant.DATE; +import static com.gdschongik.gdsc.global.common.constant.RegexConstant.DATETIME; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -10,7 +25,86 @@ public class ObjectMapperConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new JavaTimeModule()); + SimpleModule module = new SimpleModule(); + + // LocalDate + module.addSerializer(LocalDate.class, new LocalDateSerializer()); + module.addDeserializer(LocalDate.class, new LocalDateDeserializer()); + + // LocalDateTime + module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer()); + module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer()); + + // LocalTime + module.addSerializer(LocalTime.class, new LocalTimeSerializer()); + module.addDeserializer(LocalTime.class, new LocalTimeDeserializer()); + + mapper.registerModule(module); return mapper; } + + public class LocalDateSerializer extends JsonSerializer { + + @Override + public void serialize(LocalDate value, JsonGenerator generator, SerializerProvider serializers) + throws IOException { + generator.writeString(value.format(DateTimeFormatter.ofPattern(DATE))); + } + } + + public class LocalDateDeserializer extends JsonDeserializer { + @Override + public LocalDate deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + return LocalDate.parse(jsonParser.getValueAsString(), DateTimeFormatter.ofPattern(DATE)); + } + } + + public class LocalDateTimeSerializer extends JsonSerializer { + + @Override + public void serialize(LocalDateTime value, JsonGenerator generator, SerializerProvider serializers) + throws IOException { + generator.writeString(value.format(DateTimeFormatter.ofPattern(DATETIME))); + } + } + + public class LocalDateTimeDeserializer extends JsonDeserializer { + @Override + public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + return LocalDateTime.parse(jsonParser.getValueAsString(), DateTimeFormatter.ofPattern(DATETIME)); + } + } + + public class LocalTimeSerializer extends JsonSerializer { + + @Override + public void serialize(LocalTime value, JsonGenerator generator, SerializerProvider serializers) + throws IOException { + generator.writeStartObject(); + + generator.writeNumberField("hour", value.getHour()); + generator.writeNumberField("minute", value.getMinute()); + generator.writeNumberField("second", value.getSecond()); + generator.writeNumberField("nano", value.getNano()); + + generator.writeEndObject(); + } + } + + public class LocalTimeDeserializer extends JsonDeserializer { + @Override + public LocalTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + + int hour = node.get("hour").asInt(); + int minute = node.get("minute").asInt(); + int second = node.get("second").asInt(); + int nano = node.get("nano").asInt(); + + return LocalTime.of(hour, minute, second, nano); + } + } } From 554d567132a787d71831873d58e240f8a1b28f36 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Wed, 21 Aug 2024 15:06:45 +0900 Subject: [PATCH 25/40] =?UTF-8?q?feat:=20=EB=82=98=EC=9D=98=20=EC=88=98?= =?UTF-8?q?=EA=B0=95=EC=A4=91=EC=9D=B8=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80=20(#656)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 나의 수강중인 스터디 조회 api 추가 * fix: 진행중인 스터디만 취급하도록 수정 * rename: 메서드 및 dto 이름 변경 --- .../domain/study/api/StudentStudyController.java | 8 ++++++++ .../study/application/StudentStudyService.java | 11 +++++++++++ .../dto/response/StudentMyCurrentStudyResponse.java | 12 ++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudentMyCurrentStudyResponse.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyController.java index 2d85082e2..0ddff5715 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyController.java @@ -2,6 +2,7 @@ import com.gdschongik.gdsc.domain.study.application.StudentStudyService; import com.gdschongik.gdsc.domain.study.dto.request.StudyAttendCreateRequest; +import com.gdschongik.gdsc.domain.study.dto.response.StudentMyCurrentStudyResponse; import com.gdschongik.gdsc.domain.study.dto.response.StudyApplicableResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -52,4 +53,11 @@ public ResponseEntity attend( studentStudyService.attend(studyDetailId, request); return ResponseEntity.ok().build(); } + + @Operation(summary = "내 수강중인 스터디 조회", description = "나의 수강 중인 스터디를 조회합니다.") + @GetMapping("/me/ongoing") + public ResponseEntity getMyCurrentStudy() { + StudentMyCurrentStudyResponse response = studentStudyService.getMyCurrentStudy(); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java index 4da76a819..8f2743706 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java @@ -11,6 +11,7 @@ import com.gdschongik.gdsc.domain.study.domain.Attendance; import com.gdschongik.gdsc.domain.study.domain.AttendanceValidator; import com.gdschongik.gdsc.domain.study.dto.request.StudyAttendCreateRequest; +import com.gdschongik.gdsc.domain.study.dto.response.StudentMyCurrentStudyResponse; import com.gdschongik.gdsc.domain.study.dto.response.StudyApplicableResponse; import com.gdschongik.gdsc.domain.study.dto.response.StudyResponse; import com.gdschongik.gdsc.global.exception.CustomException; @@ -100,4 +101,14 @@ public void attend(Long studyDetailId, StudyAttendCreateRequest request) { log.info("[StudyService] 스터디 출석: attendanceId={}", attendance.getId()); } + + @Transactional(readOnly = true) + public StudentMyCurrentStudyResponse getMyCurrentStudy() { + Member currentMember = memberUtil.getCurrentMember(); + StudyHistory studyHistory = studyHistoryRepository.findAllByMentee(currentMember).stream() + .filter(s -> s.getStudy().isStudyOngoing()) + .findFirst() + .orElse(null); + return StudentMyCurrentStudyResponse.from(studyHistory); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudentMyCurrentStudyResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudentMyCurrentStudyResponse.java new file mode 100644 index 000000000..707c68703 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudentMyCurrentStudyResponse.java @@ -0,0 +1,12 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import com.gdschongik.gdsc.domain.study.domain.StudyHistory; + +public record StudentMyCurrentStudyResponse(Long studyId) { + public static StudentMyCurrentStudyResponse from(StudyHistory studyHistory) { + if (studyHistory == null) { + return new StudentMyCurrentStudyResponse(null); + } + return new StudentMyCurrentStudyResponse(studyHistory.getStudy().getId()); + } +} From 50db98c118f388b7f6f9a00b52ef195cd0b1eed2 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Wed, 21 Aug 2024 21:51:39 +0900 Subject: [PATCH 26/40] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C=20=EC=8B=9C=EA=B0=84=EA=B3=BC=20=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20=EC=A2=85=EB=A3=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#660)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gdsc/domain/study/dto/response/StudyResponse.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyResponse.java index d7274ad3e..e1594935b 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyResponse.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyResponse.java @@ -18,8 +18,10 @@ public record StudyResponse( @Schema(description = "멘토 이름") String mentorName, @Schema(description = "스터디 요일") DayOfWeek dayOfWeek, @Schema(description = "스터디 시작 시간") LocalTime startTime, + @Schema(description = "스터디 종료 시간") LocalTime endTime, @Schema(description = "총 주차수") Long totalWeek, - @Schema(description = "개강일") LocalDateTime openingDate) { + @Schema(description = "개강일") LocalDateTime openingDate, + @Schema(description = "신청 종료일") LocalDateTime applicationEndDate) { public static StudyResponse from(Study study) { return new StudyResponse( @@ -33,7 +35,9 @@ public static StudyResponse from(Study study) { study.getMentor().getName(), study.getDayOfWeek(), study.getStartTime(), + study.getEndTime(), study.getTotalWeek(), - study.getPeriod().getStartDate()); + study.getPeriod().getStartDate(), + study.getApplicationPeriod().getEndDate()); } } From 18276221938814b0efad45efa03d11bfbdfd5f2d Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Wed, 21 Aug 2024 21:53:36 +0900 Subject: [PATCH 27/40] =?UTF-8?q?refactor:=20Mentee=EB=A5=BC=20Student?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95=20(#661)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rename: mentee를 student로 수정 --- .../domain/study/application/CommonStudyService.java | 2 +- .../study/application/StudentStudyDetailService.java | 4 ++-- .../study/application/StudentStudyHistoryService.java | 7 +++---- .../domain/study/application/StudentStudyService.java | 10 +++++----- .../study/dao/AssignmentHistoryCustomRepository.java | 2 +- .../dao/AssignmentHistoryCustomRepositoryImpl.java | 2 +- .../gdsc/domain/study/dao/StudyHistoryRepository.java | 9 ++++----- .../gdsc/domain/study/domain/StudyHistory.java | 10 +++++----- .../study/dto/response/StudyStudentResponse.java | 10 +++++----- .../domain/study/domain/StudyHistoryValidatorTest.java | 8 ++++---- 10 files changed, 31 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/CommonStudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/CommonStudyService.java index ae45dbd70..6bcf2aae9 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/CommonStudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/CommonStudyService.java @@ -42,7 +42,7 @@ public CommonStudyResponse getStudyInformation(Long studyId) { public List getStudyAnnouncements(Long studyId) { Member currentMember = memberUtil.getCurrentMember(); final Study study = studyRepository.findById(studyId).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND)); - Optional studyHistory = studyHistoryRepository.findByMenteeAndStudyId(currentMember, studyId); + Optional studyHistory = studyHistoryRepository.findByStudentAndStudyId(currentMember, studyId); studyValidator.validateStudyMentorOrStudent(currentMember, study, studyHistory); diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java index 36fac587c..c415c75a0 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java @@ -27,11 +27,11 @@ public class StudentStudyDetailService { public AssignmentDashboardResponse getSubmittableAssignments(Long studyId) { Member currentMember = memberUtil.getCurrentMember(); StudyHistory studyHistory = studyHistoryRepository - .findByMenteeAndStudyId(currentMember, studyId) + .findByStudentAndStudyId(currentMember, studyId) .orElseThrow(() -> new CustomException(ErrorCode.STUDY_HISTORY_NOT_FOUND)); List assignmentHistories = - assignmentHistoryRepository.findAssignmentHistoriesByMenteeAndStudy(currentMember, studyId); + assignmentHistoryRepository.findAssignmentHistoriesByStudentAndStudy(currentMember, studyId); boolean isAnySubmitted = assignmentHistories.stream().anyMatch(AssignmentHistory::isSubmitted); List submittableAssignments = assignmentHistories.stream() .filter(assignmentHistory -> assignmentHistory.getStudyDetail().isAssignmentDeadlineRemaining()) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java index 7d4455343..1dea2ee3e 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java @@ -74,12 +74,11 @@ private String getOwnerRepo(String repositoryLink) { return repositoryLink.substring(startIndex); } - // TODO mentee -> study 변환 작업 필요 @Transactional(readOnly = true) public List getAllAssignmentHistories(Long studyId) { Member currentMember = memberUtil.getCurrentMember(); - return assignmentHistoryRepository.findAssignmentHistoriesByMenteeAndStudy(currentMember, studyId).stream() + return assignmentHistoryRepository.findAssignmentHistoriesByStudentAndStudy(currentMember, studyId).stream() .map(AssignmentHistoryResponse::from) .toList(); } @@ -91,7 +90,7 @@ public void submitAssignment(Long studyDetailId) { .findById(studyDetailId) .orElseThrow(() -> new CustomException(STUDY_DETAIL_NOT_FOUND)); Optional studyHistory = - studyHistoryRepository.findByMenteeAndStudy(currentMember, studyDetail.getStudy()); + studyHistoryRepository.findByStudentAndStudy(currentMember, studyDetail.getStudy()); LocalDateTime now = LocalDateTime.now(); AssignmentHistory assignmentHistory = findOrCreate(currentMember, studyDetail); @@ -106,7 +105,7 @@ public void submitAssignment(Long studyDetailId) { assignmentHistoryRepository.save(assignmentHistory); log.info( - "[StudyHistoryService] 과제 제출: studyDetailId={}, menteeId={}, submissionStatus={}, submissionFailureType={}", + "[StudyHistoryService] 과제 제출: studyDetailId={}, studentId={}, submissionStatus={}, submissionFailureType={}", studyDetailId, currentMember.getId(), assignmentHistory.getSubmissionStatus(), diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java index 8f2743706..ba5a997e8 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyService.java @@ -40,7 +40,7 @@ public class StudentStudyService { public StudyApplicableResponse getAllApplicableStudies() { Member currentMember = memberUtil.getCurrentMember(); - List studyHistories = studyHistoryRepository.findAllByMentee(currentMember); + List studyHistories = studyHistoryRepository.findAllByStudent(currentMember); Optional appliedStudy = studyHistories.stream() .map(StudyHistory::getStudy) .filter(Study::isStudyOngoing) @@ -58,7 +58,7 @@ public void applyStudy(Long studyId) { Study study = studyRepository.findById(studyId).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND)); Member currentMember = memberUtil.getCurrentMember(); - List currentMemberStudyHistories = studyHistoryRepository.findAllByMentee(currentMember); + List currentMemberStudyHistories = studyHistoryRepository.findAllByStudent(currentMember); studyHistoryValidator.validateApplyStudy(study, currentMemberStudyHistories); @@ -76,7 +76,7 @@ public void cancelStudyApply(Long studyId) { studyHistoryValidator.validateCancelStudyApply(study); StudyHistory studyHistory = studyHistoryRepository - .findByMenteeAndStudy(currentMember, study) + .findByStudentAndStudy(currentMember, study) .orElseThrow(() -> new CustomException(STUDY_HISTORY_NOT_FOUND)); studyHistoryRepository.delete(studyHistory); @@ -91,7 +91,7 @@ public void attend(Long studyDetailId, StudyAttendCreateRequest request) { final Member currentMember = memberUtil.getCurrentMember(); final Study study = studyDetail.getStudy(); final StudyHistory studyHistory = studyHistoryRepository - .findByMenteeAndStudy(currentMember, study) + .findByStudentAndStudy(currentMember, study) .orElseThrow(() -> new CustomException(STUDY_HISTORY_NOT_FOUND)); attendanceValidator.validateAttendance(studyDetail, request.attendanceNumber(), LocalDate.now()); @@ -105,7 +105,7 @@ public void attend(Long studyDetailId, StudyAttendCreateRequest request) { @Transactional(readOnly = true) public StudentMyCurrentStudyResponse getMyCurrentStudy() { Member currentMember = memberUtil.getCurrentMember(); - StudyHistory studyHistory = studyHistoryRepository.findAllByMentee(currentMember).stream() + StudyHistory studyHistory = studyHistoryRepository.findAllByStudent(currentMember).stream() .filter(s -> s.getStudy().isStudyOngoing()) .findFirst() .orElse(null); diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java index c0edd00ec..5fa5fc2d9 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java @@ -9,5 +9,5 @@ public interface AssignmentHistoryCustomRepository { boolean existsSubmittedAssignmentByMemberAndStudy(Member member, Study study); - List findAssignmentHistoriesByMenteeAndStudy(Member member, Long studyId); + List findAssignmentHistoriesByStudentAndStudy(Member member, Long studyId); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java index 02d3b57df..a37e68723 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java @@ -41,7 +41,7 @@ private BooleanExpression isSubmitted() { } @Override - public List findAssignmentHistoriesByMenteeAndStudy(Member currentMember, Long studyId) { + public List findAssignmentHistoriesByStudentAndStudy(Member currentMember, Long studyId) { return queryFactory .selectFrom(assignmentHistory) .join(assignmentHistory.studyDetail, studyDetail) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java index c35b95004..4bf4303ea 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java @@ -11,12 +11,11 @@ public interface StudyHistoryRepository extends JpaRepository findByStudyId(Long studyId); - // TODO mentee -> student로 변경 - List findAllByMentee(Member member); + List findAllByStudent(Member member); - Optional findByMenteeAndStudy(Member member, Study study); + Optional findByStudentAndStudy(Member member, Study study); - boolean existsByMenteeAndStudy(Member member, Study study); + boolean existsByStudentAndStudy(Member member, Study study); - Optional findByMenteeAndStudyId(Member member, Long studyId); + Optional findByStudentAndStudyId(Member member, Long studyId); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java index bd1a06b2a..52f2fa3f4 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java @@ -30,7 +30,7 @@ public class StudyHistory extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") - private Member mentee; + private Member student; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "study_id") @@ -39,13 +39,13 @@ public class StudyHistory extends BaseEntity { private String repositoryLink; @Builder(access = AccessLevel.PRIVATE) - private StudyHistory(Member mentee, Study study) { - this.mentee = mentee; + private StudyHistory(Member student, Study study) { + this.student = student; this.study = study; } - public static StudyHistory create(Member mentee, Study study) { - return StudyHistory.builder().mentee(mentee).study(study).build(); + public static StudyHistory create(Member student, Study study) { + return StudyHistory.builder().student(student).study(study).build(); } /** diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentResponse.java index 8f35cc2c7..fd616fdb2 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentResponse.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentResponse.java @@ -12,11 +12,11 @@ public record StudyStudentResponse( @Schema(description = "깃허브 링크") String githubLink) { public static StudyStudentResponse from(StudyHistory studyHistory) { return new StudyStudentResponse( - studyHistory.getMentee().getId(), - studyHistory.getMentee().getName(), - studyHistory.getMentee().getStudentId(), - studyHistory.getMentee().getDiscordUsername(), - studyHistory.getMentee().getNickname(), + studyHistory.getStudent().getId(), + studyHistory.getStudent().getName(), + studyHistory.getStudent().getStudentId(), + studyHistory.getStudent().getDiscordUsername(), + studyHistory.getStudent().getNickname(), studyHistory.getRepositoryLink()); } } diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyHistoryValidatorTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyHistoryValidatorTest.java index f72b97dba..5e6dcb2f1 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyHistoryValidatorTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyHistoryValidatorTest.java @@ -31,8 +31,8 @@ class 스터디_수강신청시 { Period applicationPeriod = Period.createPeriod(now.minusDays(10), now.plusDays(5)); Study study = fixtureHelper.createStudy(mentor, period, applicationPeriod); - Member mentee = fixtureHelper.createGuestMember(2L); - StudyHistory studyHistory = StudyHistory.create(mentee, study); + Member student = fixtureHelper.createGuestMember(2L); + StudyHistory studyHistory = StudyHistory.create(student, study); // when & then assertThatThrownBy(() -> studyHistoryValidator.validateApplyStudy(study, List.of(studyHistory))) @@ -68,8 +68,8 @@ class 스터디_수강신청시 { Study anotherStudy = fixtureHelper.createStudy(mentor, period, applicationPeriod); - Member mentee = fixtureHelper.createGuestMember(2L); - StudyHistory studyHistory = StudyHistory.create(mentee, anotherStudy); + Member student = fixtureHelper.createGuestMember(2L); + StudyHistory studyHistory = StudyHistory.create(student, anotherStudy); // when & then assertThatThrownBy(() -> studyHistoryValidator.validateApplyStudy(study, List.of(studyHistory))) From e502d83dc6be4f8ce3818c49f81af99b229d98e2 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, 21 Aug 2024 22:47:19 +0900 Subject: [PATCH 28/40] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=BB=A4=EB=A6=AC=ED=81=98=EB=9F=BC=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=EC=97=90=20=EC=83=81=EC=84=B8=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EA=B0=80=20(#668)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: 응답 필드에 스터디 세션 상세 정보 --- .../gdsc/domain/study/dto/response/StudySessionResponse.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudySessionResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudySessionResponse.java index 5c40f046e..874b6cfb5 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudySessionResponse.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudySessionResponse.java @@ -5,7 +5,8 @@ import com.gdschongik.gdsc.domain.study.domain.StudyDetail; import com.gdschongik.gdsc.domain.study.domain.vo.Session; -public record StudySessionResponse(Long studyDetailId, Period period, Long week, String title, Difficulty difficulty) { +public record StudySessionResponse( + Long studyDetailId, Period period, Long week, String title, String description, Difficulty difficulty) { public static StudySessionResponse from(StudyDetail studyDetail) { Session session = studyDetail.getSession(); @@ -14,6 +15,7 @@ public static StudySessionResponse from(StudyDetail studyDetail) { studyDetail.getPeriod(), studyDetail.getWeek(), session.getTitle(), + session.getDescription(), session.getDifficulty()); } } From 41d96a3bd408ba43142eb02788ce9a6e33429afb 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, 21 Aug 2024 22:55:05 +0900 Subject: [PATCH 29/40] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EA=B3=BC=EC=A0=9C=20=ED=9E=88=EC=8A=A4=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=EC=97=90=20=EC=A0=9C=EC=B6=9C=20=EC=8B=A4=ED=8C=A8=20=EC=82=AC?= =?UTF-8?q?=EC=9C=A0=20=EC=B6=94=EA=B0=80=20(#667)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: 응답 필드에 제출 실패 사유 추가 --- .../domain/study/dto/response/AssignmentHistoryResponse.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryResponse.java index 5152004e8..2565f1286 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryResponse.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryResponse.java @@ -2,6 +2,7 @@ import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus; +import com.gdschongik.gdsc.domain.study.domain.SubmissionFailureType; import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; @@ -12,6 +13,7 @@ public record AssignmentHistoryResponse( @Schema(description = "과제 명세 링크") String descriptionLink, @Schema(description = "과제 제출 링크") String submissionLink, @Schema(description = "과제 제출 상태") AssignmentSubmissionStatus assignmentSubmissionStatus, + @Schema(description = "과제 제출 실패 사유") SubmissionFailureType submissionFailureType, @Schema(description = "주차") Long week) { public static AssignmentHistoryResponse from(AssignmentHistory assignmentHistory) { return new AssignmentHistoryResponse( @@ -21,6 +23,7 @@ public static AssignmentHistoryResponse from(AssignmentHistory assignmentHistory assignmentHistory.getStudyDetail().getAssignment().getDescriptionLink(), assignmentHistory.getSubmissionLink(), assignmentHistory.getSubmissionStatus(), + assignmentHistory.getSubmissionFailureType(), assignmentHistory.getStudyDetail().getWeek()); } } From 334dbc30252d2f291754d9abe24f2cf273f8a0a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=8A=AC=EA=B8=B0?= <84620016+seulgi99@users.noreply.github.com> Date: Thu, 22 Aug 2024 23:16:17 +0900 Subject: [PATCH 30/40] =?UTF-8?q?feat:=20=EC=88=98=EA=B0=95=EC=83=9D=20?= =?UTF-8?q?=EC=8A=A4=ED=84=B0=EB=94=94=20=EC=BB=A4=EB=A6=AC=ED=81=98?= =?UTF-8?q?=EB=9F=BC=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80=20(#6?= =?UTF-8?q?62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 수강생 스터디 커리큘럼 조회 API 추가 * refactor: 스터디 상세 조회 dao메소드 정렬 추가 * feat: 커리큘럼 조회 response에 과제 개설, 제출, 실패타입 추가 * refactor: 오타수정 * refactor: 변경사항 반영 --- .../api/StudentStudyDetailController.java | 11 +++++ .../application/MentorStudyDetailService.java | 4 +- .../study/application/MentorStudyService.java | 2 +- .../StudentStudyDetailService.java | 41 ++++++++++++++++++ .../study/dao/AttendanceCustomRepository.java | 9 ++++ .../dao/AttendanceCustomRepositoryImpl.java | 36 ++++++++++++++++ .../study/dao/AttendanceRepository.java | 2 +- .../study/dao/StudyDetailRepository.java | 2 +- .../study/domain/AssignmentHistory.java | 4 ++ .../domain/AssignmentSubmissionStatus.java | 1 - .../domain/study/domain/vo/Assignment.java | 4 ++ .../AssignmentSubmissionStatusResponse.java | 26 ++++++++++++ .../response/AttendanceStatusResponse.java | 29 +++++++++++++ .../response/StudyStudentSessionResponse.java | 42 +++++++++++++++++++ .../application/MentorStudyServiceTest.java | 2 +- 15 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceCustomRepository.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceCustomRepositoryImpl.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentSubmissionStatusResponse.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AttendanceStatusResponse.java create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentSessionResponse.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java index 1f8eeed52..f122077f8 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java @@ -2,8 +2,10 @@ import com.gdschongik.gdsc.domain.study.application.StudentStudyDetailService; import com.gdschongik.gdsc.domain.study.dto.response.AssignmentDashboardResponse; +import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentSessionResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -26,4 +28,13 @@ public ResponseEntity getSubmittableAssignments( AssignmentDashboardResponse response = studentStudyDetailService.getSubmittableAssignments(studyId); return ResponseEntity.ok(response); } + + // TODO 스터디 세션 워딩을 커리큘럼으로 변경해야함 + @Operation(summary = "스터디 커리큘럼 조회", description = "해당 스터디의 커리큘럼들을 조회합니다.") + @GetMapping("/sessions") + public ResponseEntity> getStudySessions( + @RequestParam(name = "studyId") Long studyId) { + List response = studentStudyDetailService.getStudySessions(studyId); + return ResponseEntity.ok(response); + } } 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 48fbd4622..b6bb9dbb5 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 @@ -28,7 +28,7 @@ public class MentorStudyDetailService { @Transactional(readOnly = true) public List getWeeklyAssignments(Long studyId) { - List studyDetails = studyDetailRepository.findAllByStudyId(studyId); + List studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId); return studyDetails.stream().map(AssignmentResponse::from).toList(); } @@ -87,7 +87,7 @@ public void updateStudyAssignment(Long studyDetailId, AssignmentCreateUpdateRequ @Transactional(readOnly = true) public List getSessions(Long studyId) { - List studyDetails = studyDetailRepository.findAllByStudyId(studyId); + List studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId); return studyDetails.stream().map(StudySessionResponse::from).toList(); } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java index 67580e0bd..e4204e2b4 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java @@ -108,7 +108,7 @@ public void updateStudy(Long studyId, StudyUpdateRequest request) { Study study = studyRepository.findById(studyId).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND)); studyValidator.validateStudyMentor(currentMember, study); - List studyDetails = studyDetailRepository.findAllByStudyId(studyId); + List studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId); // StudyDetail ID를 추출하여 Set으로 저장 Set studyDetailIds = studyDetails.stream().map(StudyDetail::getId).collect(Collectors.toSet()); diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java index c415c75a0..7a57cab1b 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java @@ -2,14 +2,21 @@ import com.gdschongik.gdsc.domain.member.domain.Member; import com.gdschongik.gdsc.domain.study.dao.AssignmentHistoryRepository; +import com.gdschongik.gdsc.domain.study.dao.AttendanceRepository; +import com.gdschongik.gdsc.domain.study.dao.StudyDetailRepository; import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository; +import com.gdschongik.gdsc.domain.study.dao.StudyRepository; import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; +import com.gdschongik.gdsc.domain.study.domain.Attendance; +import com.gdschongik.gdsc.domain.study.domain.StudyDetail; import com.gdschongik.gdsc.domain.study.domain.StudyHistory; import com.gdschongik.gdsc.domain.study.dto.response.AssignmentDashboardResponse; import com.gdschongik.gdsc.domain.study.dto.response.AssignmentSubmittableDto; +import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentSessionResponse; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.exception.ErrorCode; import com.gdschongik.gdsc.global.util.MemberUtil; +import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -20,8 +27,11 @@ public class StudentStudyDetailService { private final MemberUtil memberUtil; + private final StudyRepository studyRepository; private final StudyHistoryRepository studyHistoryRepository; private final AssignmentHistoryRepository assignmentHistoryRepository; + private final StudyDetailRepository studyDetailRepository; + private final AttendanceRepository attendanceRepository; @Transactional(readOnly = true) public AssignmentDashboardResponse getSubmittableAssignments(Long studyId) { @@ -40,4 +50,35 @@ public AssignmentDashboardResponse getSubmittableAssignments(Long studyId) { return AssignmentDashboardResponse.of(studyHistory.getRepositoryLink(), isAnySubmitted, submittableAssignments); } + + @Transactional(readOnly = true) + public List getStudySessions(Long studyId) { + Member member = memberUtil.getCurrentMember(); + final List studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId); + final List assignmentHistories = + assignmentHistoryRepository.findAssignmentHistoriesByStudentAndStudy(member, studyId); + final List attendances = attendanceRepository.findByMemberAndStudyId(member, studyId); + + return studyDetails.stream() + .map(studyDetail -> StudyStudentSessionResponse.of( + studyDetail, + getSubmittedAssignment(assignmentHistories, studyDetail), + isAttended(attendances, studyDetail), + LocalDateTime.now())) + .toList(); + } + + private AssignmentHistory getSubmittedAssignment( + List assignmentHistories, StudyDetail studyDetail) { + return assignmentHistories.stream() + .filter(assignmentHistory -> + assignmentHistory.getStudyDetail().getId().equals(studyDetail.getId())) + .findFirst() + .orElse(null); + } + + private boolean isAttended(List attendances, StudyDetail studyDetail) { + return attendances.stream() + .anyMatch(attendance -> attendance.getStudyDetail().getId().equals(studyDetail.getId())); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceCustomRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceCustomRepository.java new file mode 100644 index 000000000..175472c02 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceCustomRepository.java @@ -0,0 +1,9 @@ +package com.gdschongik.gdsc.domain.study.dao; + +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.study.domain.Attendance; +import java.util.List; + +public interface AttendanceCustomRepository { + List findByMemberAndStudyId(Member member, Long studyId); +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceCustomRepositoryImpl.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceCustomRepositoryImpl.java new file mode 100644 index 000000000..16ed1b15c --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceCustomRepositoryImpl.java @@ -0,0 +1,36 @@ +package com.gdschongik.gdsc.domain.study.dao; + +import static com.gdschongik.gdsc.domain.member.domain.QMember.member; +import static com.gdschongik.gdsc.domain.study.domain.QAttendance.attendance; +import static com.gdschongik.gdsc.domain.study.domain.QStudyDetail.studyDetail; + +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.study.domain.Attendance; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class AttendanceCustomRepositoryImpl implements AttendanceCustomRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public List findByMemberAndStudyId(Member member, Long studyId) { + return queryFactory + .selectFrom(attendance) + .leftJoin(attendance.studyDetail, studyDetail) + .fetchJoin() + .where(eqMemberId(member.getId()), eqStudyId(studyId)) + .fetch(); + } + + private BooleanExpression eqMemberId(Long memberId) { + return memberId != null ? attendance.student.id.eq(memberId) : null; + } + + private BooleanExpression eqStudyId(Long studyId) { + return studyId != null ? attendance.studyDetail.study.id.eq(studyId) : null; + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java index 1b31a4429..4c8d83d20 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AttendanceRepository.java @@ -3,4 +3,4 @@ import com.gdschongik.gdsc.domain.study.domain.Attendance; import org.springframework.data.jpa.repository.JpaRepository; -public interface AttendanceRepository extends JpaRepository {} +public interface AttendanceRepository extends JpaRepository, AttendanceCustomRepository {} 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 350872753..5c4fc8c69 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 @@ -6,5 +6,5 @@ public interface StudyDetailRepository extends JpaRepository { - List findAllByStudyId(Long studyId); + List findAllByStudyIdOrderByWeekAsc(Long studyId); } 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 index bc34ae1fd..ca6c93fd0 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistory.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentHistory.java @@ -83,6 +83,10 @@ public boolean isSubmitted() { return submissionFailureType != NOT_SUBMITTED; } + public boolean isSuccess() { + return submissionStatus == SUCCESS; + } + // 데이터 변경 로직 public void success(String submissionLink, String commitHash, Integer contentLength, LocalDateTime committedAt) { 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 index 28fcf864a..2bc90a871 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionStatus.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/AssignmentSubmissionStatus.java @@ -6,7 +6,6 @@ @Getter @RequiredArgsConstructor public enum AssignmentSubmissionStatus { - // TODO: 클라이언트 응답에는 PENDING 상태 필요하므로, 추후 응답용 enum 클래스 생성 구현 FAILURE("제출 실패"), SUCCESS("제출 성공"); 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 c64cacfec..1563714df 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 @@ -87,6 +87,10 @@ public boolean isCancelled() { return status == CANCELLED; } + public boolean isNone() { + return status == NONE; + } + public boolean isDeadlineRemaining() { LocalDateTime now = LocalDateTime.now(); return now.isBefore(deadline); diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentSubmissionStatusResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentSubmissionStatusResponse.java new file mode 100644 index 000000000..50fb311df --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentSubmissionStatusResponse.java @@ -0,0 +1,26 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AssignmentSubmissionStatusResponse { + NOT_SUBMITTED("미제출"), + FAILURE("제출 실패"), + SUCCESS("제출 성공"); + + private final String value; + + public static AssignmentSubmissionStatusResponse from(AssignmentHistory assignmentHistory) { + if (assignmentHistory == null) { + return NOT_SUBMITTED; + } + if (assignmentHistory.isSuccess()) { + return SUCCESS; + } else { + return FAILURE; + } + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AttendanceStatusResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AttendanceStatusResponse.java new file mode 100644 index 000000000..3eb730bd9 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AttendanceStatusResponse.java @@ -0,0 +1,29 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import com.gdschongik.gdsc.domain.study.domain.StudyDetail; +import java.time.LocalDate; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +// 응답용 enum +@Getter +@RequiredArgsConstructor +public enum AttendanceStatusResponse { + ATTENDED("출석"), + NOT_ATTENDED("미출석"), + BEFORE_ATTENDANCE("출석전"); + + private final String value; + + public static AttendanceStatusResponse of(StudyDetail studyDetail, LocalDate now, boolean isAttended) { + if (studyDetail.getAttendanceDay().isAfter(now)) { + return BEFORE_ATTENDANCE; + } + + if (isAttended) { + return ATTENDED; + } else { + return NOT_ATTENDED; + } + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentSessionResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentSessionResponse.java new file mode 100644 index 000000000..47b1ee783 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentSessionResponse.java @@ -0,0 +1,42 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import static com.gdschongik.gdsc.domain.study.domain.SubmissionFailureType.NOT_SUBMITTED; + +import com.gdschongik.gdsc.domain.recruitment.domain.vo.Period; +import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; +import com.gdschongik.gdsc.domain.study.domain.Difficulty; +import com.gdschongik.gdsc.domain.study.domain.StudyDetail; +import com.gdschongik.gdsc.domain.study.domain.StudyStatus; +import com.gdschongik.gdsc.domain.study.domain.SubmissionFailureType; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +public record StudyStudentSessionResponse( + Long studyDetailId, + @Schema(description = "기간") Period period, + @Schema(description = "주차수") Long week, + @Schema(description = "제목") String title, + @Schema(description = "설명") String description, + @Schema(description = "세션 상태") StudyStatus sessionStatus, + @Schema(description = "난이도") Difficulty difficulty, + @Schema(description = "출석 상태") AttendanceStatusResponse attendanceStatus, + @Schema(description = "과제 개설 상태") StudyStatus assignmentStatus, + @Schema(description = "과제 제출 상태") AssignmentSubmissionStatusResponse assignmentSubmissionStatus, + @Schema(description = "과제 실패 타입") SubmissionFailureType submissionFailureType) { + + public static StudyStudentSessionResponse of( + StudyDetail studyDetail, AssignmentHistory assignmentHistory, boolean isAttended, LocalDateTime now) { + return new StudyStudentSessionResponse( + studyDetail.getId(), + studyDetail.getPeriod(), + studyDetail.getWeek(), + studyDetail.getSession().getTitle(), + studyDetail.getSession().getDescription(), + studyDetail.getSession().getStatus(), + studyDetail.getSession().getDifficulty(), + AttendanceStatusResponse.of(studyDetail, now.toLocalDate(), isAttended), + studyDetail.getAssignment().getStatus(), + AssignmentSubmissionStatusResponse.from(assignmentHistory), + assignmentHistory != null ? assignmentHistory.getSubmissionFailureType() : NOT_SUBMITTED); + } +} diff --git a/src/test/java/com/gdschongik/gdsc/domain/study/application/MentorStudyServiceTest.java b/src/test/java/com/gdschongik/gdsc/domain/study/application/MentorStudyServiceTest.java index 0f0d0da22..7b4707f6d 100644 --- a/src/test/java/com/gdschongik/gdsc/domain/study/application/MentorStudyServiceTest.java +++ b/src/test/java/com/gdschongik/gdsc/domain/study/application/MentorStudyServiceTest.java @@ -62,7 +62,7 @@ class 스터디_정보_작성시 { Study savedStudy = studyRepository.findById(study.getId()).get(); assertThat(savedStudy.getNotionLink()).isEqualTo(request.notionLink()); - List studyDetails = studyDetailRepository.findAllByStudyId(1L); + List studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(1L); for (int i = 0; i < studyDetails.size(); i++) { StudyDetail studyDetail = studyDetails.get(i); Long expectedId = studyDetail.getId(); From 986dda91bed101a85c10868a4ef1aa28fbe493ed 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: Fri, 23 Aug 2024 00:02:42 +0900 Subject: [PATCH 31/40] =?UTF-8?q?refactor:=20=EA=B3=BC=EC=A0=9C=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=82=9C=EC=9D=B4=EB=8F=84=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=82=AD=EC=A0=9C=20(#669)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor: 과제 필드 난이도 제거 --- .../gdsc/domain/study/domain/vo/Assignment.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 1563714df..8d6471273 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 @@ -4,7 +4,6 @@ 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; import com.gdschongik.gdsc.global.exception.CustomException; import jakarta.persistence.Column; @@ -33,20 +32,15 @@ public class Assignment { @Column(columnDefinition = "TEXT") private String descriptionLink; - @Enumerated(EnumType.STRING) - private Difficulty difficulty; - @Comment("과제 상태") @Enumerated(EnumType.STRING) private StudyStatus status; @Builder(access = AccessLevel.PRIVATE) - private Assignment( - String title, LocalDateTime deadline, String descriptionLink, Difficulty difficulty, StudyStatus status) { + private Assignment(String title, LocalDateTime deadline, String descriptionLink, StudyStatus status) { this.title = title; this.deadline = deadline; this.descriptionLink = descriptionLink; - this.difficulty = difficulty; this.status = status; } From 593413f5b6a8f65af7938995275f7ee6dd6ab044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=8A=AC=EA=B8=B0?= <84620016+seulgi99@users.noreply.github.com> Date: Fri, 23 Aug 2024 13:18:34 +0900 Subject: [PATCH 32/40] =?UTF-8?q?fix:=20dev=20=EC=8A=A4=EC=9B=A8=EA=B1=B0?= =?UTF-8?q?=20cors=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0=20(#676)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gdschongik/gdsc/global/config/WebSecurityConfig.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 e6ec97dfa..7ba713933 100644 --- a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java +++ b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java @@ -17,6 +17,8 @@ import com.gdschongik.gdsc.global.security.JwtFilter; import com.gdschongik.gdsc.global.util.CookieUtil; import com.gdschongik.gdsc.global.util.EnvironmentUtil; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -167,7 +169,11 @@ public CorsConfigurationSource corsConfigurationSource() { } if (environmentUtil.isDevProfile()) { - configuration.setAllowedOriginPatterns(DEV_AND_LOCAL_CLIENT_URLS); + List urls = new ArrayList<>(); + urls.addAll(DEV_AND_LOCAL_CLIENT_URLS); + urls.add(DEV_SERVER_URL); + urls.add(LOCAL_SERVER_URL); + configuration.setAllowedOriginPatterns(urls); } if (environmentUtil.isLocalProfile()) { From 05017d64706ffa307bd46a0cbf9e09a90680db17 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Fri, 23 Aug 2024 22:41:14 +0900 Subject: [PATCH 33/40] =?UTF-8?q?fix:=20=EC=A0=9C=EC=B6=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=A0=A5=EC=9D=B4=20=EC=97=86=EB=8A=94=20=EA=B3=BC=EC=A0=9C?= =?UTF-8?q?=EB=8F=84=20=ED=8F=AC=ED=95=A8=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#674)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 제출 이력이 없는 과제도 포함하도록 수정 * remove: 사용하지 않는 레포지토리 제거 * refactor: 기존 메서드 활용하도록 수정 * fix: 과제 없는 경우 발생하는 에러 수정 --- .../StudentStudyDetailService.java | 14 ++++++----- .../domain/study/domain/vo/Assignment.java | 4 ++++ .../response/AssignmentSubmittableDto.java | 24 +++++++++++++++---- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java index 7a57cab1b..70c73a9e2 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java @@ -5,7 +5,6 @@ import com.gdschongik.gdsc.domain.study.dao.AttendanceRepository; import com.gdschongik.gdsc.domain.study.dao.StudyDetailRepository; import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository; -import com.gdschongik.gdsc.domain.study.dao.StudyRepository; import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; import com.gdschongik.gdsc.domain.study.domain.Attendance; import com.gdschongik.gdsc.domain.study.domain.StudyDetail; @@ -27,7 +26,6 @@ public class StudentStudyDetailService { private final MemberUtil memberUtil; - private final StudyRepository studyRepository; private final StudyHistoryRepository studyHistoryRepository; private final AssignmentHistoryRepository assignmentHistoryRepository; private final StudyDetailRepository studyDetailRepository; @@ -39,13 +37,17 @@ public AssignmentDashboardResponse getSubmittableAssignments(Long studyId) { StudyHistory studyHistory = studyHistoryRepository .findByStudentAndStudyId(currentMember, studyId) .orElseThrow(() -> new CustomException(ErrorCode.STUDY_HISTORY_NOT_FOUND)); - List assignmentHistories = assignmentHistoryRepository.findAssignmentHistoriesByStudentAndStudy(currentMember, studyId); + List studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId).stream() + .filter(studyDetail -> + studyDetail.getAssignment().isOpen() && studyDetail.isAssignmentDeadlineRemaining()) + .toList(); + boolean isAnySubmitted = assignmentHistories.stream().anyMatch(AssignmentHistory::isSubmitted); - List submittableAssignments = assignmentHistories.stream() - .filter(assignmentHistory -> assignmentHistory.getStudyDetail().isAssignmentDeadlineRemaining()) - .map(AssignmentSubmittableDto::from) + List submittableAssignments = studyDetails.stream() + .map(studyDetail -> AssignmentSubmittableDto.of( + studyDetail, getSubmittedAssignment(assignmentHistories, studyDetail))) .toList(); return AssignmentDashboardResponse.of(studyHistory.getRepositoryLink(), isAnySubmitted, submittableAssignments); 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 8d6471273..1d5a5c278 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 @@ -77,6 +77,10 @@ public void validateSubmittable(LocalDateTime now) { // 데이터 전달 로직 + public boolean isOpen() { + return status == OPEN; + } + public boolean isCancelled() { return status == CANCELLED; } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentSubmittableDto.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentSubmittableDto.java index ce379f0de..5935778c8 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentSubmittableDto.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentSubmittableDto.java @@ -20,14 +20,17 @@ public record AssignmentSubmittableDto( @Nullable @Schema(description = "마감 기한") LocalDateTime deadline, @Nullable @Schema(description = "과제 제출 링크") String submissionLink, @Nullable @Schema(description = "과제 제출 실패 사유") SubmissionFailureType submissionFailureType) { - public static AssignmentSubmittableDto from(AssignmentHistory assignmentHistory) { - StudyDetail studyDetail = assignmentHistory.getStudyDetail(); + public static AssignmentSubmittableDto of(StudyDetail studyDetail, AssignmentHistory assignmentHistory) { Assignment assignment = studyDetail.getAssignment(); if (assignment.isCancelled()) { return cancelledAssignment(studyDetail, assignment); } + if (assignmentHistory == null) { + return notSubmittedAssignment(studyDetail, assignment); + } + return new AssignmentSubmittableDto( studyDetail.getId(), assignment.getStatus(), @@ -37,13 +40,24 @@ public static AssignmentSubmittableDto from(AssignmentHistory assignmentHistory) assignment.getDescriptionLink(), assignment.getDeadline(), assignmentHistory.getSubmissionLink(), - assignmentHistory.getSubmissionFailureType() == null - ? null - : assignmentHistory.getSubmissionFailureType()); + assignmentHistory.getSubmissionFailureType()); } private static AssignmentSubmittableDto cancelledAssignment(StudyDetail studyDetail, Assignment assignment) { return new AssignmentSubmittableDto( studyDetail.getId(), assignment.getStatus(), studyDetail.getWeek(), null, null, null, null, null, null); } + + private static AssignmentSubmittableDto notSubmittedAssignment(StudyDetail studyDetail, Assignment assignment) { + return new AssignmentSubmittableDto( + studyDetail.getId(), + assignment.getStatus(), + studyDetail.getWeek(), + assignment.getTitle(), + AssignmentSubmissionStatus.FAILURE, + assignment.getDescriptionLink(), + assignment.getDeadline(), + null, + SubmissionFailureType.NOT_SUBMITTED); + } } From 34ba4fa369142c73d0575f3819e56561f74af6ef Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Fri, 23 Aug 2024 23:27:29 +0900 Subject: [PATCH 34/40] =?UTF-8?q?refactor:=20=EA=B3=BC=EC=A0=9C=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=20=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#678)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/gdschongik/gdsc/domain/study/domain/StudyDetail.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 020ee5dea..fd857f50c 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 @@ -49,7 +49,8 @@ public class StudyDetail extends BaseEntity { @Embedded @AttributeOverride(name = "title", column = @Column(name = "assignment_title")) - @AttributeOverride(name = "difficulty", column = @Column(name = "assignment_difficulty")) + @AttributeOverride(name = "deadline", column = @Column(name = "assignment_deadline")) + @AttributeOverride(name = "descriptionLink", column = @Column(name = "assignment_description_link")) @AttributeOverride(name = "status", column = @Column(name = "assignment_status")) private Assignment assignment; From 5095b3d5b11c0b70b59e8dd3c6ad2f7ae7e377e5 Mon Sep 17 00:00:00 2001 From: Cho Sangwook <82208159+Sangwook02@users.noreply.github.com> Date: Fri, 23 Aug 2024 23:47:27 +0900 Subject: [PATCH 35/40] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B3=84=EC=A0=95=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80=20(#672)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 마이페이지 계정정보 조회 api 추가 * remove: 포맷팅 로직 제거 * rename: 요청 클래스 이름 변경 * fix: 커스텀 예외로 변경 * refactor: DI 대신 직접 초기화 --- .../member/api/CommonMemberController.java | 8 +++ .../application/CommonMemberService.java | 12 ++++ .../response/MemberAccountInfoResponse.java | 9 +++ .../common/constant/GithubConstant.java | 1 + .../gdsc/global/exception/ErrorCode.java | 1 + .../gdsc/infra/github/GithubUserRequest.java | 58 +++++++++++++++++++ .../infra/github/client/GithubClient.java | 16 +++++ 7 files changed, 105 insertions(+) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberAccountInfoResponse.java create mode 100644 src/main/java/com/gdschongik/gdsc/infra/github/GithubUserRequest.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/api/CommonMemberController.java b/src/main/java/com/gdschongik/gdsc/domain/member/api/CommonMemberController.java index c35303519..62495f14a 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/api/CommonMemberController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/api/CommonMemberController.java @@ -1,6 +1,7 @@ package com.gdschongik.gdsc.domain.member.api; import com.gdschongik.gdsc.domain.member.application.CommonMemberService; +import com.gdschongik.gdsc.domain.member.dto.response.MemberAccountInfoResponse; import com.gdschongik.gdsc.domain.member.dto.response.MemberDepartmentResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -34,4 +35,11 @@ public ResponseEntity> searchDepartments( List response = commonMemberService.searchDepartments(departmentName); return ResponseEntity.ok().body(response); } + + @Operation(summary = "내 계정 정보 조회", description = "내 계정 정보를 조회합니다.") + @GetMapping("/me/account-info") + public ResponseEntity getAccountInfo() { + MemberAccountInfoResponse response = commonMemberService.getAccountInfo(); + return ResponseEntity.ok().body(response); + } } 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 3ae725a12..fb19ae07c 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 @@ -5,10 +5,13 @@ import com.gdschongik.gdsc.domain.member.dao.MemberRepository; import com.gdschongik.gdsc.domain.member.domain.Department; import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.member.dto.response.MemberAccountInfoResponse; import com.gdschongik.gdsc.domain.member.dto.response.MemberDepartmentResponse; import com.gdschongik.gdsc.domain.membership.dao.MembershipRepository; import com.gdschongik.gdsc.domain.membership.domain.Membership; import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.util.MemberUtil; +import com.gdschongik.gdsc.infra.github.client.GithubClient; import java.util.Arrays; import java.util.List; import lombok.RequiredArgsConstructor; @@ -23,6 +26,8 @@ public class CommonMemberService { private final MembershipRepository membershipRepository; private final MemberRepository memberRepository; + private final MemberUtil memberUtil; + private final GithubClient githubClient; @Transactional(readOnly = true) public List getDepartments() { @@ -81,4 +86,11 @@ public void demoteMemberToAssociateByMembership(Long membershipId) { log.info("[CommonMemberService] 준회원 강등 완료: memberId={}", member.getId()); } + + @Transactional(readOnly = true) + public MemberAccountInfoResponse getAccountInfo() { + Member currentMember = memberUtil.getCurrentMember(); + String githubHandle = githubClient.getGithubHandle(currentMember.getOauthId()); + return MemberAccountInfoResponse.of(currentMember, githubHandle); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberAccountInfoResponse.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberAccountInfoResponse.java new file mode 100644 index 000000000..285c44634 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberAccountInfoResponse.java @@ -0,0 +1,9 @@ +package com.gdschongik.gdsc.domain.member.dto.response; + +import com.gdschongik.gdsc.domain.member.domain.Member; + +public record MemberAccountInfoResponse(String name, String githubHandle) { + public static MemberAccountInfoResponse of(Member member, String githubHandle) { + return new MemberAccountInfoResponse(member.getName(), githubHandle); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/GithubConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/GithubConstant.java index d524cb9f4..2ffd6b0c7 100644 --- a/src/main/java/com/gdschongik/gdsc/global/common/constant/GithubConstant.java +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/GithubConstant.java @@ -4,6 +4,7 @@ public class GithubConstant { public static final String GITHUB_DOMAIN = "github.com/"; public static final String GITHUB_ASSIGNMENT_PATH = "week%d/WIL.md"; + public static final String GITHUB_USER_API_URL = "https://api.github.com/user/%s"; private GithubConstant() {} } 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 7982912ab..2220ddc33 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -167,6 +167,7 @@ public enum ErrorCode { GITHUB_CONTENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 파일입니다."), GITHUB_FILE_READ_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "깃허브 파일 읽기에 실패했습니다."), GITHUB_COMMIT_DATE_FETCH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "깃허브 커밋 날짜 조회에 실패했습니다."), + GITHUB_USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 깃허브 유저입니다."), ; private final HttpStatus status; private final String message; diff --git a/src/main/java/com/gdschongik/gdsc/infra/github/GithubUserRequest.java b/src/main/java/com/gdschongik/gdsc/infra/github/GithubUserRequest.java new file mode 100644 index 000000000..c42c7266c --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/infra/github/GithubUserRequest.java @@ -0,0 +1,58 @@ +package com.gdschongik.gdsc.infra.github; + +import static com.gdschongik.gdsc.global.common.constant.GithubConstant.*; + +import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.exception.ErrorCode; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.kohsuke.github.connector.GitHubConnectorRequest; + +@RequiredArgsConstructor +public class GithubUserRequest implements GitHubConnectorRequest { + + private final String oauthId; + + @Override + public String method() { + return "GET"; + } + + @Override + public Map> allHeaders() { + return Map.of(); + } + + @Override + public String header(String s) { + return ""; + } + + @Override + public String contentType() { + return ""; + } + + @Override + public InputStream body() { + return null; + } + + @Override + public URL url() { + try { + return new URL(GITHUB_USER_API_URL.formatted(oauthId)); + } catch (MalformedURLException e) { + throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } + + @Override + public boolean hasBody() { + return false; + } +} diff --git a/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java b/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java index 869a2a6ec..b7eb73fb5 100644 --- a/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java +++ b/src/main/java/com/gdschongik/gdsc/infra/github/client/GithubClient.java @@ -3,19 +3,24 @@ import static com.gdschongik.gdsc.global.common.constant.GithubConstant.*; import static com.gdschongik.gdsc.global.exception.ErrorCode.*; +import com.fasterxml.jackson.databind.ObjectMapper; import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmission; import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionFetchExecutor; import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionFetcher; import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.infra.github.GithubUserRequest; import java.io.IOException; import java.io.InputStream; import java.time.LocalDateTime; import java.time.ZoneId; +import java.util.Map; import lombok.RequiredArgsConstructor; import org.kohsuke.github.GHCommit; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; +import org.kohsuke.github.connector.GitHubConnector; +import org.kohsuke.github.connector.GitHubConnectorResponse; import org.springframework.stereotype.Component; @Component @@ -23,6 +28,7 @@ public class GithubClient { private final GitHub github; + private final GitHubConnector gitHubConnector = GitHubConnector.DEFAULT; public GHRepository getRepository(String ownerRepo) { try { @@ -32,6 +38,16 @@ public GHRepository getRepository(String ownerRepo) { } } + public String getGithubHandle(String oauthId) { + try (GitHubConnectorResponse response = gitHubConnector.send(new GithubUserRequest(oauthId)); + InputStream inputStream = response.bodyStream(); ) { + // api가 login이라는 이름으로 사용자의 github handle을 반환합니다. + return (String) new ObjectMapper().readValue(inputStream, Map.class).get("login"); + } catch (IOException e) { + throw new CustomException(GITHUB_USER_NOT_FOUND); + } + } + /** * 직접 요청을 수행하는 대신, fetcher를 통해 요청을 수행합니다. * 요청 수행 시 발생하는 예외의 경우 과제 채점에 사용되므로, 실제 요청은 채점 로직 내부에서 수행되어야 합니다. From bcd0e50314cef9891a42ec0cd83bb79ddfdbbd1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=8A=AC=EA=B8=B0?= <84620016+seulgi99@users.noreply.github.com> Date: Sat, 24 Aug 2024 16:07:35 +0900 Subject: [PATCH 36/40] =?UTF-8?q?feat:=20=EB=82=B4=20=ED=95=A0=EC=9D=BC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#671)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 내 할일 목록 API 인터페이스 구현 * feat: 내할일 목록 조회 API 인터페이스 구현 * feat: 내할일 목록 조회 API 추가 * refactor: 변경사항 반영 --- .../api/StudentStudyDetailController.java | 8 +++ .../StudentStudyDetailService.java | 28 ++++++++++ .../gdsc/domain/study/domain/StudyDetail.java | 24 +++++++-- .../gdsc/domain/study/domain/vo/Session.java | 5 ++ .../study/dto/response/StudyTodoResponse.java | 53 +++++++++++++++++++ 5 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyTodoResponse.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java index f122077f8..c153140b1 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java @@ -3,6 +3,7 @@ import com.gdschongik.gdsc.domain.study.application.StudentStudyDetailService; import com.gdschongik.gdsc.domain.study.dto.response.AssignmentDashboardResponse; import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentSessionResponse; +import com.gdschongik.gdsc.domain.study.dto.response.StudyTodoResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; @@ -29,6 +30,13 @@ public ResponseEntity getSubmittableAssignments( return ResponseEntity.ok(response); } + @Operation(summary = "내 할일 리스트 조회", description = "해당 스터디의 내 할일 리스트를 조회합니다") + @GetMapping("/todo") + public ResponseEntity> getStudyTodoList(@RequestParam(name = "studyId") Long studyId) { + List response = studentStudyDetailService.getStudyTodoList(studyId); + return ResponseEntity.ok(response); + } + // TODO 스터디 세션 워딩을 커리큘럼으로 변경해야함 @Operation(summary = "스터디 커리큘럼 조회", description = "해당 스터디의 커리큘럼들을 조회합니다.") @GetMapping("/sessions") diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java index 70c73a9e2..652b397fc 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java @@ -12,10 +12,13 @@ import com.gdschongik.gdsc.domain.study.dto.response.AssignmentDashboardResponse; import com.gdschongik.gdsc.domain.study.dto.response.AssignmentSubmittableDto; import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentSessionResponse; +import com.gdschongik.gdsc.domain.study.dto.response.StudyTodoResponse; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.exception.ErrorCode; import com.gdschongik.gdsc.global.util.MemberUtil; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -54,6 +57,31 @@ studyDetail, getSubmittedAssignment(assignmentHistories, studyDetail))) } @Transactional(readOnly = true) + public List getStudyTodoList(Long studyId) { + Member member = memberUtil.getCurrentMember(); + final List studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId); + final List assignmentHistories = + assignmentHistoryRepository.findAssignmentHistoriesByStudentAndStudy(member, studyId); + final List attendances = attendanceRepository.findByMemberAndStudyId(member, studyId); + + LocalDate now = LocalDate.now(); + List response = new ArrayList<>(); + // 출석체크 정보 (개설 상태이고, 오늘이 출석체크날짜인 것) + studyDetails.stream() + .filter(studyDetail -> studyDetail.getSession().isOpen() + && studyDetail.getAttendanceDay().equals(now)) + .forEach(studyDetail -> response.add(StudyTodoResponse.createAttendanceType( + studyDetail, now, isAttended(attendances, studyDetail)))); + + // 과제 정보 (오늘이 과제 제출 기간에 포함된 과제 정보) + studyDetails.stream() + .filter(studyDetail -> studyDetail.getAssignment().isOpen() + && studyDetail.getAssignment().isDeadlineRemaining()) + .forEach(studyDetail -> response.add(StudyTodoResponse.createAssignmentType( + studyDetail, getSubmittedAssignment(assignmentHistories, studyDetail)))); + return response; + } + public List getStudySessions(Long studyId) { Member member = memberUtil.getCurrentMember(); final List studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId); 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 fd857f50c..be1a4a174 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 @@ -8,6 +8,7 @@ import com.gdschongik.gdsc.domain.study.domain.vo.Session; import com.gdschongik.gdsc.global.exception.CustomException; import jakarta.persistence.*; +import java.time.DayOfWeek; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -97,10 +98,25 @@ public boolean isAssignmentDeadlineRemaining() { // 스터디 시작일자 + 현재 주차 * 7 + (스터디 요일 - 스터디 기간 시작 요일) public LocalDate getAttendanceDay() { - return study.getStartDate() - .plusDays(week * 7 - + study.getDayOfWeek().getValue() - - study.getStartDate().getDayOfWeek().getValue()); + // 스터디 시작일자 + LocalDate startDate = study.getStartDate(); + + // 스터디 요일 + DayOfWeek studyDayOfWeek = study.getDayOfWeek(); + + // 스터디 기간 시작 요일 + DayOfWeek startDayOfWeek = startDate.getDayOfWeek(); + + // 스터디 요일이 스터디 기간 시작 요일보다 앞서면, 다음 주로 넘어가게 처리 + Long daysDifference = Long.valueOf(studyDayOfWeek.getValue() - startDayOfWeek.getValue()); + if (daysDifference < 0) { + daysDifference += 7; + } + + // 현재 주차에 따른 일수 계산 + Long daysToAdd = (week - 1) * 7 + daysDifference; + + return startDate.plusDays(daysToAdd); } public void updateSession( diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/vo/Session.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/vo/Session.java index 1b26ff63b..07a2282c0 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/vo/Session.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/vo/Session.java @@ -55,4 +55,9 @@ public static Session generateSession( .status(status) .build(); } + + // 데이터 전달 로직 + public boolean isOpen() { + return status == StudyStatus.OPEN; + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyTodoResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyTodoResponse.java new file mode 100644 index 000000000..7399b6181 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyTodoResponse.java @@ -0,0 +1,53 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import static com.gdschongik.gdsc.domain.study.dto.response.StudyTodoResponse.StudyTodoType.ASSIGNMENT; +import static com.gdschongik.gdsc.domain.study.dto.response.StudyTodoResponse.StudyTodoType.ATTENDANCE; + +import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; +import com.gdschongik.gdsc.domain.study.domain.StudyDetail; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +public record StudyTodoResponse( + Long studyDetailId, + @Schema(description = "현 주차수") Long week, + @Schema(description = "할일 타입") StudyTodoType todoType, + @Schema(description = "마감 시각") LocalDateTime deadLine, + @Schema(description = "출석 상태 (출석타입일 때만 사용)") AttendanceStatusResponse attendanceStatus, + @Schema(description = "과제 제목 (과제타입일 때만 사용)") String assignmentTitle, + @Schema(description = "과제 제출 상태 (과제타입일 때만 사용)") AssignmentSubmissionStatusResponse assignmentSubmissionStatus) { + + public static StudyTodoResponse createAttendanceType(StudyDetail studyDetail, LocalDate now, boolean isAttended) { + return new StudyTodoResponse( + studyDetail.getId(), + studyDetail.getWeek(), + ATTENDANCE, + studyDetail.getAttendanceDay().atTime(23, 59, 59), + AttendanceStatusResponse.of(studyDetail, now, isAttended), + null, + null); + } + + public static StudyTodoResponse createAssignmentType(StudyDetail studyDetail, AssignmentHistory assignmentHistory) { + return new StudyTodoResponse( + studyDetail.getId(), + studyDetail.getWeek(), + ASSIGNMENT, + studyDetail.getAssignment().getDeadline(), + null, + studyDetail.getAssignment().getTitle(), + AssignmentSubmissionStatusResponse.from(assignmentHistory)); + } + + @Getter + @RequiredArgsConstructor + public enum StudyTodoType { + ATTENDANCE("출석"), + ASSIGNMENT("과제"); + + private final String value; + } +} From 9baf58c854a3b5791611f571f9f5d9ebed14e9aa 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: Sat, 24 Aug 2024 18:16:06 +0900 Subject: [PATCH 37/40] =?UTF-8?q?feat:=20=EC=9D=B4=EB=B2=88=EC=A3=BC=20?= =?UTF-8?q?=EA=B3=BC=EC=A0=9C=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#647)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 이번주 과제 조회 API 구현 * feat: 불필요한 코드 제거 및 엔드포인트 수정 * feat: 코드 오타 수정 * fix: 레포지토리 메소드 위치 수정 * feat: 이번주까지 마감인 과제들 반환 로직 추가 * feat: 도메인 메소드에 추가 * feat: 도메인 메소드 주석 가독성 향상 * feat: 과제 제출 내역이 null일때 고려해서 response반환 * feat: 과제 제출 이력이 없을 때 StudyDetail에서 가져와서 반환 * feat: 과제 응답 클래스명 변경 * feat: 과제 응답 로직 개선 * feat: npe 예방 코드 * fix: 머지 컨플릭트 해결 * feat: merge develop --- .../api/StudentStudyDetailController.java | 9 ++++ .../StudentStudyDetailService.java | 26 +++++++-- .../StudentStudyHistoryService.java | 2 +- .../AssignmentHistoryCustomRepository.java | 2 +- ...AssignmentHistoryCustomRepositoryImpl.java | 2 +- .../study/dao/StudyDetailRepository.java | 2 + .../gdsc/domain/study/domain/StudyDetail.java | 4 ++ .../domain/study/domain/vo/Assignment.java | 15 ++++++ .../AssignmentHistoryStatusResponse.java | 54 +++++++++++++++++++ 9 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryStatusResponse.java diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java index c153140b1..ccc11013c 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/StudentStudyDetailController.java @@ -2,6 +2,7 @@ import com.gdschongik.gdsc.domain.study.application.StudentStudyDetailService; import com.gdschongik.gdsc.domain.study.dto.response.AssignmentDashboardResponse; +import com.gdschongik.gdsc.domain.study.dto.response.AssignmentHistoryStatusResponse; import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentSessionResponse; import com.gdschongik.gdsc.domain.study.dto.response.StudyTodoResponse; import io.swagger.v3.oas.annotations.Operation; @@ -45,4 +46,12 @@ public ResponseEntity> getStudySessions( List response = studentStudyDetailService.getStudySessions(studyId); return ResponseEntity.ok(response); } + + @Operation(summary = "이번주 제출해야 할 과제 조회", description = "마감 기한이 이번주까지인 과제를 조회합니다.") + @GetMapping("/assignments/upcoming") + public ResponseEntity> getUpcomingAssignments( + @RequestParam(name = "studyId") Long studyId) { + List response = studentStudyDetailService.getUpcomingAssignments(studyId); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java index 652b397fc..046a1204d 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyDetailService.java @@ -10,6 +10,7 @@ import com.gdschongik.gdsc.domain.study.domain.StudyDetail; import com.gdschongik.gdsc.domain.study.domain.StudyHistory; import com.gdschongik.gdsc.domain.study.dto.response.AssignmentDashboardResponse; +import com.gdschongik.gdsc.domain.study.dto.response.AssignmentHistoryStatusResponse; import com.gdschongik.gdsc.domain.study.dto.response.AssignmentSubmittableDto; import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentSessionResponse; import com.gdschongik.gdsc.domain.study.dto.response.StudyTodoResponse; @@ -41,7 +42,7 @@ public AssignmentDashboardResponse getSubmittableAssignments(Long studyId) { .findByStudentAndStudyId(currentMember, studyId) .orElseThrow(() -> new CustomException(ErrorCode.STUDY_HISTORY_NOT_FOUND)); List assignmentHistories = - assignmentHistoryRepository.findAssignmentHistoriesByStudentAndStudy(currentMember, studyId); + assignmentHistoryRepository.findAssignmentHistoriesByStudentAndStudyId(currentMember, studyId); List studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId).stream() .filter(studyDetail -> studyDetail.getAssignment().isOpen() && studyDetail.isAssignmentDeadlineRemaining()) @@ -61,7 +62,7 @@ public List getStudyTodoList(Long studyId) { Member member = memberUtil.getCurrentMember(); final List studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId); final List assignmentHistories = - assignmentHistoryRepository.findAssignmentHistoriesByStudentAndStudy(member, studyId); + assignmentHistoryRepository.findAssignmentHistoriesByStudentAndStudyId(member, studyId); final List attendances = attendanceRepository.findByMemberAndStudyId(member, studyId); LocalDate now = LocalDate.now(); @@ -86,7 +87,7 @@ public List getStudySessions(Long studyId) { Member member = memberUtil.getCurrentMember(); final List studyDetails = studyDetailRepository.findAllByStudyIdOrderByWeekAsc(studyId); final List assignmentHistories = - assignmentHistoryRepository.findAssignmentHistoriesByStudentAndStudy(member, studyId); + assignmentHistoryRepository.findAssignmentHistoriesByStudentAndStudyId(member, studyId); final List attendances = attendanceRepository.findByMemberAndStudyId(member, studyId); return studyDetails.stream() @@ -111,4 +112,23 @@ private boolean isAttended(List attendances, StudyDetail studyDetail return attendances.stream() .anyMatch(attendance -> attendance.getStudyDetail().getId().equals(studyDetail.getId())); } + + @Transactional(readOnly = true) + public List getUpcomingAssignments(Long studyId) { + Member currentMember = memberUtil.getCurrentMember(); + List studyDetails = studyDetailRepository.findAllByStudyId(studyId).stream() + .filter(studyDetail -> + studyDetail.getAssignment().isOpen() && studyDetail.isAssignmentDeadlineThisWeek()) + .toList(); + List assignmentHistories = + assignmentHistoryRepository.findAssignmentHistoriesByStudentAndStudyId(currentMember, studyId).stream() + .filter(assignmentHistory -> + assignmentHistory.getStudyDetail().isAssignmentDeadlineThisWeek()) + .toList(); + + return studyDetails.stream() + .map(studyDetail -> AssignmentHistoryStatusResponse.of( + studyDetail, getSubmittedAssignment(assignmentHistories, studyDetail))) + .toList(); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java index 1dea2ee3e..8a1eb7b42 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/StudentStudyHistoryService.java @@ -78,7 +78,7 @@ private String getOwnerRepo(String repositoryLink) { public List getAllAssignmentHistories(Long studyId) { Member currentMember = memberUtil.getCurrentMember(); - return assignmentHistoryRepository.findAssignmentHistoriesByStudentAndStudy(currentMember, studyId).stream() + return assignmentHistoryRepository.findAssignmentHistoriesByStudentAndStudyId(currentMember, studyId).stream() .map(AssignmentHistoryResponse::from) .toList(); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java index 5fa5fc2d9..108112e6f 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepository.java @@ -9,5 +9,5 @@ public interface AssignmentHistoryCustomRepository { boolean existsSubmittedAssignmentByMemberAndStudy(Member member, Study study); - List findAssignmentHistoriesByStudentAndStudy(Member member, Long studyId); + List findAssignmentHistoriesByStudentAndStudyId(Member member, Long studyId); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java index a37e68723..4c6df554c 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/AssignmentHistoryCustomRepositoryImpl.java @@ -41,7 +41,7 @@ private BooleanExpression isSubmitted() { } @Override - public List findAssignmentHistoriesByStudentAndStudy(Member currentMember, Long studyId) { + public List findAssignmentHistoriesByStudentAndStudyId(Member currentMember, Long studyId) { return queryFactory .selectFrom(assignmentHistory) .join(assignmentHistory.studyDetail, 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 5c4fc8c69..c3f317f54 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 @@ -7,4 +7,6 @@ public interface StudyDetailRepository extends JpaRepository { List findAllByStudyIdOrderByWeekAsc(Long studyId); + + List findAllByStudyId(Long studyId); } 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 be1a4a174..41bb4a0b5 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 @@ -96,6 +96,10 @@ public boolean isAssignmentDeadlineRemaining() { return assignment.isDeadlineRemaining(); } + public boolean isAssignmentDeadlineThisWeek() { + return assignment.isDeadLineThisWeek(); + } + // 스터디 시작일자 + 현재 주차 * 7 + (스터디 요일 - 스터디 기간 시작 요일) public LocalDate getAttendanceDay() { // 스터디 시작일자 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 1d5a5c278..38be69b1f 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 @@ -10,6 +10,8 @@ import jakarta.persistence.Embeddable; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import java.time.DayOfWeek; +import java.time.LocalDate; import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; @@ -93,4 +95,17 @@ public boolean isDeadlineRemaining() { LocalDateTime now = LocalDateTime.now(); return now.isBefore(deadline); } + + public boolean isDeadLineThisWeek() { + // 현재 날짜와 마감일의 날짜 부분을 비교할 것이므로 LocalDate로 변환 + LocalDate now = LocalDate.now(); + LocalDate startOfWeek = now.with(DayOfWeek.MONDAY); // 이번 주 월요일 + LocalDate endOfWeek = now.with(DayOfWeek.SUNDAY); // 이번 주 일요일 + + // 마감일의 날짜 부분을 가져옴 + LocalDate deadlineDate = deadline.toLocalDate(); + + // 마감일이 이번 주 내에 있는지 확인 + return !deadlineDate.isBefore(startOfWeek) && !deadlineDate.isAfter(endOfWeek); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryStatusResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryStatusResponse.java new file mode 100644 index 000000000..3209dc8cc --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryStatusResponse.java @@ -0,0 +1,54 @@ +package com.gdschongik.gdsc.domain.study.dto.response; + +import static com.gdschongik.gdsc.domain.study.domain.SubmissionFailureType.NOT_SUBMITTED; + +import com.gdschongik.gdsc.domain.study.domain.*; +import com.gdschongik.gdsc.domain.study.domain.vo.Assignment; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.annotation.Nullable; +import java.time.LocalDateTime; + +public record AssignmentHistoryStatusResponse( + Long studyDetailId, + @Schema(description = "과제 상태") StudyStatus assignmentStatus, + @Schema(description = "주차") Long week, + @Nullable @Schema(description = "과제 제목") String title, + // TODO 추후 처리 예정 + @Nullable @Schema(description = "과제 제출 상태") AssignmentSubmissionStatus assignmentSubmissionStatus, + @Nullable @Schema(description = "과제 명세 링크") String descriptionLink, + @Nullable @Schema(description = "마감 기한") LocalDateTime deadline, + @Nullable @Schema(description = "과제 제출 링크") String submissionLink, + @Nullable @Schema(description = "과제 제출 실패 사유. 제출 여부도 포함되어 있습니다. 미제출 상태라면 기본 과제 정보만 반환합니다.") + SubmissionFailureType submissionFailureType, + @Nullable @Schema(description = "최종 수정 일시") LocalDateTime committedAt) { + + // 과제 제출이 없는 경우, 과제 정보만 사용하여 AssignmentHistoryStatusResponse 생성 + public static AssignmentHistoryStatusResponse of(StudyDetail studyDetail, AssignmentHistory assignmentHistory) { + if (assignmentHistory == null) { + return new AssignmentHistoryStatusResponse( + studyDetail.getId(), + studyDetail.getAssignment().getStatus(), + studyDetail.getWeek(), + studyDetail.getAssignment().getTitle(), + null, + studyDetail.getAssignment().getDescriptionLink(), + studyDetail.getAssignment().getDeadline(), + null, + NOT_SUBMITTED, + null); + } + + Assignment assignment = studyDetail.getAssignment(); + return new AssignmentHistoryStatusResponse( + studyDetail.getId(), + assignment.getStatus(), + studyDetail.getWeek(), + assignment.getTitle(), + assignmentHistory.getSubmissionStatus(), + assignment.getDescriptionLink(), + assignment.getDeadline(), + assignmentHistory.getSubmissionLink(), + assignmentHistory.getSubmissionFailureType(), + assignmentHistory.getCommittedAt()); + } +} From 94c694cc69d4407b4c0f4d0a36ce9b335a63e883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=8A=AC=EA=B8=B0?= <84620016+seulgi99@users.noreply.github.com> Date: Sat, 24 Aug 2024 20:47:07 +0900 Subject: [PATCH 38/40] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EA=B8=B0=EC=B4=88=20=EB=82=9C=EC=9D=B4=EB=8F=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=84=B8=EC=85=98=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=97=90=20=EA=B3=BC=EC=A0=9C=EC=A0=9C=EC=B6=9C=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/gdschongik/gdsc/domain/study/domain/Difficulty.java | 3 ++- .../study/dto/response/StudyStudentSessionResponse.java | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/domain/Difficulty.java b/src/main/java/com/gdschongik/gdsc/domain/study/domain/Difficulty.java index bea99e59b..98f526bc2 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/domain/Difficulty.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/domain/Difficulty.java @@ -8,7 +8,8 @@ public enum Difficulty { HIGH("상"), MEDIUM("중"), - LOW("하"); + LOW("하"), + BASIC("기초"); private final String value; } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentSessionResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentSessionResponse.java index 47b1ee783..b903bd230 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentSessionResponse.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyStudentSessionResponse.java @@ -22,7 +22,8 @@ public record StudyStudentSessionResponse( @Schema(description = "출석 상태") AttendanceStatusResponse attendanceStatus, @Schema(description = "과제 개설 상태") StudyStatus assignmentStatus, @Schema(description = "과제 제출 상태") AssignmentSubmissionStatusResponse assignmentSubmissionStatus, - @Schema(description = "과제 실패 타입") SubmissionFailureType submissionFailureType) { + @Schema(description = "과제 실패 타입") SubmissionFailureType submissionFailureType, + @Schema(description = "과제 제출 링크") String submissionLink) { public static StudyStudentSessionResponse of( StudyDetail studyDetail, AssignmentHistory assignmentHistory, boolean isAttended, LocalDateTime now) { @@ -37,6 +38,7 @@ public static StudyStudentSessionResponse of( AttendanceStatusResponse.of(studyDetail, now.toLocalDate(), isAttended), studyDetail.getAssignment().getStatus(), AssignmentSubmissionStatusResponse.from(assignmentHistory), - assignmentHistory != null ? assignmentHistory.getSubmissionFailureType() : NOT_SUBMITTED); + assignmentHistory != null ? assignmentHistory.getSubmissionFailureType() : NOT_SUBMITTED, + assignmentHistory != null ? assignmentHistory.getSubmissionLink() : null); } } From fb8b08608afd55cb76722fc2c90e05b867960935 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: Sat, 24 Aug 2024 21:00:32 +0900 Subject: [PATCH 39/40] =?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=20API=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EB=B0=94=EB=94=94=EC=97=90=20=EA=B3=BC=EC=A0=9C=20=ED=9C=B4?= =?UTF-8?q?=EA=B0=95=20=EC=83=81=ED=83=9C=20=EC=B6=94=EA=B0=80=20=20(#683)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: 과제 휴강 상태 응답바디에 추가 --- .../domain/study/dto/response/AssignmentHistoryResponse.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryResponse.java index 2565f1286..2c7bbf997 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryResponse.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/AssignmentHistoryResponse.java @@ -2,12 +2,14 @@ import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory; import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus; +import com.gdschongik.gdsc.domain.study.domain.StudyStatus; import com.gdschongik.gdsc.domain.study.domain.SubmissionFailureType; import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; public record AssignmentHistoryResponse( Long assignmentHistoryId, + @Schema(description = "과제 휴강 여부") StudyStatus status, @Schema(description = "과제 제목") String title, @Schema(description = "마감 기한") LocalDateTime deadline, @Schema(description = "과제 명세 링크") String descriptionLink, @@ -18,6 +20,7 @@ public record AssignmentHistoryResponse( public static AssignmentHistoryResponse from(AssignmentHistory assignmentHistory) { return new AssignmentHistoryResponse( assignmentHistory.getId(), + assignmentHistory.getStudyDetail().getAssignment().getStatus(), assignmentHistory.getStudyDetail().getAssignment().getTitle(), assignmentHistory.getStudyDetail().getAssignment().getDeadline(), assignmentHistory.getStudyDetail().getAssignment().getDescriptionLink(), From 2ee4399aec8a9d78bd17d70d1d3b0f31b91f1cae Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Sun, 25 Aug 2024 05:18:31 +0900 Subject: [PATCH 40/40] =?UTF-8?q?fix:=20OAuth=20=EC=BD=9C=EB=B0=B1=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=EB=A1=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20=EC=9C=84=EC=B9=98?= =?UTF-8?q?=EB=A5=BC=20=EC=A7=80=EC=A0=95=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#685)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 인가 요청 시 추가 파라미터 설정하는 커스텀 리졸버 구현 * feat: 시큐리티 설정에 커스텀 리졸버 등록 * feat: 리다이렉트 시 파라미터로부터 타깃 위치 로드하도록 설정 --- .../common/constant/SecurityConstant.java | 1 + .../gdsc/global/config/WebSecurityConfig.java | 17 +++++-- ...tomOAuth2AuthorizationRequestResolver.java | 49 +++++++++++++++++++ .../global/security/CustomSuccessHandler.java | 2 +- 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2AuthorizationRequestResolver.java 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 15e402d9d..3c01c44b9 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 @@ -8,6 +8,7 @@ public class SecurityConstant { 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"; + public static final String OAUTH_TARGET_URL_PARAM_NAME = "target"; private SecurityConstant() {} } 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 7ba713933..3dab1279d 100644 --- a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java +++ b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java @@ -11,6 +11,7 @@ import com.gdschongik.gdsc.domain.member.dao.MemberRepository; import com.gdschongik.gdsc.global.annotation.ConditionalOnProfile; import com.gdschongik.gdsc.global.property.BasicAuthProperty; +import com.gdschongik.gdsc.global.security.CustomOAuth2AuthorizationRequestResolver; import com.gdschongik.gdsc.global.security.CustomSuccessHandler; import com.gdschongik.gdsc.global.security.CustomUserService; import com.gdschongik.gdsc.global.security.JwtExceptionFilter; @@ -31,6 +32,7 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.logout.LogoutFilter; @@ -49,6 +51,7 @@ public class WebSecurityConfig { private final ObjectMapper objectMapper; private final EnvironmentUtil environmentUtil; private final BasicAuthProperty basicAuthProperty; + private final ClientRegistrationRepository clientRegistrationRepository; private void defaultFilterChain(HttpSecurity http) throws Exception { http.httpBasic(AbstractHttpConfigurer::disable) @@ -94,10 +97,11 @@ public SecurityFilterChain prometheusFilterChain(HttpSecurity http) throws Excep public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { defaultFilterChain(http); - http.oauth2Login( - oauth2 -> oauth2.userInfoEndpoint(userInfo -> userInfo.userService(customUserService(memberRepository))) - .successHandler(customSuccessHandler(jwtService, cookieUtil)) - .failureHandler((request, response, exception) -> response.setStatus(401))); + http.oauth2Login(oauth2 -> oauth2.authorizationEndpoint( + endpoint -> endpoint.authorizationRequestResolver(customOAuth2AuthorizationRequestResolver())) + .userInfoEndpoint(userInfo -> userInfo.userService(customUserService(memberRepository))) + .successHandler(customSuccessHandler(jwtService, cookieUtil)) + .failureHandler((request, response, exception) -> response.setStatus(401))); http.exceptionHandling(exception -> exception.authenticationEntryPoint((request, response, authException) -> response.setStatus(401))); @@ -140,6 +144,11 @@ public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + @Bean + public CustomOAuth2AuthorizationRequestResolver customOAuth2AuthorizationRequestResolver() { + return new CustomOAuth2AuthorizationRequestResolver(clientRegistrationRepository); + } + @Bean public CustomUserService customUserService(MemberRepository memberRepository) { return new CustomUserService(memberRepository); diff --git a/src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2AuthorizationRequestResolver.java b/src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2AuthorizationRequestResolver.java new file mode 100644 index 000000000..1d3ae33e3 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/security/CustomOAuth2AuthorizationRequestResolver.java @@ -0,0 +1,49 @@ +package com.gdschongik.gdsc.global.security; + +import static com.gdschongik.gdsc.global.common.constant.SecurityConstant.*; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; + +public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { + + private final DefaultOAuth2AuthorizationRequestResolver delegate; + + public CustomOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) { + this.delegate = + new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, "/oauth2/authorization"); + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { + OAuth2AuthorizationRequest authorizationRequest = delegate.resolve(request); + return authorizationRequest != null ? customizeAuthorizationRequest(request, authorizationRequest) : null; + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) { + OAuth2AuthorizationRequest authorizationRequest = delegate.resolve(request, clientRegistrationId); + return authorizationRequest != null ? customizeAuthorizationRequest(request, authorizationRequest) : null; + } + + private OAuth2AuthorizationRequest customizeAuthorizationRequest( + HttpServletRequest request, OAuth2AuthorizationRequest authorizationRequest) { + + String referer = request.getHeader("Referer"); + if (referer == null || referer.isEmpty()) { + return authorizationRequest; + } + + Map additionalParameters = new HashMap<>(); + additionalParameters.put(OAUTH_TARGET_URL_PARAM_NAME, referer); + + return OAuth2AuthorizationRequest.from(authorizationRequest) + .additionalParameters(additionalParameters) + .build(); + } +} 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 08c1d687d..41633875e 100644 --- a/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java +++ b/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java @@ -24,7 +24,7 @@ public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler public CustomSuccessHandler(JwtService jwtService, CookieUtil cookieUtil) { this.jwtService = jwtService; this.cookieUtil = cookieUtil; - setUseReferer(true); + setTargetUrlParameter(OAUTH_TARGET_URL_PARAM_NAME); } @Override