diff --git a/src/main/java/com/example/spot/common/config/RegionDataLoader.java b/src/main/java/com/example/spot/common/config/RegionDataLoader.java index 16abf971..6f7ca747 100644 --- a/src/main/java/com/example/spot/common/config/RegionDataLoader.java +++ b/src/main/java/com/example/spot/common/config/RegionDataLoader.java @@ -1,9 +1,9 @@ package com.example.spot.common.config; -import com.example.spot.study.domain.aggregate.Theme; +import com.example.spot.study.domain.association.Theme; import com.example.spot.study.domain.enums.ThemeType; import com.example.spot.study.domain.repository.RegionRepository; -import com.example.spot.study.domain.aggregate.Region; +import com.example.spot.study.domain.association.Region; import com.example.spot.study.domain.repository.ThemeRepository; import java.io.IOException; import java.io.InputStreamReader; diff --git a/src/main/java/com/example/spot/member/application/legacy/MemberServiceImpl.java b/src/main/java/com/example/spot/member/application/legacy/MemberServiceImpl.java index 62db6150..6e630924 100644 --- a/src/main/java/com/example/spot/member/application/legacy/MemberServiceImpl.java +++ b/src/main/java/com/example/spot/member/application/legacy/MemberServiceImpl.java @@ -1,64 +1,20 @@ package com.example.spot.member.application.legacy; -import com.example.spot.common.api.code.status.ErrorStatus; -import com.example.spot.common.api.exception.GeneralException; -import com.example.spot.common.api.exception.handler.MemberHandler; -import com.example.spot.member.domain.association.StudyJoinReason; -import com.example.spot.member.domain.enums.Gender; -import com.example.spot.member.domain.enums.LoginType; -import com.example.spot.member.domain.enums.Reason; -import com.example.spot.member.domain.enums.Status; -import com.example.spot.study.domain.enums.ThemeType; -import com.example.spot.member.domain.association.StudyJoinReasonRepository; -import com.example.spot.common.security.utils.JwtTokenProvider; import com.example.spot.member.domain.Member; import com.example.spot.member.domain.MemberRepository; -import com.example.spot.member.presentation.dto.MemberRequestDTO; -import com.example.spot.member.presentation.dto.MemberRequestDTO.MemberReasonDTO; import com.example.spot.auth.domain.CustomUserDetails; -import com.example.spot.auth.domain.RefreshToken; -import com.example.spot.auth.domain.RefreshTokenRepository; -import com.example.spot.auth.infrastructure.kakao.KakaoOAuthClient; -import com.example.spot.member.presentation.dto.MemberResponseDTO; -import com.example.spot.member.presentation.dto.MemberResponseDTO.MemberRegionDTO.RegionDTO; -import com.example.spot.member.presentation.dto.MemberResponseDTO.MemberSignInDTO; -import com.example.spot.member.presentation.dto.MemberResponseDTO.MemberStudyReasonDTO; -import com.example.spot.member.presentation.dto.MemberResponseDTO.MemberTestDTO; -import com.example.spot.member.presentation.dto.MemberResponseDTO.SocialLoginSignInDTO; -import com.example.spot.auth.presentation.dto.kakao.KaKaoOAuthToken.KaKaoOAuthTokenDTO; -import com.example.spot.auth.presentation.dto.kakao.KaKaoUser; -import com.example.spot.auth.presentation.dto.token.TokenResponseDTO.TokenDTO; -import com.fasterxml.jackson.core.JsonProcessingException; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; + import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; -import com.example.spot.study.domain.aggregate.Region; -import com.example.spot.study.domain.aggregate.Theme; -import com.example.spot.member.domain.association.MemberTheme; -import com.example.spot.member.domain.association.PreferredRegion; -import com.example.spot.member.domain.association.MemberThemeRepository; -import com.example.spot.member.domain.association.PreferredRegionRepository; -import com.example.spot.study.domain.repository.RegionRepository; -import com.example.spot.study.domain.repository.ThemeRepository; -import com.example.spot.member.presentation.dto.MemberRequestDTO.MemberInfoListDTO; -import com.example.spot.member.presentation.dto.MemberRequestDTO.MemberRegionDTO; -import com.example.spot.member.presentation.dto.MemberRequestDTO.MemberThemeDTO; -import com.example.spot.member.presentation.dto.MemberResponseDTO.MemberUpdateDTO; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.UUID; - // TODO 추후 삭제 예정 -> 구글 로그인 관련 로직이 남아있음 diff --git a/src/main/java/com/example/spot/member/application/refactor/impl/MemberPreferenceServiceImpl.java b/src/main/java/com/example/spot/member/application/refactor/impl/MemberPreferenceServiceImpl.java index a762a188..e3e84aaa 100644 --- a/src/main/java/com/example/spot/member/application/refactor/impl/MemberPreferenceServiceImpl.java +++ b/src/main/java/com/example/spot/member/application/refactor/impl/MemberPreferenceServiceImpl.java @@ -20,8 +20,8 @@ import com.example.spot.member.domain.enums.Reason; import com.example.spot.member.presentation.dto.MemberRequestDTO; import com.example.spot.member.presentation.dto.MemberResponseDTO; -import com.example.spot.study.domain.aggregate.Region; -import com.example.spot.study.domain.aggregate.Theme; +import com.example.spot.study.domain.association.Region; +import com.example.spot.study.domain.association.Theme; import com.example.spot.study.domain.enums.ThemeType; import com.example.spot.study.domain.repository.RegionRepository; import com.example.spot.study.domain.repository.ThemeRepository; diff --git a/src/main/java/com/example/spot/member/domain/Member.java b/src/main/java/com/example/spot/member/domain/Member.java index 60896dc4..da9c3cba 100644 --- a/src/main/java/com/example/spot/member/domain/Member.java +++ b/src/main/java/com/example/spot/member/domain/Member.java @@ -8,26 +8,26 @@ import com.example.spot.report.domain.MemberReport; import com.example.spot.report.domain.PostReport; import com.example.spot.schedule.domain.Schedule; -import com.example.spot.schedule.domain.aggregate.Quiz; +import com.example.spot.schedule.domain.association.Quiz; import com.example.spot.story.domain.Story; -import com.example.spot.story.domain.aggregate.LikedStoryComment; -import com.example.spot.story.domain.aggregate.LikedStory; +import com.example.spot.story.domain.association.LikedStoryComment; +import com.example.spot.story.domain.association.LikedStory; import com.example.spot.member.domain.association.StudyJoinReason; import com.example.spot.common.entity.BaseEntity; import com.example.spot.member.domain.enums.Carrier; import com.example.spot.member.domain.enums.Gender; import com.example.spot.member.domain.enums.LoginType; import com.example.spot.member.domain.enums.Status; -import com.example.spot.schedule.domain.aggregate.QuizSubmission; +import com.example.spot.schedule.domain.association.QuizSubmission; import com.example.spot.post.domain.association.MemberScrap; -import com.example.spot.study.domain.aggregate.StudyMember; +import com.example.spot.study.domain.association.StudyMember; import com.example.spot.member.domain.association.MemberTheme; import com.example.spot.todo.domain.ToDo; import com.example.spot.vote.domain.Vote; -import com.example.spot.vote.domain.aggregate.VoteParticipant; +import com.example.spot.vote.domain.association.VoteParticipant; import com.example.spot.member.domain.association.PreferredRegion; import com.example.spot.member.domain.association.PreferredStudy; -import com.example.spot.story.domain.aggregate.StoryComment; +import com.example.spot.story.domain.association.StoryComment; import com.example.spot.member.presentation.dto.MemberRequestDTO.MemberUpdateDTO; import jakarta.persistence.*; import java.util.ArrayList; diff --git a/src/main/java/com/example/spot/member/domain/association/MemberTheme.java b/src/main/java/com/example/spot/member/domain/association/MemberTheme.java index 20e439b6..b8696024 100644 --- a/src/main/java/com/example/spot/member/domain/association/MemberTheme.java +++ b/src/main/java/com/example/spot/member/domain/association/MemberTheme.java @@ -1,7 +1,7 @@ package com.example.spot.member.domain.association; import com.example.spot.member.domain.Member; -import com.example.spot.study.domain.aggregate.Theme; +import com.example.spot.study.domain.association.Theme; import com.example.spot.common.entity.BaseEntity; import jakarta.persistence.*; import lombok.AccessLevel; diff --git a/src/main/java/com/example/spot/member/domain/association/PreferredRegion.java b/src/main/java/com/example/spot/member/domain/association/PreferredRegion.java index ccc8eee5..625a0553 100644 --- a/src/main/java/com/example/spot/member/domain/association/PreferredRegion.java +++ b/src/main/java/com/example/spot/member/domain/association/PreferredRegion.java @@ -1,6 +1,6 @@ package com.example.spot.member.domain.association; -import com.example.spot.study.domain.aggregate.Region; +import com.example.spot.study.domain.association.Region; import com.example.spot.member.domain.Member; import com.example.spot.common.entity.BaseEntity; import jakarta.persistence.*; diff --git a/src/main/java/com/example/spot/notification/application/notification/NotificationCommandServiceImpl.java b/src/main/java/com/example/spot/notification/application/notification/NotificationCommandServiceImpl.java index 3fa26680..b87c4c5e 100644 --- a/src/main/java/com/example/spot/notification/application/notification/NotificationCommandServiceImpl.java +++ b/src/main/java/com/example/spot/notification/application/notification/NotificationCommandServiceImpl.java @@ -3,7 +3,7 @@ import com.example.spot.common.api.code.status.ErrorStatus; import com.example.spot.common.api.exception.GeneralException; import com.example.spot.notification.domain.Notification; -import com.example.spot.study.domain.aggregate.StudyMember; +import com.example.spot.study.domain.association.StudyMember; import com.example.spot.study.domain.enums.StudyApplicationStatus; import com.example.spot.notification.domain.enums.NotifyType; import com.example.spot.study.domain.repository.StudyMemberRepository; diff --git a/src/main/java/com/example/spot/post/application/PostCommandService.java b/src/main/java/com/example/spot/post/application/PostCommandService.java index 5ee24f7c..1290ad8a 100644 --- a/src/main/java/com/example/spot/post/application/PostCommandService.java +++ b/src/main/java/com/example/spot/post/application/PostCommandService.java @@ -53,7 +53,4 @@ public interface PostCommandService { //게시글 스크랩 모두 취소 ScrapsPostDeleteResponse cancelPostScraps(ScrapAllDeleteRequest request); - - //게시글 신고 - PostReportResponse reportPost(Long postId, Long memberId); } diff --git a/src/main/java/com/example/spot/post/application/PostCommandServiceImpl.java b/src/main/java/com/example/spot/post/application/PostCommandServiceImpl.java index db6c555c..6845e985 100644 --- a/src/main/java/com/example/spot/post/application/PostCommandServiceImpl.java +++ b/src/main/java/com/example/spot/post/application/PostCommandServiceImpl.java @@ -599,32 +599,4 @@ public ScrapsPostDeleteResponse cancelPostScraps(ScrapAllDeleteRequest request) .cancelScraps(deletePostResponses) .build(); } - - @Override - public PostReportResponse reportPost(Long postId, Long memberId) { - - // 동일한 게시글에 대한 중복 신고 방지 - if (postReportRepository.existsByPostIdAndMemberId(postId, memberId)) { - throw new PostHandler(ErrorStatus._POST_ALREADY_REPORTED); - } - - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - - Post post = postRepository.findById(postId) - .orElseThrow(() -> new PostHandler(ErrorStatus._POST_NOT_FOUND)); - - if (post.getMember().getId().equals(memberId)) { - throw new PostHandler(ErrorStatus._POST_REPORT_SELF); - } - - PostReport postReport = PostReport.builder() - .postStatus(PostStatus.신고접수) - .post(post) - .member(member).build(); - - postReportRepository.save(postReport); - - return PostReportResponse.toDTO(postId, memberId); - } } diff --git a/src/main/java/com/example/spot/post/presentation/controller/PostController.java b/src/main/java/com/example/spot/post/presentation/controller/PostController.java index 73290afe..7cdafead 100644 --- a/src/main/java/com/example/spot/post/presentation/controller/PostController.java +++ b/src/main/java/com/example/spot/post/presentation/controller/PostController.java @@ -343,15 +343,4 @@ public ApiResponse deleteAllPostScrap( ScrapsPostDeleteResponse response = postCommandService.cancelPostScraps(request); return ApiResponse.onSuccess(SuccessStatus._NO_CONTENT, response); } - - @Tag(name = "게시글 신고", description = "게시글 신고 관련 API") - @Operation(summary = "[게시판] 게시글 신고 API", description = "게시글 ID와 회원 ID를 받아 게시글을 신고합니다.") - @PostMapping("/{postId}/report") - public ApiResponse reportPost( - @PathVariable @ExistPost Long postId - ) { - PostReportResponse response = postCommandService.reportPost(postId, SecurityUtils.getCurrentUserId()); - return ApiResponse.onSuccess(SuccessStatus._OK, response); - } - } diff --git a/src/main/java/com/example/spot/report/application/ReportCommandService.java b/src/main/java/com/example/spot/report/application/ReportCommandService.java new file mode 100644 index 00000000..43c150c4 --- /dev/null +++ b/src/main/java/com/example/spot/report/application/ReportCommandService.java @@ -0,0 +1,19 @@ +package com.example.spot.report.application; + +import com.example.spot.member.presentation.dto.MemberResponseDTO; +import com.example.spot.report.presentation.dto.PostReportResponse; +import com.example.spot.story.web.dto.response.StoryResDTO; +import com.example.spot.study.presentation.dto.request.StudyMemberReportDTO; +import jakarta.validation.Valid; + +public interface ReportCommandService { + + // 게시글 신고 + PostReportResponse reportPost(Long postId, Long memberId); + + // 스터디 회원 신고 + MemberResponseDTO.ReportedMemberDTO reportStudyMember(Long studyId, Long memberId, @Valid StudyMemberReportDTO studyMemberReportDTO); + + // 스터디 게시글 신고 + StoryResDTO.PostPreviewDTO reportStudyPost(Long studyId, Long postId); +} diff --git a/src/main/java/com/example/spot/report/application/ReportCommandServiceImpl.java b/src/main/java/com/example/spot/report/application/ReportCommandServiceImpl.java new file mode 100644 index 00000000..bbae7fbc --- /dev/null +++ b/src/main/java/com/example/spot/report/application/ReportCommandServiceImpl.java @@ -0,0 +1,156 @@ +package com.example.spot.report.application; + +import com.example.spot.common.api.code.status.ErrorStatus; +import com.example.spot.common.api.exception.handler.MemberHandler; +import com.example.spot.common.api.exception.handler.PostHandler; +import com.example.spot.common.api.exception.handler.StudyHandler; +import com.example.spot.common.security.utils.SecurityUtils; +import com.example.spot.member.domain.Member; +import com.example.spot.member.domain.MemberRepository; +import com.example.spot.member.presentation.dto.MemberResponseDTO; +import com.example.spot.post.domain.Post; +import com.example.spot.post.domain.PostRepository; +import com.example.spot.post.domain.enums.PostStatus; +import com.example.spot.report.domain.*; +import com.example.spot.report.presentation.dto.PostReportResponse; +import com.example.spot.story.domain.Story; +import com.example.spot.story.domain.StoryRepository; +import com.example.spot.story.web.dto.response.StoryResDTO; +import com.example.spot.study.domain.Study; +import com.example.spot.study.domain.StudyRepository; +import com.example.spot.study.domain.enums.StudyApplicationStatus; +import com.example.spot.study.domain.repository.StudyMemberRepository; +import com.example.spot.study.presentation.dto.request.StudyMemberReportDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class ReportCommandServiceImpl implements ReportCommandService { + + private final MemberRepository memberRepository; + private final PostRepository postRepository; + private final StudyRepository studyRepository; + private final StudyMemberRepository studyMemberRepository; + private final StoryRepository storyRepository; + + private final MemberReportRepository memberReportRepository; + private final StoryReportRepository storyReportRepository; + private final PostReportRepository postReportRepository; + + @Override + public PostReportResponse reportPost(Long postId, Long memberId) { + + // 동일한 게시글에 대한 중복 신고 방지 + if (postReportRepository.existsByPostIdAndMemberId(postId, memberId)) { + throw new PostHandler(ErrorStatus._POST_ALREADY_REPORTED); + } + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + + Post post = postRepository.findById(postId) + .orElseThrow(() -> new PostHandler(ErrorStatus._POST_NOT_FOUND)); + + if (post.getMember().getId().equals(memberId)) { + throw new PostHandler(ErrorStatus._POST_REPORT_SELF); + } + + PostReport postReport = PostReport.builder() + .postStatus(PostStatus.신고접수) + .post(post) + .member(member).build(); + + postReportRepository.save(postReport); + + return PostReportResponse.toDTO(postId, memberId); + } + + /** + * 스터디원을 신고하고 신고 내역을 저장하는 메서드입니다. + * @param studyId 타겟 스터디의 아이디를 입력 받습니다. + * @param memberId 신고할 회원의 아이디를 입력 받습니다. + * @param studyMemberReportDTO 신고 사유를 입력 받습니다. + * @return 신고를 당한 회원의 아이디와 이름을 반환합니다. + */ + @Override + public MemberResponseDTO.ReportedMemberDTO reportStudyMember(Long studyId, Long memberId, StudyMemberReportDTO studyMemberReportDTO) { + + //=== Exception ===// + Long reporterId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(reporterId); + + Member reporter = memberRepository.findById(reporterId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(reporterId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 신고당한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 자기 자신을 신고할 수 없음 + if (reporterId.equals(memberId)) { + throw new StudyHandler(ErrorStatus._STUDY_MEMBER_REPORT_INVALID); + } + + + //=== Feature ===// + MemberReport memberReport = MemberReport.builder() + .content(studyMemberReportDTO.getContent()) + .member(member) + .build(); + + memberReport = memberReportRepository.save(memberReport); + member.addMemberReport(memberReport); + + return MemberResponseDTO.ReportedMemberDTO.toDTO(member); + } + + /** + * 스터디 게시글을 신고하고 신고 내역을 저장하는 메서드입니다. + * @param studyId 타겟 스터디의 아이디를 입력합니다. + * @param postId 신고할 게시글의 아이디를 입력합니다. + * @return 신고를 당한 스터디 게시글의 아이디와 제목을 반환합니다. + */ + @Override + public StoryResDTO.PostPreviewDTO reportStudyPost(Long studyId, Long postId) { + + //=== Exception ===// + Long reporterId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(reporterId); + + memberRepository.findById(reporterId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + Story story = storyRepository.findById(postId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(reporterId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 해당 스터디의 게시글인지 확인 + storyRepository.findByIdAndStudyId(postId, studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_POST_NOT_FOUND)); + + //=== Feature ===// + StoryReport storyReport = StoryReport.builder() + .story(story) + .build(); + + storyReport = storyReportRepository.save(storyReport); + story.addStudyPostReport(storyReport); + + return StoryResDTO.PostPreviewDTO.toDTO(story); + } +} diff --git a/src/main/java/com/example/spot/story/domain/aggregate/StoryReport.java b/src/main/java/com/example/spot/report/domain/StoryReport.java similarity index 94% rename from src/main/java/com/example/spot/story/domain/aggregate/StoryReport.java rename to src/main/java/com/example/spot/report/domain/StoryReport.java index ffdbc912..19951a27 100644 --- a/src/main/java/com/example/spot/story/domain/aggregate/StoryReport.java +++ b/src/main/java/com/example/spot/report/domain/StoryReport.java @@ -1,4 +1,4 @@ -package com.example.spot.story.domain.aggregate; +package com.example.spot.report.domain; import com.example.spot.story.domain.Story; import jakarta.persistence.*; diff --git a/src/main/java/com/example/spot/story/domain/repository/StoryReportRepository.java b/src/main/java/com/example/spot/report/domain/StoryReportRepository.java similarity index 69% rename from src/main/java/com/example/spot/story/domain/repository/StoryReportRepository.java rename to src/main/java/com/example/spot/report/domain/StoryReportRepository.java index 12c5dad0..16d70dd6 100644 --- a/src/main/java/com/example/spot/story/domain/repository/StoryReportRepository.java +++ b/src/main/java/com/example/spot/report/domain/StoryReportRepository.java @@ -1,6 +1,5 @@ -package com.example.spot.story.domain.repository; +package com.example.spot.report.domain; -import com.example.spot.story.domain.aggregate.StoryReport; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/spot/report/presentation/controller/ReportController.java b/src/main/java/com/example/spot/report/presentation/controller/ReportController.java new file mode 100644 index 00000000..d15eab33 --- /dev/null +++ b/src/main/java/com/example/spot/report/presentation/controller/ReportController.java @@ -0,0 +1,59 @@ +package com.example.spot.report.presentation.controller; + +import com.example.spot.common.api.ApiResponse; +import com.example.spot.common.api.code.status.SuccessStatus; +import com.example.spot.member.domain.validation.annotation.ExistMember; +import com.example.spot.member.presentation.dto.MemberResponseDTO; +import com.example.spot.report.application.ReportCommandService; +import com.example.spot.story.domain.validation.annotation.ExistStory; +import com.example.spot.story.web.dto.response.StoryResDTO; +import com.example.spot.study.domain.validation.annotation.ExistStudy; +import com.example.spot.study.presentation.dto.request.StudyMemberReportDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/spot") +@Validated +public class ReportController { + + private final ReportCommandService reportCommandService; + +/* ----------------------------- 스터디 회원 신고 관련 API ------------------------------------- */ + + @Tag(name = "스터디 신고") + @Operation(summary = "[스터디 신고] 스터디원 신고하기", description = """ + ## [스터디 신고] 로그인한 회원이 참여하는 스터디의 다른 회원을 신고합니다. + 신고당한 회원의 id와 이름이 반환됩니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "memberId", description = "신고할 스터디원의 id를 입력합니다.", required = true) + @PostMapping("/studies/{studyId}/members/{memberId}/reports") + public ApiResponse reportStudyMember( + @PathVariable @ExistStudy Long studyId, @PathVariable @ExistMember Long memberId, + @RequestBody @Valid StudyMemberReportDTO studyMemberReportDTO) { + MemberResponseDTO.ReportedMemberDTO reportedMemberDTO = reportCommandService.reportStudyMember(studyId, memberId, studyMemberReportDTO); + return ApiResponse.onSuccess(SuccessStatus._STUDY_MEMBER_REPORTED, reportedMemberDTO); + } + + @Tag(name = "스터디 신고") + @Operation(summary = "[스터디 신고] 스터디 게시글 신고하기", description = """ + ## [스터디 신고] 로그인한 회원이 참여하는 스터디의 게시글을 신고합니다. + 신고당한 스터디 게시글의 id와 제목이 반환됩니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "postId", description = "신고할 스터디 게시글의 id를 입력합니다.", required = true) + @PostMapping("/studies/{studyId}/posts/{postId}/reports") + public ApiResponse reportStudyPost( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistStory Long postId) { + StoryResDTO.PostPreviewDTO postPreviewDTO = reportCommandService.reportStudyPost(studyId, postId); + return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_REPORTED, postPreviewDTO); + } +} diff --git a/src/main/java/com/example/spot/schedule/application/ScheduleCommandService.java b/src/main/java/com/example/spot/schedule/application/ScheduleCommandService.java new file mode 100644 index 00000000..a23fa0b2 --- /dev/null +++ b/src/main/java/com/example/spot/schedule/application/ScheduleCommandService.java @@ -0,0 +1,26 @@ +package com.example.spot.schedule.application; + +import com.example.spot.schedule.presentation.dto.request.ScheduleRequestDTO; +import com.example.spot.schedule.presentation.dto.request.StudyQuizRequestDTO; +import com.example.spot.schedule.presentation.dto.response.ScheduleResponseDTO; +import com.example.spot.schedule.presentation.dto.response.StudyQuizResponseDTO; + +import java.time.LocalDate; + +public interface ScheduleCommandService { + + // 일정 생성 + ScheduleResponseDTO.ScheduleDTO addSchedule(Long studyId, ScheduleRequestDTO.ScheduleDTO scheduleRequestDTO); + + // 일정 수정 + ScheduleResponseDTO.ScheduleDTO modSchedule(Long studyId, Long scheduleId, ScheduleRequestDTO.ScheduleDTO scheduleModDTO); + + // 스터디 퀴즈 생성 + StudyQuizResponseDTO.QuizDTO createAttendanceQuiz(Long studyId, Long scheduleId, StudyQuizRequestDTO.QuizDTO quizRequestDTO); + + // 스터디 출석 + StudyQuizResponseDTO.AttendanceDTO attendantStudy(Long studyId, Long scheduleId, StudyQuizRequestDTO.AttendanceDTO attendanceRequestDTO); + + // 스터디 퀴즈 삭제 + StudyQuizResponseDTO.QuizDTO deleteAttendanceQuiz(Long studyId, Long scheduleId, LocalDate date); +} diff --git a/src/main/java/com/example/spot/schedule/application/ScheduleCommandServiceImpl.java b/src/main/java/com/example/spot/schedule/application/ScheduleCommandServiceImpl.java new file mode 100644 index 00000000..e0313710 --- /dev/null +++ b/src/main/java/com/example/spot/schedule/application/ScheduleCommandServiceImpl.java @@ -0,0 +1,369 @@ +package com.example.spot.schedule.application; + +import com.example.spot.common.api.code.status.ErrorStatus; +import com.example.spot.common.api.exception.handler.MemberHandler; +import com.example.spot.common.api.exception.handler.StudyHandler; +import com.example.spot.common.security.utils.SecurityUtils; +import com.example.spot.member.domain.Member; +import com.example.spot.member.domain.MemberRepository; +import com.example.spot.notification.domain.Notification; +import com.example.spot.notification.domain.NotificationRepository; +import com.example.spot.notification.domain.enums.NotifyType; +import com.example.spot.schedule.domain.Schedule; +import com.example.spot.schedule.domain.ScheduleRepository; +import com.example.spot.schedule.domain.association.Quiz; +import com.example.spot.schedule.domain.association.QuizSubmission; +import com.example.spot.schedule.domain.repository.QuizRepository; +import com.example.spot.schedule.domain.repository.QuizSubmissionRepository; +import com.example.spot.study.domain.Study; +import com.example.spot.study.domain.StudyRepository; +import com.example.spot.study.domain.association.StudyMember; +import com.example.spot.study.domain.enums.StudyApplicationStatus; +import com.example.spot.study.domain.repository.StudyMemberRepository; +import com.example.spot.schedule.presentation.dto.request.ScheduleRequestDTO; +import com.example.spot.schedule.presentation.dto.request.StudyQuizRequestDTO; +import com.example.spot.schedule.presentation.dto.response.ScheduleResponseDTO; +import com.example.spot.schedule.presentation.dto.response.StudyQuizResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +public class ScheduleCommandServiceImpl implements ScheduleCommandService { + + private final MemberRepository memberRepository; + private final StudyRepository studyRepository; + private final StudyMemberRepository studyMemberRepository; + private final NotificationRepository notificationRepository; + private final ScheduleRepository scheduleRepository; + private final QuizRepository quizRepository; + private final QuizSubmissionRepository quizSubmissionRepository; + + /* ----------------------------- 스터디 일정 관련 API ------------------------------------- */ + + /** + * 스터디 일정을 추가하는 메서드입니다. + * @param studyId 타겟 스터디의 아이디를 입력 받습니다. + * @param scheduleRequestDTO 생성할 일정의 제목, 위치, 시작 일시, 종료 일시, 종일 진행 여부, 반복 여부를 입력 받습니다. + * @return 스터디 아이디와 생성된 일정의 아이디, 제목을 반환합니다. + */ + @Override + public ScheduleResponseDTO.ScheduleDTO addSchedule(Long studyId, ScheduleRequestDTO.ScheduleDTO scheduleRequestDTO) { + + //=== Exception ===// + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + //=== Feature ===// + + // Period 기반 시작일 종료일 제한 + checkStartAndFinishDate(scheduleRequestDTO); + + Schedule schedule = Schedule.builder() + .study(study) + .member(member) + .title(scheduleRequestDTO.getTitle()) + .location(scheduleRequestDTO.getLocation()) + .startedAt(scheduleRequestDTO.getStartedAt()) + .finishedAt(scheduleRequestDTO.getFinishedAt()) + .isAllDay(scheduleRequestDTO.getIsAllDay()) + .schedulePeriod(scheduleRequestDTO.getSchedulePeriod()) + .build(); + + // 알림 생성 + + // 스터디에 참여중인 회원들에게 알림 전송 위해 회원 조회 + List members = studyMemberRepository.findAllByStudyIdAndStatus(studyId, StudyApplicationStatus.APPROVED).stream() + .map(StudyMember::getMember) + .toList(); + + if (members.isEmpty()) + throw new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND); + + members.forEach(studyMember -> { + Notification notification = Notification.builder() + .member(studyMember) + .study(study) + .notifierName(member.getName()) // 일정 생성자 이름 + .type(NotifyType.SCHEDULE_UPDATE) + .isChecked(Boolean.FALSE) + .build(); + notificationRepository.save(notification); + }); + + scheduleRepository.save(schedule); + study.addSchedule(schedule); + member.addSchedule(schedule); + + return ScheduleResponseDTO.ScheduleDTO.toDTO(schedule); + } + + /** + * 스터디 일정을 변경하는 메서드입니다. + * @param studyId 타겟 스터디의 아이디를 입력 받습니다. + * @param scheduleId 변경할 일정의 아이디를 입력 받습니다. + * @param scheduleModDTO 변경된 일정의 제목, 위치, 시작 일시, 종료 일시, 종일 진행 여부, 반복 여부를 입력 받습니다. + * @return 스터디 아이디와 변경된 일정의 아이디, 제목을 반환합니다. + */ + @Override + public ScheduleResponseDTO.ScheduleDTO modSchedule(Long studyId, Long scheduleId, ScheduleRequestDTO.ScheduleDTO scheduleModDTO) { + + //=== Exception ===// + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + Schedule schedule = scheduleRepository.findById(scheduleId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 로그인한 회원이 일정 생성자인지 확인 + scheduleRepository.findByIdAndMemberId(scheduleId, memberId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._SCHEDULE_MOD_INVALID)); + + // 해당 스터디의 일정인지 확인 + scheduleRepository.findByIdAndStudyId(scheduleId, studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND)); + + //=== Feature ===// + + // Period 기반 시작일 종료일 제한 + checkStartAndFinishDate(scheduleModDTO); + + schedule.modSchedule(scheduleModDTO); + schedule = scheduleRepository.save(schedule); + + study.updateSchedule(schedule); + member.updateSchedule(schedule); + + return ScheduleResponseDTO.ScheduleDTO.toDTO(schedule); + } + + private static void checkStartAndFinishDate(ScheduleRequestDTO.ScheduleDTO scheduleRequestDTO) { + LocalDate startDate = scheduleRequestDTO.getStartedAt().toLocalDate(); + LocalDate finishDate = scheduleRequestDTO.getFinishedAt().toLocalDate(); + System.out.println(startDate); + System.out.println(finishDate); + switch (scheduleRequestDTO.getSchedulePeriod()) { + case DAILY : + // 시작일과 종료일이 일치해야 함 + if (finishDate.equals(startDate.plusDays(1)) || + finishDate.isAfter(startDate.plusDays(1))) { + throw new StudyHandler(ErrorStatus._STUDY_SCHEDULE_WRONG_FORMAT); + } + case WEEKLY : + // 시작일과 종료일이 일주일 이상 차이나지 않아야 함 + if (finishDate.equals(startDate.plusWeeks(1)) || + finishDate.isAfter(startDate.plusWeeks(1))) { + throw new StudyHandler(ErrorStatus._STUDY_SCHEDULE_WRONG_FORMAT); + } + case BIWEEKLY : + // 시작일과 종료일이 2주 이상 차이나지 않아야 함 + if (finishDate.equals(startDate.plusWeeks(2)) || + finishDate.isAfter(startDate.plusWeeks(2))) { + throw new StudyHandler(ErrorStatus._STUDY_SCHEDULE_WRONG_FORMAT); + } + case MONTHLY : + // 시작일과 종료일이 한 달 이상 차이나지 않아야 함 + if (finishDate.equals(startDate.plusMonths(1)) || + finishDate.isAfter(startDate.plusMonths(1))) { + throw new StudyHandler(ErrorStatus._STUDY_SCHEDULE_WRONG_FORMAT); + } + } + } + + + /* ----------------------------- 스터디 출석 관련 API ------------------------------------- */ + + /** + * 출석 퀴즈를 생성하는 메서드입니다. + * @param studyId 타겟 스터디의 아이디를 입력 받습니다. + * @param scheduleId 타겟 일정의 아이디를 입력 받습니다. + * @param quizRequestDTO 출석 퀴즈에 담길 질문과 정답을 입력 받습니다. + * @return 생성된 퀴즈의 아이디와 질문이 반환됩니다. + */ + @Override + public StudyQuizResponseDTO.QuizDTO createAttendanceQuiz(Long studyId, Long scheduleId, StudyQuizRequestDTO.QuizDTO quizRequestDTO) { + + //=== Exception ===// + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Schedule schedule = scheduleRepository.findById(scheduleId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + + // 해당 스터디에서 생성된 일정인지 확인 + if (!schedule.getStudy().equals(study)) { + throw new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND); + } + + // 로그인한 회원이 스터디장인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndIsOwned(memberId, studyId, Boolean.TRUE) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_QUIZ_CREATION_INVALID)); + + // 요청한 날짜에 이미 출석 퀴즈가 생성되었는지 확인 + LocalDateTime startOfDay = quizRequestDTO.getCreatedAt().withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfDay = quizRequestDTO.getCreatedAt().withHour(23).withMinute(59).withSecond(59).withNano(999_999_000); + List todayQuizzes = quizRepository.findAllByScheduleIdAndCreatedAtBetween(scheduleId, startOfDay, endOfDay); + if (!todayQuizzes.isEmpty()) { + throw new StudyHandler(ErrorStatus._STUDY_QUIZ_ALREADY_EXIST); + } + + //=== Feature ===// + Quiz quiz = Quiz.builder() + .schedule(schedule) + .member(member) + .question(quizRequestDTO.getQuestion()) + .answer(quizRequestDTO.getAnswer()) + .createdAt(quizRequestDTO.getCreatedAt()) + .build(); + + quiz = quizRepository.save(quiz); + schedule.addQuiz(quiz); + member.addQuiz(quiz); + + return StudyQuizResponseDTO.QuizDTO.toDTO(quiz); + } + + /** + * 출석 체크에 사용되는 메서드입니다. + * 메서드 내에서 퀴즈의 제한 시간과 시도 횟수를 확인하며, 조건을 충족하는 경우 회원 출석 정보를 저장합니다. + * @param studyId 타겟 스터디의 아이디를 입력 받습니다. + * @param scheduleId 출석을 체크할 일정을 입력 받습니다. + * @param attendanceRequestDTO 퀴즈에 대한 회원의 답변을 입력 받습니다. + * @return 회원 아이디, 퀴즈 아이디, 출석 아이디, 정답 여부, 시도 횟수, 출석 정보 생성 시각을 반환합니다. + */ + @Override + public StudyQuizResponseDTO.AttendanceDTO attendantStudy(Long studyId, Long scheduleId, StudyQuizRequestDTO.AttendanceDTO attendanceRequestDTO) { + + //=== Exception ===// + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + + // 요청한 날짜에 생성된 출석 퀴즈 조회 + LocalDateTime startOfDay = attendanceRequestDTO.getDateTime().withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfDay = attendanceRequestDTO.getDateTime().withHour(23).withMinute(59).withSecond(59).withNano(999_999_000); + List quizzes = quizRepository.findAllByScheduleIdAndCreatedAtBetween(scheduleId, startOfDay, endOfDay); + if (quizzes.isEmpty()) { + throw new StudyHandler(ErrorStatus._STUDY_QUIZ_NOT_FOUND); + } + Quiz quiz = quizzes.get(0); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(member.getId(), study.getId(), StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 퀴즈 제한시간 확인 + if (attendanceRequestDTO.getDateTime().isAfter(quiz.getCreatedAt().plusMinutes(5))) { + throw new StudyHandler(ErrorStatus._STUDY_QUIZ_NOT_VALID); + } + + // 이미 출석이 완료되었거나 시도 횟수를 초과하였는지 확인 + List attendanceList = quizSubmissionRepository.findByQuizIdAndMemberId(quiz.getId(), member.getId()); + int try_num = 0; + for (QuizSubmission attendance : attendanceList) { + if (attendance.getIsCorrect()) + throw new StudyHandler(ErrorStatus._STUDY_ATTENDANCE_ALREADY_EXIST); + else + try_num++; + } + if (try_num >= 3) { + throw new StudyHandler(ErrorStatus._STUDY_ATTENDANCE_ATTEMPT_LIMIT_EXCEEDED); + } + + //=== Feature ===// + Boolean isCorrect; + if (attendanceRequestDTO.getAnswer().equals(quiz.getAnswer())) { + isCorrect = Boolean.TRUE; + } else { + isCorrect = Boolean.FALSE; + } + + QuizSubmission quizSubmission = new QuizSubmission(isCorrect); + member.addMemberAttendance(quizSubmission); + quiz.addMemberAttendance(quizSubmission); + quizSubmission = quizSubmissionRepository.save(quizSubmission); + + return StudyQuizResponseDTO.AttendanceDTO.toDTO(quizSubmission, try_num+1); + } + + /** + * 출석 퀴즈를 삭제하는 메서드입니다. + * + * @param studyId 타겟 스터디의 아이디를 입력 받습니다. + * @param scheduleId 스터디 일정의 아이디를 입력 받습니다. + * @param date 출석 퀴즈가 생성된 날짜를 입력 받습니다. + * @return 삭제된 퀴즈의 아이디와 질문을 반환합니다. + */ + @Override + public StudyQuizResponseDTO.QuizDTO deleteAttendanceQuiz(Long studyId, Long scheduleId, LocalDate date) { + + //=== Exception ===// + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + + // 요청한 날짜에 생성된 출석 퀴즈 조회 + LocalDateTime startOfDay = date.atStartOfDay(); + LocalDateTime endOfDay = date.atStartOfDay().plusDays(1); + List todayQuizzes = quizRepository.findAllByScheduleIdAndCreatedAtBetween(scheduleId, startOfDay, endOfDay); + if (todayQuizzes.isEmpty()) { + throw new StudyHandler(ErrorStatus._STUDY_QUIZ_NOT_FOUND); + } + Quiz quiz = todayQuizzes.get(0); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(member.getId(), study.getId(), StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 로그인한 회원이 스터디장인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndIsOwned(memberId, studyId, Boolean.TRUE) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_QUIZ_DELETION_INVALID)); + + //=== Feature ===// + quizSubmissionRepository.findByQuizId(quiz.getId()) + .forEach(memberAttendance -> { + quiz.deleteMemberAttendance(memberAttendance); + quizSubmissionRepository.delete(memberAttendance); + }); + quizRepository.delete(quiz); + + return StudyQuizResponseDTO.QuizDTO.toDTO(quiz); + } + + +} diff --git a/src/main/java/com/example/spot/schedule/application/ScheduleQueryService.java b/src/main/java/com/example/spot/schedule/application/ScheduleQueryService.java new file mode 100644 index 00000000..696c1276 --- /dev/null +++ b/src/main/java/com/example/spot/schedule/application/ScheduleQueryService.java @@ -0,0 +1,26 @@ +package com.example.spot.schedule.application; + +import com.example.spot.schedule.presentation.dto.response.ScheduleResponseDTO; +import com.example.spot.schedule.presentation.dto.response.StudyQuizResponseDTO; +import com.example.spot.schedule.presentation.dto.response.StudyScheduleResponseDTO; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDate; + +public interface ScheduleQueryService { + + // 특정 연월 일정 조회하기 + ScheduleResponseDTO.MonthlyScheduleListDTO getMonthlySchedules(Long studyId, int year, int month); + + // 일정 단건 조회하기 + ScheduleResponseDTO.MonthlyScheduleDTO getSchedule(Long studyId, Long scheduleId); + + // 다가오는 모임 일정 조회하기 + StudyScheduleResponseDTO findStudySchedule(Long studyId, Pageable pageable); + + // 금일 회원 출석 여부 조회하기 + StudyQuizResponseDTO.AttendanceListDTO getAllAttendances(Long studyId, Long scheduleId, LocalDate date); + + // 스터디 출석퀴즈 조회하기 + StudyQuizResponseDTO.QuizDTO getAttendanceQuiz(Long studyId, Long scheduleId, LocalDate date); +} diff --git a/src/main/java/com/example/spot/schedule/application/ScheduleQueryServiceImpl.java b/src/main/java/com/example/spot/schedule/application/ScheduleQueryServiceImpl.java new file mode 100644 index 00000000..069aecb0 --- /dev/null +++ b/src/main/java/com/example/spot/schedule/application/ScheduleQueryServiceImpl.java @@ -0,0 +1,314 @@ +package com.example.spot.schedule.application; + +import com.example.spot.common.api.code.status.ErrorStatus; +import com.example.spot.common.api.exception.GeneralException; +import com.example.spot.common.api.exception.handler.StudyHandler; +import com.example.spot.common.security.utils.SecurityUtils; +import com.example.spot.member.domain.Member; +import com.example.spot.member.domain.MemberRepository; +import com.example.spot.schedule.domain.Schedule; +import com.example.spot.schedule.domain.ScheduleRepository; +import com.example.spot.schedule.domain.association.Quiz; +import com.example.spot.schedule.domain.association.QuizSubmission; +import com.example.spot.schedule.domain.enums.SchedulePeriod; +import com.example.spot.schedule.domain.repository.QuizRepository; +import com.example.spot.schedule.domain.repository.QuizSubmissionRepository; +import com.example.spot.schedule.presentation.dto.response.StudyScheduleResponseDTO; +import com.example.spot.study.domain.Study; +import com.example.spot.study.domain.StudyRepository; +import com.example.spot.study.domain.enums.StudyApplicationStatus; +import com.example.spot.study.domain.repository.StudyMemberRepository; +import com.example.spot.schedule.presentation.dto.response.ScheduleResponseDTO; +import com.example.spot.schedule.presentation.dto.response.StudyQuizResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ScheduleQueryServiceImpl implements ScheduleQueryService { + + private final MemberRepository memberRepository; + private final StudyRepository studyRepository; + private final StudyMemberRepository studyMemberRepository; + private final ScheduleRepository scheduleRepository; + private final QuizRepository quizRepository; + private final QuizSubmissionRepository quizSubmissionRepository; + + /* ----------------------------- 스터디 일정 관련 API ------------------------------------- */ + + /** + * 특정 연/월의 일정을 불러오는 메서드입니다. + * @param studyId 일정을 불러올 스터디의 아이디를 입력 받습니다. + * @param year 일정을 불러올 기준 연도를 입력 받습니다. + * @param month 일정을 불러올 달을 입력 받습니다. + * @return 스터디 아이디와 해당 스터디의 월별 일정 목록을 반환합니다. + */ + @Override + public ScheduleResponseDTO.MonthlyScheduleListDTO getMonthlySchedules(Long studyId, int year, int month) { + + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + boolean isStudyMember; + if (studyMemberRepository.existsByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED)) { + isStudyMember = true; + } else { + isStudyMember = false; + } + + List monthlyScheduleDTOS = new ArrayList<>(); + + study.getSchedules().forEach(schedule -> { + if (schedule.getSchedulePeriod().equals(SchedulePeriod.NONE)) { + addSchedule(schedule, year, month, monthlyScheduleDTOS, isStudyMember); + } else { + addPeriodSchedules(schedule, year, month, monthlyScheduleDTOS, isStudyMember); + } + }); + + return ScheduleResponseDTO.MonthlyScheduleListDTO.toDTO(study, monthlyScheduleDTOS); + } + + /** + * 하나의 일정에 대한 상세 정보를 불러오는 메서드입니다. + * @param studyId 일정을 불러올 스터디의 아이디를 입력 받습니다. + * @param scheduleId 상세 정보를 물러올 일정의 아이디를 입력 받습니다. + * @return 일정 아이디, 제목, 위치, 시작 일시, 종료 일시, 매일 진행 여부, 주기를 반환합니다. + */ + @Override + public ScheduleResponseDTO.MonthlyScheduleDTO getSchedule(Long studyId, Long scheduleId) { + + // Exception + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + Schedule schedule = scheduleRepository.findById(scheduleId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + boolean isStudyMember; + if (studyMemberRepository.existsByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED)) { + isStudyMember = true; + } else { + isStudyMember = false; + } + + // 해당 스터디의 일정인지 확인 + scheduleRepository.findByIdAndStudyId(scheduleId, studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND)); + + return ScheduleResponseDTO.MonthlyScheduleDTO.toDTO(schedule, isStudyMember); + } + + /** + * 로그인한 회원이 참여하는 특정 스터디의 다가오는 모임 목록을 페이징 조회 합니다. + * @param studyId 스터디 ID + * @param pageable 페이징 정보 + * @return 다가오는 모임 목록을 반환합니다. + * @throws GeneralException 스터디 일정이 존재하지 않는 경우 + * @throws GeneralException 스터디 멤버가 아닌 경우 + */ + @Override + public StudyScheduleResponseDTO findStudySchedule(Long studyId, Pageable pageable) { + + // 로그인한 회원이 해당 스터디 회원인지 확인 + if (!isMember(SecurityUtils.getCurrentUserId(), studyId)) + throw new GeneralException(ErrorStatus._ONLY_STUDY_MEMBER_CAN_ACCESS_SCHEDULE); + + // 스터디 일정 조회 + List schedules = scheduleRepository.findAllByStudyId(studyId, pageable); + + // 스터디 일정이 존재하지 않는 경우 + if (schedules.isEmpty()) + throw new GeneralException(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND); + + // DTO로 변환하여 반환 + List scheduleDTOS = schedules.stream().map(schedule -> StudyScheduleResponseDTO.StudyScheduleDTO.builder() + .title(schedule.getTitle()) + .location(schedule.getLocation()) + .startedAt(schedule.getStartedAt()) + .finishedAt(schedule.getFinishedAt()) + .build()).toList(); + + // 페이징 처리 + return new StudyScheduleResponseDTO(new PageImpl<>(scheduleDTOS, pageable, schedules.size()), scheduleDTOS, schedules.size()); + } + + /** + * 회원이 스터디 구성원인지 확인합니다. + * @param memberId 확인 하려는 회원 ID + * @param studyId 확인 하려는 스터디 ID + * @return 스터디 참여 여부를 반환합니다. + */ + private boolean isMember(Long memberId, Long studyId) { + return studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED).isPresent(); + } + + /** + * 월별 일정 리스트에 주기가 정해져 있지 않은 일정을 추가하기 위한 메서드입니다. + * 일정의 시작일이 기준 연월과 일치하는 경우 월별 일정 리스트에 추가합니다. + * getMonthlySchedules API에서 호출되는 내부 메서드입니다. + * @param schedule 리스트에 추가할 일정 정보를 입력 받습니다. + * @param year 기준 연도를 입력 받습니다. + * @param month 기준 월을 입력 받습니다. + * @param monthlyScheduleDTOS 일정을 추가할 월별 일정 리스트를 입력 받습니다. + * @param isStudyMember 스터디 회원 여부를 입력 받습니다. + */ + private void addSchedule(Schedule schedule, int year, int month, List monthlyScheduleDTOS, boolean isStudyMember) { + if (schedule.getStartedAt().getYear() == year && schedule.getStartedAt().getMonthValue() == month) { + monthlyScheduleDTOS.add(ScheduleResponseDTO.MonthlyScheduleDTO.toDTO(schedule, isStudyMember)); + } + } + + /** + * 월별 일정 리스트에 반복적인 일정을 추가하기 위한 메서드입니다. + * 일정의 시작일이 기준 연월 내에 있는 경우에만 일정을 추가하며, 주기에 따라 하나의 일정이라도 여러 번 추가될 수 있습니다. + * 예를 들어 기준 연월이 2024년 8월이고, 2024년 8월 2일부터 시작되는 WEEKLY 일정이 있다고 가정 + * 1. 이 일정은 기준 연월 내에서 2024년 8월 2일, 8월 9일, 8월 16일, 8월 23일, 8월 30일에 시행 + * 2. 따라서 monthlyScheduleDTOS에 추가되는 일정은 총 5개 + * @param schedule 리스트에 추가할 일정 정보를 입력 받습니다. + * @param year 기준 연도를 입력 받습니다. + * @param month 기준 월을 입력 받습니다. + * @param monthlyScheduleDTOS 일정을 추가할 월별 일정 리스트를 입력 받습니다. + * @param isStudyMember 스터디 회원 여부를 입력 받습니다. + */ + private void addPeriodSchedules(Schedule schedule, int year, int month, List monthlyScheduleDTOS, boolean isStudyMember) { + + LocalDateTime startedAt = schedule.getStartedAt(); + LocalDateTime finishedAt = schedule.getFinishedAt(); + + YearMonth yearMonth = YearMonth.of(year, month); // 탐색 연월 + LocalDateTime endOfMonth = yearMonth.atEndOfMonth().atTime(23, 59, 59); // 탐색 연월의 마지막 날 + + // 일정 시작일이 탐색 연월 내에 있는 경우만 반복 + while (startedAt.isBefore(endOfMonth)) { + // 업데이트된 일정 시작일의 month가 탐색 month와 일치하면 추가 + if (startedAt.getMonthValue() == month) { + monthlyScheduleDTOS.add(ScheduleResponseDTO.MonthlyScheduleDTO.toDTOWithDate(schedule, startedAt, finishedAt, isStudyMember)); + } + + if (schedule.getSchedulePeriod().equals(SchedulePeriod.DAILY)) { + startedAt = startedAt.plusDays(1); + finishedAt = finishedAt.plusDays(1); + } else if (schedule.getSchedulePeriod().equals(SchedulePeriod.WEEKLY)) { + startedAt = startedAt.plusWeeks(1); + finishedAt = finishedAt.plusWeeks(1); + } else if (schedule.getSchedulePeriod().equals(SchedulePeriod.BIWEEKLY)) { + startedAt = startedAt.plusWeeks(2); + finishedAt = finishedAt.plusWeeks(2); + } else if (schedule.getSchedulePeriod().equals(SchedulePeriod.MONTHLY)) { + startedAt = startedAt.plusMonths(1); + finishedAt = finishedAt.plusMonths(1); + } + } + } + + /* ----------------------------- 스터디 출석 관련 API ------------------------------------- */ + + /** + * 특정 일자에 대한 모든 스터디 회원의 출석 정보를 불러옵니다. + * @param studyId 출석 정보를 불러올 스터디의 아이디를 입력 받습니다. + * @param scheduleId 스터디 일정의 아이디를 입력 받습니다. + * @param date 출석 정보를 불러올 날짜를 입력 받습니다. + * @return 모든 스터디 회원에 대한 정보와 출석 여부를 담은 리스트를 반환합니다. + */ + @Override + public StudyQuizResponseDTO.AttendanceListDTO getAllAttendances(Long studyId, Long scheduleId, LocalDate date) { + + //=== Exception ===// + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); + studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + + // 요청한 날짜에 생성된 출석 퀴즈 조회 + LocalDateTime startOfDay = date.atStartOfDay(); + LocalDateTime endOfDay = date.atStartOfDay().plusDays(1); + List todayQuizzes = quizRepository.findAllByScheduleIdAndCreatedAtBetween(scheduleId, startOfDay, endOfDay); + if (todayQuizzes.isEmpty()) { + throw new StudyHandler(ErrorStatus._STUDY_QUIZ_NOT_FOUND); + } + Quiz quiz = todayQuizzes.get(0); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + //=== Feature ===// + List studyMembers = studyMemberRepository.findAllByStudyIdAndStatus(studyId, StudyApplicationStatus.APPROVED).stream() + .map(memberStudy -> { + List attendanceList = quizSubmissionRepository.findByQuizIdAndMemberId(quiz.getId(), memberStudy.getMember().getId()); + for (QuizSubmission attendance : attendanceList) { + // MemberAttendance에 퀴즈에 대한 정답이 저장되어 있으면 금일 출석 성공 + if (attendance.getIsCorrect()) + return StudyQuizResponseDTO.StudyMemberDTO.toDTO(memberStudy, Boolean.TRUE); + } + // 퀴즈를 풀지 않았거나 MemberAttendance에 오답만 저장되어 있으면 금일 출석 실패 + return StudyQuizResponseDTO.StudyMemberDTO.toDTO(memberStudy, Boolean.FALSE); + }) + .toList(); + + return StudyQuizResponseDTO.AttendanceListDTO.toDTO(quiz, studyMembers); + + } + + @Override + public StudyQuizResponseDTO.QuizDTO getAttendanceQuiz(Long studyId, Long scheduleId, LocalDate date) { + + // Authorization + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + Schedule schedule = scheduleRepository.findById(scheduleId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND)); + + // 해당 스터디에서 생성된 일정인지 확인 + if (!schedule.getStudy().equals(study)) { + throw new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND); + } + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 해당 날짜에 생성된 스터디 퀴즈 조회 + LocalDateTime startOfDay = date.atStartOfDay(); + LocalDateTime endOfDay = date.atStartOfDay().plusDays(1); + List todayQuizzes = quizRepository.findAllByScheduleIdAndCreatedAtBetween(scheduleId, startOfDay, endOfDay); + if (todayQuizzes.isEmpty()) { + throw new StudyHandler(ErrorStatus._STUDY_QUIZ_NOT_FOUND); + } + Quiz quiz = todayQuizzes.get(0); + + return StudyQuizResponseDTO.QuizDTO.toDTO(quiz); + } + + +} diff --git a/src/main/java/com/example/spot/schedule/domain/Schedule.java b/src/main/java/com/example/spot/schedule/domain/Schedule.java index cb2fc039..ff8e0914 100644 --- a/src/main/java/com/example/spot/schedule/domain/Schedule.java +++ b/src/main/java/com/example/spot/schedule/domain/Schedule.java @@ -1,10 +1,10 @@ package com.example.spot.schedule.domain; import com.example.spot.member.domain.Member; import com.example.spot.common.entity.BaseEntity; -import com.example.spot.schedule.domain.aggregate.Quiz; +import com.example.spot.schedule.domain.association.Quiz; import com.example.spot.study.domain.Study; import com.example.spot.schedule.domain.enums.SchedulePeriod; -import com.example.spot.study.presentation.dto.request.ScheduleRequestDTO; +import com.example.spot.schedule.presentation.dto.request.ScheduleRequestDTO; import jakarta.persistence.*; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/spot/schedule/domain/aggregate/Quiz.java b/src/main/java/com/example/spot/schedule/domain/association/Quiz.java similarity index 97% rename from src/main/java/com/example/spot/schedule/domain/aggregate/Quiz.java rename to src/main/java/com/example/spot/schedule/domain/association/Quiz.java index 11906e88..d909089d 100644 --- a/src/main/java/com/example/spot/schedule/domain/aggregate/Quiz.java +++ b/src/main/java/com/example/spot/schedule/domain/association/Quiz.java @@ -1,4 +1,4 @@ -package com.example.spot.schedule.domain.aggregate; +package com.example.spot.schedule.domain.association; import com.example.spot.member.domain.Member; diff --git a/src/main/java/com/example/spot/schedule/domain/aggregate/QuizSubmission.java b/src/main/java/com/example/spot/schedule/domain/association/QuizSubmission.java similarity index 94% rename from src/main/java/com/example/spot/schedule/domain/aggregate/QuizSubmission.java rename to src/main/java/com/example/spot/schedule/domain/association/QuizSubmission.java index 86c3b5ec..d495c014 100644 --- a/src/main/java/com/example/spot/schedule/domain/aggregate/QuizSubmission.java +++ b/src/main/java/com/example/spot/schedule/domain/association/QuizSubmission.java @@ -1,4 +1,4 @@ -package com.example.spot.schedule.domain.aggregate; +package com.example.spot.schedule.domain.association; import com.example.spot.member.domain.Member; import com.example.spot.common.entity.BaseEntity; diff --git a/src/main/java/com/example/spot/schedule/domain/repository/QuizRepository.java b/src/main/java/com/example/spot/schedule/domain/repository/QuizRepository.java index d1af47de..fcb3070a 100644 --- a/src/main/java/com/example/spot/schedule/domain/repository/QuizRepository.java +++ b/src/main/java/com/example/spot/schedule/domain/repository/QuizRepository.java @@ -1,6 +1,6 @@ package com.example.spot.schedule.domain.repository; -import com.example.spot.schedule.domain.aggregate.Quiz; +import com.example.spot.schedule.domain.association.Quiz; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/spot/schedule/domain/repository/QuizSubmissionRepository.java b/src/main/java/com/example/spot/schedule/domain/repository/QuizSubmissionRepository.java index 775e8cad..a5860e2b 100644 --- a/src/main/java/com/example/spot/schedule/domain/repository/QuizSubmissionRepository.java +++ b/src/main/java/com/example/spot/schedule/domain/repository/QuizSubmissionRepository.java @@ -1,6 +1,6 @@ package com.example.spot.schedule.domain.repository; -import com.example.spot.schedule.domain.aggregate.QuizSubmission; +import com.example.spot.schedule.domain.association.QuizSubmission; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/spot/schedule/presentation/controller/ScheduleController.java b/src/main/java/com/example/spot/schedule/presentation/controller/ScheduleController.java new file mode 100644 index 00000000..17dd1e67 --- /dev/null +++ b/src/main/java/com/example/spot/schedule/presentation/controller/ScheduleController.java @@ -0,0 +1,209 @@ +package com.example.spot.schedule.presentation.controller; + +import com.example.spot.common.api.ApiResponse; +import com.example.spot.common.api.code.status.SuccessStatus; +import com.example.spot.common.presentation.validator.IntSize; +import com.example.spot.schedule.application.ScheduleCommandService; +import com.example.spot.schedule.application.ScheduleQueryService; +import com.example.spot.schedule.domain.validation.annotation.ExistSchedule; +import com.example.spot.schedule.presentation.dto.request.ScheduleRequestDTO; +import com.example.spot.schedule.presentation.dto.request.StudyQuizRequestDTO; +import com.example.spot.schedule.presentation.dto.response.ScheduleResponseDTO; +import com.example.spot.schedule.presentation.dto.response.StudyQuizResponseDTO; +import com.example.spot.schedule.presentation.dto.response.StudyScheduleResponseDTO; +import com.example.spot.study.domain.validation.annotation.ExistStudy; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/spot") +@Validated +public class ScheduleController { + + private final ScheduleQueryService scheduleQueryService; + private final ScheduleCommandService scheduleCommandService; + + /* ----------------------------- 스터디 일정 관련 API ------------------------------------- */ + + @Tag(name = "스터디 일정") + @Operation(summary = "[스터디 일정] 월별 일정 불러오기", description = """ + ## [스터디 일정] 내 스터디 > 스터디 > 캘린더 클릭, 로그인한 회원이 참여하는 특정 스터디의 일정을 월 단위로 불러옵니다. + 처음 캘린더를 클릭하면 오늘 날짜가 포함된 연/월에 해당하는 일정 목록이 schedule에서 반환됩니다. + 캘린더를 넘기면 해당 연/월에 해당하는 일정 목록이 schedule에서 반환됩니다. + """) + @Parameter(name = "studyId", description = "일정을 불러올 스터디의 id를 입력합니다.", required = true) + @GetMapping("/studies/{studyId}/schedules") + public ApiResponse getMonthlySchedules( + @PathVariable @ExistStudy Long studyId, + @RequestParam @IntSize(min = 1) Integer year, + @RequestParam @IntSize(min = 1, max = 12) Integer month) { + ScheduleResponseDTO.MonthlyScheduleListDTO monthlyScheduleDTO = scheduleQueryService.getMonthlySchedules(studyId, year, month); + return ApiResponse.onSuccess(SuccessStatus._STUDY_SCHEDULE_FOUND, monthlyScheduleDTO); + } + + @Tag(name = "스터디 일정") + @Operation(summary = "[스터디 일정] 상세 일정 불러오기", description = """ + ## [스터디 일정] 내 스터디 > 스터디 > 캘린더 > 일정 클릭, 로그인한 회원이 참여하는 특정 스터디의 상세 일정을 불러옵니다. + 스터디의 일정 정보를 상세하게 불러옵니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "scheduleId", description = "불러올 스터디 일정의 id를 입력합니다.", required = true) + @GetMapping("/studies/{studyId}/schedules/{scheduleId}") + public ApiResponse getSchedule( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistSchedule Long scheduleId) { + ScheduleResponseDTO.MonthlyScheduleDTO scheduleDTO = scheduleQueryService.getSchedule(studyId, scheduleId); + return ApiResponse.onSuccess(SuccessStatus._STUDY_SCHEDULE_FOUND, scheduleDTO); + } + + @Tag(name = "스터디 일정") + @Operation(summary = "[스터디 일정] 일정 추가하기", description = """ + ## [스터디 일정] 내 스터디 > 스터디 > 캘린더 > 추가 버튼 클릭, 로그인한 회원이 운영하는 특정 스터디에 일정을 추가합니다. + 로그인한 회원이 owner인 경우 schedule에 새로운 일정을 등록합니다. + + period에는 [NONE, DAILY, WEEKLY, BIWEEKLY, MONTHLY] 중 하나를 입력해야 합니다. + """) + @Parameter(name = "studyId", description = "일정을 추가할 스터디의 id를 입력합니다.", required = true) + @PostMapping("/studies/{studyId}/schedules") + public ApiResponse addSchedule( + @PathVariable @ExistStudy Long studyId, + @RequestBody @Valid ScheduleRequestDTO.ScheduleDTO scheduleRequestDTO) { + ScheduleResponseDTO.ScheduleDTO scheduleResponseDTO = scheduleCommandService.addSchedule(studyId, scheduleRequestDTO); + return ApiResponse.onSuccess(SuccessStatus._STUDY_SCHEDULE_CREATED, scheduleResponseDTO); + } + + @Tag(name = "스터디 일정") + @Operation(summary = "[스터디 일정] 일정 변경하기", description = """ + ## [스터디 일정] 내 스터디 > 스터디 > 캘린더 > 일정 클릭, 로그인한 회원이 특정 스터디에 등록한 일정을 수정합니다. + 로그인한 회원이 owner인 경우 schedule에 등록한 일정을 수정할 수 있습니다. + + period에는 [NONE, DAILY, WEEKLY, BIWEEKLY, MONTHLY] 중 하나를 입력해야 합니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "scheduleId", description = "변경할 일정의 id를 입력합니다.", required = true) + @PatchMapping("/studies/{studyId}/schedules/{scheduleId}") + public ApiResponse modSchedule( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistSchedule Long scheduleId, + @RequestBody @Valid ScheduleRequestDTO.ScheduleDTO scheduleModDTO) { + ScheduleResponseDTO.ScheduleDTO scheduleResponseDTO = scheduleCommandService.modSchedule(studyId, scheduleId, scheduleModDTO); + return ApiResponse.onSuccess(SuccessStatus._STUDY_SCHEDULE_UPDATED, scheduleResponseDTO); + } + + @Tag(name = "스터디 일정") + @Operation(summary = "[스터디 일정] 다가오는 모임 목록 조회하기", description = """ + ## [스터디 일정] 내 스터디 > 스터디 클릭, 로그인한 회원이 참여하는 특정 스터디의 다가오는 모임 목록을 페이징 조회 합니다. + 현재 시점 이후에 진행되는 모임 일정의 목록을 schedule에서 반환합니다. + """) + @GetMapping("/studies/{studyId}/upcoming-schedules") + public ApiResponse getUpcomingSchedules( + @PathVariable @ExistStudy Long studyId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "1") int size){ + StudyScheduleResponseDTO studyScheduleResponseDTO = scheduleQueryService.findStudySchedule(studyId, PageRequest.of(page, size)); + return ApiResponse.onSuccess(SuccessStatus._STUDY_SCHEDULE_FOUND, studyScheduleResponseDTO); + } + + + /* ----------------------------- 스터디 출석체크 관련 API ------------------------------------- */ + + @Tag(name = "스터디 출석체크") + @Operation(summary = "[스터디 출석체크] 출석 퀴즈 생성하기", description = """ + ## [스터디 출석체크] 내 스터디 > 스터디 > 캘린더 > 출석체크 > 퀴즈 만들기 클릭, 로그인한 회원이 운영하는 스터디에 퀴즈를 생성합니다. + * 로그인한 회원이 스터디장인 경우 quiz에 새로운 퀴즈를 생성합니다. + * createdAt에는 출석 퀴즈를 생성할 날짜를 입력합니다. + """) + @Parameter(name = "studyId", description = "출석 퀴즈를 생성할 스터디의 id를 입력합니다.", required = true) + @Parameter(name = "scheduleId", description = "출석 퀴즈를 생성할 일정의 id를 입력합니다.", required = true) + @PostMapping("/studies/{studyId}/schedules/{scheduleId}/quiz") + public ApiResponse createAttendanceQuiz( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistSchedule Long scheduleId, + @RequestBody @Valid StudyQuizRequestDTO.QuizDTO quizRequestDTO) { + StudyQuizResponseDTO.QuizDTO quizResponseDTO = scheduleCommandService.createAttendanceQuiz(studyId, scheduleId, quizRequestDTO); + return ApiResponse.onSuccess(SuccessStatus._STUDY_QUIZ_CREATED, quizResponseDTO); + } + + @Tag(name = "스터디 출석체크") + @Operation(summary = "[스터디 출석체크] 출석 퀴즈 불러오기", description = """ + ## [스터디 출석체크] 내 스터디 > 스터디 > 캘린더 > 출석체크, 로그인한 회원이 참여하는 스터디의 퀴즈를 불러옵니다. + * 날짜에 해당하는 퀴즈의 아이디와 질문이 반환됩니다. + * date에는 출석 퀴즈를 불러올 날짜를 입력합니다. + """) + @Parameter(name = "studyId", description = "출석 퀴즈를 불러올 스터디의 id를 입력합니다.", required = true) + @Parameter(name = "scheduleId", description = "출석 퀴즈를 불러올 일정의 id를 입력합니다.", required = true) + @GetMapping("/studies/{studyId}/schedules/{scheduleId}/quiz") + public ApiResponse getAttendanceQuiz( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistSchedule Long scheduleId, + @RequestParam LocalDate date) { + StudyQuizResponseDTO.QuizDTO quizDTO = scheduleQueryService.getAttendanceQuiz(studyId, scheduleId, date); + return ApiResponse.onSuccess(SuccessStatus._STUDY_QUIZ_FOUND, quizDTO); + } + + + @Tag(name = "스터디 출석체크") + @Operation(summary = "[스터디 출석체크] 출석 체크하기", description = """ + ## [스터디 출석체크] 내 스터디 > 스터디 > 캘린더 > 이미지 클릭, 로그인한 회원이 참여하는 스터디에서 오늘의 퀴즈를 풀어 출석을 체크합니다. + * 특정 시점의 quiz에 대해 member_attendance 튜플을 추가합니다. + * dateTime에는 출석을 체크할 날짜와 시간을 입력합니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "scheduleId", description = "일정의 id를 입력합니다.", required = true) + @PostMapping("/studies/{studyId}/schedules/{scheduleId}/attendance") + public ApiResponse attendantStudy( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistSchedule Long scheduleId, + @RequestBody @Valid StudyQuizRequestDTO.AttendanceDTO attendanceRequestDTO) { + StudyQuizResponseDTO.AttendanceDTO attendanceResponseDTO = scheduleCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO); + if (attendanceResponseDTO.getIsCorrect()) { + return ApiResponse.onSuccess(SuccessStatus._STUDY_ATTENDANCE_CREATED_CORRECT_ANSWER, attendanceResponseDTO); + } else { + return ApiResponse.onSuccess(SuccessStatus._STUDY_ATTENDANCE_CREATED_WRONG_ANSWER, attendanceResponseDTO); + } + } + + @Tag(name = "스터디 출석체크") + @Operation(summary = "[스터디 출석체크] 출석 퀴즈 삭제하기", description = """ + ## [스터디 출석체크] 기한이 지난 출석 퀴즈를 삭제합니다. (화면 X) + * PathVariable을 통해 전달받은 정보를 바탕으로 출석 퀴즈를 삭제합니다. + * 출석 퀴즈 정보와 함께 퀴즈에 대한 MemberAttendance(회원 출석) 목록도 함께 삭제됩니다. + * date에는 출석 퀴즈를 삭제할 날짜를 입력합니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "scheduleId", description = "일정의 id를 입력합니다.", required = true) + @DeleteMapping("/studies/{studyId}/schedules/{scheduleId}/quiz") + public ApiResponse deleteAttendanceQuiz( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistSchedule Long scheduleId, + @RequestParam LocalDate date) { + StudyQuizResponseDTO.QuizDTO quizDTO = scheduleCommandService.deleteAttendanceQuiz(studyId, scheduleId, date); + return ApiResponse.onSuccess(SuccessStatus._STUDY_QUIZ_DELETED, quizDTO); + } + + @Tag(name = "스터디 출석체크") + @Operation(summary = "[스터디 출석체크] 회원 출석부 불러오기", description = """ + ## [스터디 출석체크] 지정된 날짜의 모든 스터디 회원의 출석 여부를 불러옵니다. + * 출석체크 화면에 표시되는 스터디 회원 정보(프로필 사진, 이름, 출석 여부, 스터디장 여부) 목록를 반환합니다. + * date에는 출석 정보를 확인할 날짜를 입력합니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "scheduleId", description = "출석을 확인할 일정의 id를 입력합니다.", required = true) + @GetMapping("/studies/{studyId}/schedules/{scheduleId}/attendance") + public ApiResponse getAllAttendances( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistSchedule Long scheduleId, + @RequestParam LocalDate date) { + StudyQuizResponseDTO.AttendanceListDTO attendanceListDTO = scheduleQueryService.getAllAttendances(studyId, scheduleId, date); + return ApiResponse.onSuccess(SuccessStatus._STUDY_MEMBER_ATTENDANCES_FOUND, attendanceListDTO); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/spot/study/presentation/dto/request/ScheduleRequestDTO.java b/src/main/java/com/example/spot/schedule/presentation/dto/request/ScheduleRequestDTO.java similarity index 92% rename from src/main/java/com/example/spot/study/presentation/dto/request/ScheduleRequestDTO.java rename to src/main/java/com/example/spot/schedule/presentation/dto/request/ScheduleRequestDTO.java index f6404ed5..03e3e8ac 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/request/ScheduleRequestDTO.java +++ b/src/main/java/com/example/spot/schedule/presentation/dto/request/ScheduleRequestDTO.java @@ -1,4 +1,4 @@ -package com.example.spot.study.presentation.dto.request; +package com.example.spot.schedule.presentation.dto.request; import com.example.spot.schedule.domain.enums.SchedulePeriod; import com.example.spot.common.presentation.validator.TextLength; diff --git a/src/main/java/com/example/spot/study/presentation/dto/request/StudyQuizRequestDTO.java b/src/main/java/com/example/spot/schedule/presentation/dto/request/StudyQuizRequestDTO.java similarity index 92% rename from src/main/java/com/example/spot/study/presentation/dto/request/StudyQuizRequestDTO.java rename to src/main/java/com/example/spot/schedule/presentation/dto/request/StudyQuizRequestDTO.java index 3ed655e4..907ed5e6 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/request/StudyQuizRequestDTO.java +++ b/src/main/java/com/example/spot/schedule/presentation/dto/request/StudyQuizRequestDTO.java @@ -1,4 +1,4 @@ -package com.example.spot.study.presentation.dto.request; +package com.example.spot.schedule.presentation.dto.request; import com.example.spot.common.presentation.validator.TextLength; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/example/spot/study/presentation/dto/response/ScheduleResponseDTO.java b/src/main/java/com/example/spot/schedule/presentation/dto/response/ScheduleResponseDTO.java similarity index 98% rename from src/main/java/com/example/spot/study/presentation/dto/response/ScheduleResponseDTO.java rename to src/main/java/com/example/spot/schedule/presentation/dto/response/ScheduleResponseDTO.java index d74f0018..2b56c54e 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/response/ScheduleResponseDTO.java +++ b/src/main/java/com/example/spot/schedule/presentation/dto/response/ScheduleResponseDTO.java @@ -1,4 +1,4 @@ -package com.example.spot.study.presentation.dto.response; +package com.example.spot.schedule.presentation.dto.response; import com.example.spot.schedule.domain.Schedule; import com.example.spot.schedule.domain.enums.SchedulePeriod; diff --git a/src/main/java/com/example/spot/study/presentation/dto/response/StudyQuizResponseDTO.java b/src/main/java/com/example/spot/schedule/presentation/dto/response/StudyQuizResponseDTO.java similarity index 92% rename from src/main/java/com/example/spot/study/presentation/dto/response/StudyQuizResponseDTO.java rename to src/main/java/com/example/spot/schedule/presentation/dto/response/StudyQuizResponseDTO.java index 495a1934..98e88fb7 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/response/StudyQuizResponseDTO.java +++ b/src/main/java/com/example/spot/schedule/presentation/dto/response/StudyQuizResponseDTO.java @@ -1,8 +1,8 @@ -package com.example.spot.study.presentation.dto.response; +package com.example.spot.schedule.presentation.dto.response; -import com.example.spot.schedule.domain.aggregate.Quiz; -import com.example.spot.schedule.domain.aggregate.QuizSubmission; -import com.example.spot.study.domain.aggregate.StudyMember; +import com.example.spot.schedule.domain.association.Quiz; +import com.example.spot.schedule.domain.association.QuizSubmission; +import com.example.spot.study.domain.association.StudyMember; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/example/spot/study/presentation/dto/response/StudyScheduleResponseDTO.java b/src/main/java/com/example/spot/schedule/presentation/dto/response/StudyScheduleResponseDTO.java similarity index 95% rename from src/main/java/com/example/spot/study/presentation/dto/response/StudyScheduleResponseDTO.java rename to src/main/java/com/example/spot/schedule/presentation/dto/response/StudyScheduleResponseDTO.java index bc66ab6b..fdd0fa7f 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/response/StudyScheduleResponseDTO.java +++ b/src/main/java/com/example/spot/schedule/presentation/dto/response/StudyScheduleResponseDTO.java @@ -1,4 +1,4 @@ -package com.example.spot.study.presentation.dto.response; +package com.example.spot.schedule.presentation.dto.response; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/spot/story/application/StoryCommandService.java b/src/main/java/com/example/spot/story/application/StoryCommandService.java new file mode 100644 index 00000000..9ab27cdb --- /dev/null +++ b/src/main/java/com/example/spot/story/application/StoryCommandService.java @@ -0,0 +1,46 @@ +package com.example.spot.story.application; + +import com.example.spot.story.web.dto.request.StoryCommentRequestDTO; +import com.example.spot.story.web.dto.request.StoryRequestDTO; +import com.example.spot.story.web.dto.response.StoryCommentResponseDTO; +import com.example.spot.story.web.dto.response.StoryResDTO; + +public interface StoryCommandService { + + // 스터디 게시글 생성 + StoryResDTO.PostPreviewDTO createPost(Long studyId, StoryRequestDTO.PostDTO postRequestDTO); + + // 스터디 게시글 편집 + StoryResDTO.PostPreviewDTO updatePost(Long studyId, Long postId, StoryRequestDTO.PostDTO postDTO); + + // 스터디 게시글 삭제 + StoryResDTO.PostPreviewDTO deletePost(Long studyId, Long postId); + + // 스터디 게시글 좋아요 + StoryResDTO.PostLikeNumDTO likePost(Long studyId, Long postId); + + // 스터디 게시글 좋아요 취소 + StoryResDTO.PostLikeNumDTO cancelPostLike(Long studyId, Long postId); + + // 스터디 게시글 댓글 생성 + StoryCommentResponseDTO.CommentDTO createComment(Long studyId, Long postId, StoryCommentRequestDTO.CommentDTO commentRequestDTO); + + // 스터디 게시글 답글 생성 + StoryCommentResponseDTO.CommentDTO createReply(Long studyId, Long postId, Long commentId, StoryCommentRequestDTO.CommentDTO commentRequestDTO); + + // 스터디 게시글 댓글 삭제 (댓/답글 구분 X) + StoryCommentResponseDTO.CommentIdDTO deleteComment(Long studyId, Long postId, Long commentId); + + // 스터디 게시글 댓글 좋아요 + StoryCommentResponseDTO.CommentPreviewDTO likeComment(Long studyId, Long postId, Long commentId); + + // 스터디 게시글 댓글 싫어요 + StoryCommentResponseDTO.CommentPreviewDTO dislikeComment(Long studyId, Long postId, Long commentId); + + // 스터디 게시글 댓글 좋아요 취소 + StoryCommentResponseDTO.CommentPreviewDTO cancelCommentLike(Long studyId, Long postId, Long commentId); + + // 스터디 게시글 댓글 싫어요 취소 + StoryCommentResponseDTO.CommentPreviewDTO cancelCommentDislike(Long studyId, Long postId, Long commentId); + +} diff --git a/src/main/java/com/example/spot/study/application/StudyPostCommandServiceImpl.java b/src/main/java/com/example/spot/story/application/StoryCommandServiceImpl.java similarity index 91% rename from src/main/java/com/example/spot/study/application/StudyPostCommandServiceImpl.java rename to src/main/java/com/example/spot/story/application/StoryCommandServiceImpl.java index a78184cb..03c8d3d4 100644 --- a/src/main/java/com/example/spot/study/application/StudyPostCommandServiceImpl.java +++ b/src/main/java/com/example/spot/story/application/StoryCommandServiceImpl.java @@ -1,4 +1,4 @@ -package com.example.spot.study.application; +package com.example.spot.story.application; import com.example.spot.common.api.code.status.ErrorStatus; import com.example.spot.common.api.exception.handler.MemberHandler; @@ -7,16 +7,16 @@ import com.example.spot.notification.domain.Notification; import com.example.spot.story.domain.Story; import com.example.spot.story.domain.StoryRepository; -import com.example.spot.story.domain.aggregate.LikedStory; -import com.example.spot.story.domain.aggregate.LikedStoryComment; -import com.example.spot.story.domain.aggregate.StoryComment; -import com.example.spot.story.domain.aggregate.StoryImage; +import com.example.spot.story.domain.association.LikedStory; +import com.example.spot.story.domain.association.LikedStoryComment; +import com.example.spot.story.domain.association.StoryComment; +import com.example.spot.story.domain.association.StoryImage; import com.example.spot.story.domain.repository.LikedStoryCommentRepository; import com.example.spot.story.domain.repository.LikedStoryRepository; import com.example.spot.story.domain.repository.StoryCommentRepository; import com.example.spot.story.domain.repository.StoryImageRepository; -import com.example.spot.story.domain.repository.StoryReportRepository; -import com.example.spot.study.domain.aggregate.StudyMember; +import com.example.spot.report.domain.StoryReportRepository; +import com.example.spot.study.domain.association.StudyMember; import com.example.spot.study.domain.enums.StudyApplicationStatus; import com.example.spot.notification.domain.enums.NotifyType; import com.example.spot.study.domain.Study; @@ -26,10 +26,10 @@ import com.example.spot.study.domain.StudyRepository; import com.example.spot.common.security.utils.SecurityUtils; import com.example.spot.common.application.s3.S3ImageService; -import com.example.spot.study.presentation.dto.request.StudyPostCommentRequestDTO; -import com.example.spot.study.presentation.dto.request.StudyPostRequestDTO; -import com.example.spot.study.presentation.dto.response.StudyPostCommentResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyPostResDTO; +import com.example.spot.story.web.dto.request.StoryCommentRequestDTO; +import com.example.spot.story.web.dto.request.StoryRequestDTO; +import com.example.spot.story.web.dto.response.StoryCommentResponseDTO; +import com.example.spot.story.web.dto.response.StoryResDTO; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -43,7 +43,7 @@ @Service @Transactional @RequiredArgsConstructor -public class StudyPostCommandServiceImpl implements StudyPostCommandService { +public class StoryCommandServiceImpl implements StoryCommandService { @Value("${image.post.anonymous.profile}") private String defaultImage; @@ -72,7 +72,7 @@ public class StudyPostCommandServiceImpl implements StudyPostCommandService { * @return 작성된 스터디 게시글의 Preview(게시글 아이디, 제목)를 반환합니다. */ @Override - public StudyPostResDTO.PostPreviewDTO createPost(Long studyId, StudyPostRequestDTO.PostDTO postRequestDTO) { + public StoryResDTO.PostPreviewDTO createPost(Long studyId, StoryRequestDTO.PostDTO postRequestDTO) { //=== Exception ===// Long memberId = SecurityUtils.getCurrentUserId(); @@ -157,11 +157,11 @@ public StudyPostResDTO.PostPreviewDTO createPost(Long studyId, StudyPostRequestD member.updateStudyPost(story); study.updateStudyPost(story); - return StudyPostResDTO.PostPreviewDTO.toDTO(story); + return StoryResDTO.PostPreviewDTO.toDTO(story); } @Override - public StudyPostResDTO.PostPreviewDTO updatePost(Long studyId, Long postId, StudyPostRequestDTO.PostDTO postDTO) { + public StoryResDTO.PostPreviewDTO updatePost(Long studyId, Long postId, StoryRequestDTO.PostDTO postDTO) { Long memberId = SecurityUtils.getCurrentUserId(); SecurityUtils.verifyUserId(memberId); @@ -192,10 +192,10 @@ public StudyPostResDTO.PostPreviewDTO updatePost(Long studyId, Long postId, Stud // 스터디 게시글 업데이트 story.updatePost(postDTO); - return StudyPostResDTO.PostPreviewDTO.toDTO(story); + return StoryResDTO.PostPreviewDTO.toDTO(story); } - private void updateStudyPostImage(StudyPostRequestDTO.PostDTO postDTO, Story story) { + private void updateStudyPostImage(StoryRequestDTO.PostDTO postDTO, Story story) { List storyImages = story.getImages(); // 기존 이미지가 존재하는 경우 이미지 유지 if (!StringUtils.hasText(postDTO.getExistingImage())) { @@ -217,7 +217,7 @@ private void updateStudyPostImage(StudyPostRequestDTO.PostDTO postDTO, Story sto * @return 삭제된 스터디 게시글의 Preview(게시글 아이디, 제목)를 반환합니다. */ @Override - public StudyPostResDTO.PostPreviewDTO deletePost(Long studyId, Long postId) { + public StoryResDTO.PostPreviewDTO deletePost(Long studyId, Long postId) { //=== Exception ===// Long memberId = SecurityUtils.getCurrentUserId(); @@ -261,7 +261,7 @@ public StudyPostResDTO.PostPreviewDTO deletePost(Long studyId, Long postId) { throw new StudyHandler(ErrorStatus._STUDY_POST_DELETION_INVALID); } - return StudyPostResDTO.PostPreviewDTO.toDTO(story); + return StoryResDTO.PostPreviewDTO.toDTO(story); } /** @@ -272,7 +272,7 @@ public StudyPostResDTO.PostPreviewDTO deletePost(Long studyId, Long postId) { * @return 게시글의 Preview(게시글 아이디, 제목)와 함께 좋아요 개수가 반환됩니다. */ @Override - public StudyPostResDTO.PostLikeNumDTO likePost(Long studyId, Long postId) { + public StoryResDTO.PostLikeNumDTO likePost(Long studyId, Long postId) { //=== Exception ===// Long memberId = SecurityUtils.getCurrentUserId(); @@ -311,7 +311,7 @@ public StudyPostResDTO.PostLikeNumDTO likePost(Long studyId, Long postId) { story.plusLikeNum(); story = storyRepository.save(story); - return StudyPostResDTO.PostLikeNumDTO.toDTO(story); + return StoryResDTO.PostLikeNumDTO.toDTO(story); } /** @@ -322,7 +322,7 @@ public StudyPostResDTO.PostLikeNumDTO likePost(Long studyId, Long postId) { * @return 게시글의 Preview(게시글 아이디, 제목)와 함께 좋아요 개수가 반환됩니다. */ @Override - public StudyPostResDTO.PostLikeNumDTO cancelPostLike(Long studyId, Long postId) { + public StoryResDTO.PostLikeNumDTO cancelPostLike(Long studyId, Long postId) { //=== Exception ===// Long memberId = SecurityUtils.getCurrentUserId(); @@ -353,7 +353,7 @@ public StudyPostResDTO.PostLikeNumDTO cancelPostLike(Long studyId, Long postId) likedStoryRepository.delete(likedStory); storyRepository.save(story); - return StudyPostResDTO.PostLikeNumDTO.toDTO(story); + return StoryResDTO.PostLikeNumDTO.toDTO(story); } /* ----------------------------- 스터디 게시글 댓글 관련 API ------------------------------------- */ @@ -366,7 +366,7 @@ public StudyPostResDTO.PostLikeNumDTO cancelPostLike(Long studyId, Long postId) * @return 댓글 아이디와 작성자, 내용, 좋아요와 싫어요 개수를 함께 반환합니다. */ @Override - public StudyPostCommentResponseDTO.CommentDTO createComment(Long studyId, Long postId, StudyPostCommentRequestDTO.CommentDTO commentRequestDTO) { + public StoryCommentResponseDTO.CommentDTO createComment(Long studyId, Long postId, StoryCommentRequestDTO.CommentDTO commentRequestDTO) { //=== Exception ===// Long memberId = SecurityUtils.getCurrentUserId(); @@ -410,7 +410,7 @@ public StudyPostCommentResponseDTO.CommentDTO createComment(Long studyId, Long p story.addComment(storyComment); member.addComment(storyComment); - return StudyPostCommentResponseDTO.CommentDTO.toDTO(storyComment, "익명"+anonymousNum, defaultImage); + return StoryCommentResponseDTO.CommentDTO.toDTO(storyComment, "익명"+anonymousNum, defaultImage); } /** @@ -421,7 +421,7 @@ public StudyPostCommentResponseDTO.CommentDTO createComment(Long studyId, Long p * @return 댓글 아이디와 작성자, 내용, 좋아요와 싫어요 개수를 함께 반환합니다. */ @Override - public StudyPostCommentResponseDTO.CommentDTO createReply(Long studyId, Long postId, Long commentId, StudyPostCommentRequestDTO.CommentDTO commentRequestDTO) { + public StoryCommentResponseDTO.CommentDTO createReply(Long studyId, Long postId, Long commentId, StoryCommentRequestDTO.CommentDTO commentRequestDTO) { //=== Exception ===// Long memberId = SecurityUtils.getCurrentUserId(); @@ -470,7 +470,7 @@ public StudyPostCommentResponseDTO.CommentDTO createReply(Long studyId, Long pos member.addComment(storyComment); parentComment.addChildrenComment(storyComment); - return StudyPostCommentResponseDTO.CommentDTO.toDTO(storyComment, "익명"+anonymousNum, defaultImage); + return StoryCommentResponseDTO.CommentDTO.toDTO(storyComment, "익명"+anonymousNum, defaultImage); } /** @@ -482,7 +482,7 @@ public StudyPostCommentResponseDTO.CommentDTO createReply(Long studyId, Long pos * @return 댓글 작성자의 익명 번호를 반환합니다. * 회원이 이미 타겟 스터디 게시글에 익명으로 댓글을 작성한 이력이 있는 경우 해당 번호를 반환합니다. */ - private Integer getAnonymousNum(Long postId, StudyPostCommentRequestDTO.CommentDTO commentRequestDTO, Member member) { + private Integer getAnonymousNum(Long postId, StoryCommentRequestDTO.CommentDTO commentRequestDTO, Member member) { Integer anonymousNum = null; List storyComments = storyCommentRepository.findAllByStoryId(postId); @@ -520,7 +520,7 @@ private Integer getAnonymousNum(Long postId, StudyPostCommentRequestDTO.CommentD * @return 삭제한 댓글의 아이디를 반환합니다. */ @Override - public StudyPostCommentResponseDTO.CommentIdDTO deleteComment(Long studyId, Long postId, Long commentId) { + public StoryCommentResponseDTO.CommentIdDTO deleteComment(Long studyId, Long postId, Long commentId) { //=== Exception ===// Long memberId = SecurityUtils.getCurrentUserId(); @@ -558,7 +558,7 @@ public StudyPostCommentResponseDTO.CommentIdDTO deleteComment(Long studyId, Long member.updateComment(storyComment); storyCommentRepository.save(storyComment); - return new StudyPostCommentResponseDTO.CommentIdDTO(commentId); + return new StoryCommentResponseDTO.CommentIdDTO(commentId); } /** @@ -570,9 +570,9 @@ public StudyPostCommentResponseDTO.CommentIdDTO deleteComment(Long studyId, Long * @return 댓글 아이디와 타겟 댓글의 좋아요 수와 싫어요 수가 반환됩니다. */ @Override - public StudyPostCommentResponseDTO.CommentPreviewDTO likeComment(Long studyId, Long postId, Long commentId) { + public StoryCommentResponseDTO.CommentPreviewDTO likeComment(Long studyId, Long postId, Long commentId) { StoryComment storyComment = saveStudyPostComment(studyId, postId, commentId, Boolean.TRUE); - return StudyPostCommentResponseDTO.CommentPreviewDTO.toDTO(storyComment); + return StoryCommentResponseDTO.CommentPreviewDTO.toDTO(storyComment); } /** @@ -584,9 +584,9 @@ public StudyPostCommentResponseDTO.CommentPreviewDTO likeComment(Long studyId, L * @return 댓글 아이디와 타겟 댓글의 좋아요 수와 싫어요 수가 반환됩니다. */ @Override - public StudyPostCommentResponseDTO.CommentPreviewDTO dislikeComment(Long studyId, Long postId, Long commentId) { + public StoryCommentResponseDTO.CommentPreviewDTO dislikeComment(Long studyId, Long postId, Long commentId) { StoryComment storyComment = saveStudyPostComment(studyId, postId, commentId, Boolean.FALSE); - return StudyPostCommentResponseDTO.CommentPreviewDTO.toDTO(storyComment); + return StoryCommentResponseDTO.CommentPreviewDTO.toDTO(storyComment); } /** @@ -658,7 +658,7 @@ private StoryComment saveStudyPostComment(Long studyId, Long postId, Long commen * @return 댓글 아이디와 타겟 댓글의 좋아요 수와 싫어요 수가 반환됩니다. */ @Override - public StudyPostCommentResponseDTO.CommentPreviewDTO cancelCommentLike(Long studyId, Long postId, Long commentId) { + public StoryCommentResponseDTO.CommentPreviewDTO cancelCommentLike(Long studyId, Long postId, Long commentId) { Long memberId = SecurityUtils.getCurrentUserId(); SecurityUtils.verifyUserId(memberId); @@ -667,7 +667,7 @@ public StudyPostCommentResponseDTO.CommentPreviewDTO cancelCommentLike(Long stud .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_LIKED_COMMENT_NOT_FOUND)); StoryComment storyComment = deleteStudyLikedComment(studyId, postId, commentId, memberId, likedStoryComment); - return StudyPostCommentResponseDTO.CommentPreviewDTO.toDTO(storyComment); + return StoryCommentResponseDTO.CommentPreviewDTO.toDTO(storyComment); } /** @@ -679,7 +679,7 @@ public StudyPostCommentResponseDTO.CommentPreviewDTO cancelCommentLike(Long stud * @return 댓글 아이디와 타겟 댓글의 좋아요 수와 싫어요 수가 반환됩니다. */ @Override - public StudyPostCommentResponseDTO.CommentPreviewDTO cancelCommentDislike(Long studyId, Long postId, Long commentId) { + public StoryCommentResponseDTO.CommentPreviewDTO cancelCommentDislike(Long studyId, Long postId, Long commentId) { Long memberId = SecurityUtils.getCurrentUserId(); SecurityUtils.verifyUserId(memberId); @@ -688,7 +688,7 @@ public StudyPostCommentResponseDTO.CommentPreviewDTO cancelCommentDislike(Long s .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_DISLIKED_COMMENT_NOT_FOUND)); StoryComment storyComment = deleteStudyLikedComment(studyId, postId, commentId, memberId, likedStoryComment); - return StudyPostCommentResponseDTO.CommentPreviewDTO.toDTO(storyComment); + return StoryCommentResponseDTO.CommentPreviewDTO.toDTO(storyComment); } /** diff --git a/src/main/java/com/example/spot/story/application/StoryQueryService.java b/src/main/java/com/example/spot/story/application/StoryQueryService.java new file mode 100644 index 00000000..846f751e --- /dev/null +++ b/src/main/java/com/example/spot/story/application/StoryQueryService.java @@ -0,0 +1,27 @@ +package com.example.spot.story.application; + +import com.example.spot.story.domain.enums.StoryCategoryQuery; +import com.example.spot.story.web.dto.response.StoryCommentResponseDTO; +import com.example.spot.story.web.dto.response.StoryResDTO; +import com.example.spot.story.web.dto.response.StoryResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyImageResponseDTO; +import org.springframework.data.domain.PageRequest; + +public interface StoryQueryService { + + // 스터디 게시글 목록 불러오기 + StoryResDTO.PostListDTO getAllPosts(PageRequest pageRequest, Long studyId, StoryCategoryQuery storyCategoryQuery); + + // 스터디 게시글 불러오기 + StoryResDTO.PostDetailDTO getPost(Long studyId, Long postId, Boolean likeOrScrap); + + // 스터디 공지 게시글 불러오기 + StoryResponseDTO findStudyAnnouncementPost(Long studyId); + + // 스터디 게시글 댓글 목록 불러오기 + StoryCommentResponseDTO.CommentReplyListDTO getAllComments(Long studyId, Long postId); + + // 스터디 이미지 목록 조회 + StudyImageResponseDTO.ImageListDTO getAllStudyImages(Long studyId, PageRequest pageRequest); + +} diff --git a/src/main/java/com/example/spot/study/application/StudyPostQueryServiceImpl.java b/src/main/java/com/example/spot/story/application/StoryQueryServiceImpl.java similarity index 65% rename from src/main/java/com/example/spot/study/application/StudyPostQueryServiceImpl.java rename to src/main/java/com/example/spot/story/application/StoryQueryServiceImpl.java index 9974b350..0f0c6b99 100644 --- a/src/main/java/com/example/spot/study/application/StudyPostQueryServiceImpl.java +++ b/src/main/java/com/example/spot/story/application/StoryQueryServiceImpl.java @@ -1,15 +1,17 @@ -package com.example.spot.study.application; +package com.example.spot.story.application; import com.example.spot.common.api.code.status.ErrorStatus; +import com.example.spot.common.api.exception.GeneralException; import com.example.spot.common.api.exception.handler.MemberHandler; import com.example.spot.common.api.exception.handler.StudyHandler; import com.example.spot.member.domain.Member; import com.example.spot.story.domain.Story; import com.example.spot.story.domain.enums.StoryCategory; +import com.example.spot.story.web.dto.response.StoryResponseDTO; import com.example.spot.study.domain.enums.StudyApplicationStatus; import com.example.spot.story.domain.enums.StoryCategoryQuery; import com.example.spot.study.domain.Study; -import com.example.spot.story.domain.aggregate.StoryComment; +import com.example.spot.story.domain.association.StoryComment; import com.example.spot.member.domain.MemberRepository; import com.example.spot.study.domain.repository.StudyMemberRepository; import com.example.spot.story.domain.repository.LikedStoryRepository; @@ -17,8 +19,9 @@ import com.example.spot.story.domain.StoryRepository; import com.example.spot.study.domain.StudyRepository; import com.example.spot.common.security.utils.SecurityUtils; -import com.example.spot.study.presentation.dto.response.StudyPostCommentResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyPostResDTO; +import com.example.spot.story.web.dto.response.StoryCommentResponseDTO; +import com.example.spot.story.web.dto.response.StoryResDTO; +import com.example.spot.study.presentation.dto.response.StudyImageResponseDTO; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; @@ -31,7 +34,7 @@ @Service @Transactional(readOnly = true) @RequiredArgsConstructor -public class StudyPostQueryServiceImpl implements StudyPostQueryService { +public class StoryQueryServiceImpl implements StoryQueryService { private final StoryCommentRepository storyCommentRepository; private final LikedStoryRepository likedStoryRepository; @@ -56,7 +59,7 @@ public class StudyPostQueryServiceImpl implements StudyPostQueryService { * 2. themeQuery가 null인 경우 필터링 없이 게시글 목록을 반환합니다. */ @Override - public StudyPostResDTO.PostListDTO getAllPosts(PageRequest pageRequest, Long studyId, StoryCategoryQuery storyCategoryQuery) { + public StoryResDTO.PostListDTO getAllPosts(PageRequest pageRequest, Long studyId, StoryCategoryQuery storyCategoryQuery) { Long memberId = SecurityUtils.getCurrentUserId(); SecurityUtils.verifyUserId(memberId); @@ -89,14 +92,14 @@ else if (storyCategoryQuery.equals(StoryCategoryQuery.ANNOUNCEMENT)) { totalPosts = storyRepository.countByStudyIdAndStoryCategory(studyId, storyCategory); } - return StudyPostResDTO.PostListDTO.builder() + return StoryResDTO.PostListDTO.builder() .studyId(studyId) .posts(stories.stream() .map(studyPost -> { if (likedStoryRepository.existsByMemberIdAndStoryId(memberId, studyPost.getId())) { - return StudyPostResDTO.PostDTO.toDTO(studyPost, true); + return StoryResDTO.PostDTO.toDTO(studyPost, true); } else { - return StudyPostResDTO.PostDTO.toDTO(studyPost, false); + return StoryResDTO.PostDTO.toDTO(studyPost, false); } }) .toList()) @@ -116,7 +119,7 @@ else if (storyCategoryQuery.equals(StoryCategoryQuery.ANNOUNCEMENT)) { */ @Override @Transactional(readOnly = false) - public StudyPostResDTO.PostDetailDTO getPost(Long studyId, Long postId, Boolean likeOrScrap) { + public StoryResDTO.PostDetailDTO getPost(Long studyId, Long postId, Boolean likeOrScrap) { //=== Exception ===// Long memberId = SecurityUtils.getCurrentUserId(); @@ -151,10 +154,46 @@ public StudyPostResDTO.PostDetailDTO getPost(Long studyId, Long postId, Boolean Integer commentNum = storyCommentRepository.findAllByStoryId(postId).size(); boolean isLiked = likedStoryRepository.existsByMemberIdAndStoryId(memberId, story.getId()); boolean isWriter = story.getMember().getId().equals(memberId); - return StudyPostResDTO.PostDetailDTO.toDTO(story, commentNum, isLiked, isWriter); + return StoryResDTO.PostDetailDTO.toDTO(story, commentNum, isLiked, isWriter); } -/* ----------------------------- 스터디 게시글 댓글 관련 API ------------------------------------- */ + /** + * 스터디 최근 공지사항을 1개 조회합니다. + * + * @param studyId 스터디 ID + * @return 제목과 내용을 반환합니다. + * @throws GeneralException 스터디 공지사항이 존재하지 않는 경우 + * @throws GeneralException 스터디 멤버가 아닌 경우 + */ + @Override + public StoryResponseDTO findStudyAnnouncementPost(Long studyId) { + + // 로그인한 회원이 해당 스터디 회원인지 확인 + if (!isMember(SecurityUtils.getCurrentUserId(), studyId)) + throw new GeneralException(ErrorStatus._ONLY_STUDY_MEMBER_CAN_ACCESS_ANNOUNCEMENT_POST); + + // 스터디 공지사항 조회 + Story story = storyRepository.findByStudyIdAndIsAnnouncement( + studyId, true).orElseThrow(() -> new GeneralException(ErrorStatus._STUDY_POST_NOT_FOUND)); + + // DTO로 변환하여 반환 + return StoryResponseDTO.builder() + .title(story.getTitle()) + .content(story.getContent()).build(); + } + + /** + * 회원이 스터디 구성원인지 확인합니다. + * + * @param memberId 확인 하려는 회원 ID + * @param studyId 확인 하려는 스터디 ID + * @return 스터디 참여 여부를 반환합니다. + */ + private boolean isMember(Long memberId, Long studyId) { + return studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED).isPresent(); + } + + /* ----------------------------- 스터디 게시글 댓글 관련 API ------------------------------------- */ /** * 특정 스터디 게시글의 모든 댓글을 조회하는 메서드입니다. @@ -163,7 +202,7 @@ public StudyPostResDTO.PostDetailDTO getPost(Long studyId, Long postId, Boolean * @return 스터디 게시글에 작성된 댓글의 목록을 반환합니다. 하나의 댓글에는 해당 댓글에 대한 답글 목록이 포함되어 있습니다. */ @Override - public StudyPostCommentResponseDTO.CommentReplyListDTO getAllComments(Long studyId, Long postId) { + public StoryCommentResponseDTO.CommentReplyListDTO getAllComments(Long studyId, Long postId) { //=== Exception ===// Long memberId = SecurityUtils.getCurrentUserId(); @@ -190,7 +229,43 @@ public StudyPostCommentResponseDTO.CommentReplyListDTO getAllComments(Long study .sorted(Comparator.comparing(StoryComment::getCreatedAt)) .toList(); - return StudyPostCommentResponseDTO.CommentReplyListDTO.toDTO(story.getId(), storyComments, member, defaultImage); + return StoryCommentResponseDTO.CommentReplyListDTO.toDTO(story.getId(), storyComments, member, defaultImage); + } + + +/* ----------------------------- 스터디 갤러리 관련 API ------------------------------------- */ + + /** + * 스터디 게시판에 업로드한 이미지 목록을 불러오는 메서드입니다. + * @param studyId 타겟 스터디의 아이디를 입력 받습니다. + * @param pageRequest 페이징에 필요한 페이지 번호와 크기를 입력 받습니다. + * @return 스터디 아이디와 해당 스터디에 업로드된 이미지 목록을 반환합니다. + */ + @Override + public StudyImageResponseDTO.ImageListDTO getAllStudyImages(Long studyId, PageRequest pageRequest) { + + //=== Exception ===// + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + memberRepository.findById(memberId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + //=== Feature ===// + List images = storyRepository.findAllByStudyId(studyId, pageRequest) + .stream() + .sorted(Comparator.comparing(Story::getCreatedAt).reversed()) + .flatMap(studyPost -> studyPost.getImages().stream()) + .map(StudyImageResponseDTO.ImageDTO::toDTO) + .toList(); + + return StudyImageResponseDTO.ImageListDTO.toDTO(studyId, images); + } } diff --git a/src/main/java/com/example/spot/story/domain/Story.java b/src/main/java/com/example/spot/story/domain/Story.java index 7301946a..8306ec88 100644 --- a/src/main/java/com/example/spot/story/domain/Story.java +++ b/src/main/java/com/example/spot/story/domain/Story.java @@ -2,13 +2,13 @@ import com.example.spot.member.domain.Member; import com.example.spot.common.entity.BaseEntity; -import com.example.spot.story.domain.aggregate.LikedStory; -import com.example.spot.story.domain.aggregate.StoryComment; -import com.example.spot.story.domain.aggregate.StoryImage; -import com.example.spot.story.domain.aggregate.StoryReport; +import com.example.spot.story.domain.association.LikedStory; +import com.example.spot.story.domain.association.StoryComment; +import com.example.spot.story.domain.association.StoryImage; +import com.example.spot.report.domain.StoryReport; import com.example.spot.story.domain.enums.StoryCategory; import com.example.spot.study.domain.Study; -import com.example.spot.study.presentation.dto.request.StudyPostRequestDTO; +import com.example.spot.story.web.dto.request.StoryRequestDTO; import jakarta.persistence.*; import java.time.LocalDateTime; @@ -136,7 +136,7 @@ public void addStudyPostReport(StoryReport storyReport) { storyReports.add(storyReport); } - public void updatePost(StudyPostRequestDTO.PostDTO requestDTO) { + public void updatePost(StoryRequestDTO.PostDTO requestDTO) { isAnnouncement = requestDTO.getIsAnnouncement(); storyCategory = requestDTO.getStoryCategory(); title = requestDTO.getTitle(); diff --git a/src/main/java/com/example/spot/story/domain/aggregate/LikedStory.java b/src/main/java/com/example/spot/story/domain/association/LikedStory.java similarity index 95% rename from src/main/java/com/example/spot/story/domain/aggregate/LikedStory.java rename to src/main/java/com/example/spot/story/domain/association/LikedStory.java index 0737fdf9..9a93bdd4 100644 --- a/src/main/java/com/example/spot/story/domain/aggregate/LikedStory.java +++ b/src/main/java/com/example/spot/story/domain/association/LikedStory.java @@ -1,4 +1,4 @@ -package com.example.spot.story.domain.aggregate; +package com.example.spot.story.domain.association; import com.example.spot.member.domain.Member; import com.example.spot.common.entity.BaseEntity; diff --git a/src/main/java/com/example/spot/story/domain/aggregate/LikedStoryComment.java b/src/main/java/com/example/spot/story/domain/association/LikedStoryComment.java similarity index 94% rename from src/main/java/com/example/spot/story/domain/aggregate/LikedStoryComment.java rename to src/main/java/com/example/spot/story/domain/association/LikedStoryComment.java index da2ebcd3..7bb475eb 100644 --- a/src/main/java/com/example/spot/story/domain/aggregate/LikedStoryComment.java +++ b/src/main/java/com/example/spot/story/domain/association/LikedStoryComment.java @@ -1,4 +1,4 @@ -package com.example.spot.story.domain.aggregate; +package com.example.spot.story.domain.association; import com.example.spot.member.domain.Member; import com.example.spot.common.entity.BaseEntity; diff --git a/src/main/java/com/example/spot/story/domain/aggregate/StoryComment.java b/src/main/java/com/example/spot/story/domain/association/StoryComment.java similarity index 98% rename from src/main/java/com/example/spot/story/domain/aggregate/StoryComment.java rename to src/main/java/com/example/spot/story/domain/association/StoryComment.java index c8088eaf..41d5d48f 100644 --- a/src/main/java/com/example/spot/story/domain/aggregate/StoryComment.java +++ b/src/main/java/com/example/spot/story/domain/association/StoryComment.java @@ -1,4 +1,4 @@ -package com.example.spot.story.domain.aggregate; +package com.example.spot.story.domain.association; import com.example.spot.member.domain.Member; import com.example.spot.common.entity.BaseEntity; diff --git a/src/main/java/com/example/spot/story/domain/aggregate/StoryImage.java b/src/main/java/com/example/spot/story/domain/association/StoryImage.java similarity index 94% rename from src/main/java/com/example/spot/story/domain/aggregate/StoryImage.java rename to src/main/java/com/example/spot/story/domain/association/StoryImage.java index 862c9bef..f9f16bc2 100644 --- a/src/main/java/com/example/spot/story/domain/aggregate/StoryImage.java +++ b/src/main/java/com/example/spot/story/domain/association/StoryImage.java @@ -1,4 +1,4 @@ -package com.example.spot.story.domain.aggregate; +package com.example.spot.story.domain.association; import com.example.spot.common.entity.BaseEntity; import com.example.spot.story.domain.Story; diff --git a/src/main/java/com/example/spot/story/domain/repository/LikedStoryCommentRepository.java b/src/main/java/com/example/spot/story/domain/repository/LikedStoryCommentRepository.java index 8e90f078..b443066d 100644 --- a/src/main/java/com/example/spot/story/domain/repository/LikedStoryCommentRepository.java +++ b/src/main/java/com/example/spot/story/domain/repository/LikedStoryCommentRepository.java @@ -1,6 +1,6 @@ package com.example.spot.story.domain.repository; -import com.example.spot.story.domain.aggregate.LikedStoryComment; +import com.example.spot.story.domain.association.LikedStoryComment; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/spot/story/domain/repository/LikedStoryRepository.java b/src/main/java/com/example/spot/story/domain/repository/LikedStoryRepository.java index 255948db..a9fd00d2 100644 --- a/src/main/java/com/example/spot/story/domain/repository/LikedStoryRepository.java +++ b/src/main/java/com/example/spot/story/domain/repository/LikedStoryRepository.java @@ -1,6 +1,6 @@ package com.example.spot.story.domain.repository; -import com.example.spot.story.domain.aggregate.LikedStory; +import com.example.spot.story.domain.association.LikedStory; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/spot/story/domain/repository/StoryCommentRepository.java b/src/main/java/com/example/spot/story/domain/repository/StoryCommentRepository.java index 1ab6dfac..c084dbae 100644 --- a/src/main/java/com/example/spot/story/domain/repository/StoryCommentRepository.java +++ b/src/main/java/com/example/spot/story/domain/repository/StoryCommentRepository.java @@ -1,6 +1,6 @@ package com.example.spot.story.domain.repository; -import com.example.spot.story.domain.aggregate.StoryComment; +import com.example.spot.story.domain.association.StoryComment; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/spot/story/domain/repository/StoryImageRepository.java b/src/main/java/com/example/spot/story/domain/repository/StoryImageRepository.java index 987d5d4c..f6f31687 100644 --- a/src/main/java/com/example/spot/story/domain/repository/StoryImageRepository.java +++ b/src/main/java/com/example/spot/story/domain/repository/StoryImageRepository.java @@ -1,6 +1,6 @@ package com.example.spot.story.domain.repository; -import com.example.spot.story.domain.aggregate.StoryImage; +import com.example.spot.story.domain.association.StoryImage; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; diff --git a/src/main/java/com/example/spot/study/presentation/controller/StudyPostController.java b/src/main/java/com/example/spot/story/web/controller/StoryController.java similarity index 73% rename from src/main/java/com/example/spot/study/presentation/controller/StudyPostController.java rename to src/main/java/com/example/spot/story/web/controller/StoryController.java index 418c9dfc..05c7caed 100644 --- a/src/main/java/com/example/spot/study/presentation/controller/StudyPostController.java +++ b/src/main/java/com/example/spot/story/web/controller/StoryController.java @@ -1,19 +1,20 @@ -package com.example.spot.study.presentation.controller; +package com.example.spot.story.web.controller; import com.example.spot.common.api.ApiResponse; import com.example.spot.common.api.code.status.SuccessStatus; import com.example.spot.story.domain.enums.StoryCategoryQuery; -import com.example.spot.common.application.s3.S3ImageService; -import com.example.spot.study.application.StudyPostCommandService; -import com.example.spot.study.application.StudyPostQueryService; +import com.example.spot.story.application.StoryCommandService; +import com.example.spot.story.application.StoryQueryService; +import com.example.spot.story.web.dto.response.StoryResponseDTO; import com.example.spot.study.domain.validation.annotation.ExistStudy; import com.example.spot.story.domain.validation.annotation.ExistStory; import com.example.spot.story.domain.validation.annotation.ExistStoryComment; -import com.example.spot.study.presentation.dto.request.StudyPostCommentRequestDTO; -import com.example.spot.study.presentation.dto.request.StudyPostRequestDTO; -import com.example.spot.study.presentation.dto.response.StudyPostCommentResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyPostResDTO; +import com.example.spot.story.web.dto.request.StoryCommentRequestDTO; +import com.example.spot.story.web.dto.request.StoryRequestDTO; +import com.example.spot.story.web.dto.response.StoryCommentResponseDTO; +import com.example.spot.story.web.dto.response.StoryResDTO; +import com.example.spot.study.presentation.dto.response.StudyImageResponseDTO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -29,11 +30,10 @@ @RequiredArgsConstructor @RequestMapping("/spot") @Validated -public class StudyPostController { +public class StoryController { - private final StudyPostQueryService studyPostQueryService; - private final StudyPostCommandService studyPostCommandService; - private final S3ImageService s3ImageService; + private final StoryQueryService storyQueryService; + private final StoryCommandService storyCommandService; /* ----------------------------- 스터디 게시글 관련 API ------------------------------------- */ @@ -44,10 +44,10 @@ public class StudyPostController { """) @Parameter(name = "studyId", description = "게시글을 작성할 스터디의 id를 입력합니다.", required = true) @PostMapping(value = "/studies/{studyId}/posts", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ApiResponse createPost( + public ApiResponse createPost( @PathVariable @ExistStudy Long studyId, - @ModelAttribute(name = "post") @Valid StudyPostRequestDTO.PostDTO postRequestDTO) { - StudyPostResDTO.PostPreviewDTO postPreviewDTO = studyPostCommandService.createPost(studyId, postRequestDTO); + @ModelAttribute(name = "post") @Valid StoryRequestDTO.PostDTO postRequestDTO) { + StoryResDTO.PostPreviewDTO postPreviewDTO = storyCommandService.createPost(studyId, postRequestDTO); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_CREATED, postPreviewDTO); } @@ -59,12 +59,12 @@ public ApiResponse createPost( @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) @Parameter(name = "postId", description = "편집할 스터디 게시글의 id를 입력합니다.", required = true) @PatchMapping(value = "/studies/{studyId}/posts/{postId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ApiResponse updatePost( + public ApiResponse updatePost( @PathVariable @ExistStudy Long studyId, @PathVariable @ExistStory Long postId, - @ModelAttribute(name= "post") @Valid StudyPostRequestDTO.PostDTO postDTO + @ModelAttribute(name= "post") @Valid StoryRequestDTO.PostDTO postDTO ) { - StudyPostResDTO.PostPreviewDTO postPreviewDTO = studyPostCommandService.updatePost(studyId, postId, postDTO); + StoryResDTO.PostPreviewDTO postPreviewDTO = storyCommandService.updatePost(studyId, postId, postDTO); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_UPDATED, postPreviewDTO); } @@ -77,10 +77,10 @@ public ApiResponse updatePost( @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) @Parameter(name = "postId", description = "삭제할 스터디 게시글의 id를 입력합니다.", required = true) @DeleteMapping("/studies/{studyId}/posts/{postId}") - public ApiResponse deletePost( + public ApiResponse deletePost( @PathVariable @ExistStudy Long studyId, @PathVariable @ExistStory Long postId) { - StudyPostResDTO.PostPreviewDTO postPreviewDTO = studyPostCommandService.deletePost(studyId, postId); + StoryResDTO.PostPreviewDTO postPreviewDTO = storyCommandService.deletePost(studyId, postId); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_DELETED, postPreviewDTO); } @@ -95,12 +95,12 @@ public ApiResponse deletePost( """) @Parameter(name = "studyId", description = "게시글 목록을 불러올 스터디의 id를 입력합니다.", required = true) @GetMapping("/studies/{studyId}/posts") - public ApiResponse getAllPosts( + public ApiResponse getAllPosts( @PathVariable @ExistStudy Long studyId, @RequestParam(required = false) StoryCategoryQuery storyCategoryQuery, @RequestParam @Min(0) Integer offset, @RequestParam @Min(1) Integer limit) { - StudyPostResDTO.PostListDTO postListDTO = studyPostQueryService.getAllPosts(PageRequest.of(offset, limit), studyId, storyCategoryQuery); + StoryResDTO.PostListDTO postListDTO = storyQueryService.getAllPosts(PageRequest.of(offset, limit), studyId, storyCategoryQuery); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_LIST_FOUND, postListDTO); } @@ -112,14 +112,25 @@ public ApiResponse getAllPosts( @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) @Parameter(name = "postId", description = "불러올 스터디 게시글의 id를 입력합니다.", required = true) @GetMapping("/studies/{studyId}/posts/{postId}") - public ApiResponse getPost( + public ApiResponse getPost( @PathVariable @ExistStudy Long studyId, @PathVariable @ExistStory Long postId, @RequestParam Boolean likeOrScrap) { - StudyPostResDTO.PostDetailDTO postDetailDTO = studyPostQueryService.getPost(studyId, postId, likeOrScrap); + StoryResDTO.PostDetailDTO postDetailDTO = storyQueryService.getPost(studyId, postId, likeOrScrap); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_FOUND, postDetailDTO); } + @Tag(name = "스터디 상세 정보") + @Operation(summary = "[스터디 상세 정보] 스터디 최근 공지 1개 불러오기", description = """ + ## [스터디 상세 정보] 내 스터디 > 스터디 클릭, 로그인한 회원이 참여하는 특정 스터디의 최근 공지 1개를 불러옵니다. + study_post의 announced_at이 가장 최근인 공지 1개가 반환됩니다. + """) + @GetMapping("/studies/{studyId}/announce") + public ApiResponse getRecentAnnouncement(@PathVariable @ExistStudy Long studyId) { + StoryResponseDTO storyResponseDTO = storyQueryService.findStudyAnnouncementPost(studyId); + return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_FOUND, storyResponseDTO); + } + @Tag(name = "스터디 게시글") @Operation(summary = "[스터디 게시글] 좋아요 누르기", description = """ ## [스터디 게시글] 내 스터디 > 스터디 > 게시판 > 게시글 클릭, 로그인한 회원이 참여하는 특정 스터디의 게시글에 좋아요를 누릅니다. @@ -128,10 +139,10 @@ public ApiResponse getPost( @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) @Parameter(name = "postId", description = "좋아요를 누를 스터디 게시글의 id를 입력합니다.", required = true) @PostMapping("/studies/{studyId}/posts/{postId}/likes") - public ApiResponse likePost( + public ApiResponse likePost( @PathVariable @ExistStudy Long studyId, @PathVariable @ExistStory Long postId) { - StudyPostResDTO.PostLikeNumDTO postLikeNumDTO = studyPostCommandService.likePost(studyId, postId); + StoryResDTO.PostLikeNumDTO postLikeNumDTO = storyCommandService.likePost(studyId, postId); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_LIKED, postLikeNumDTO); } @@ -143,10 +154,10 @@ public ApiResponse likePost( @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) @Parameter(name = "postId", description = "좋아요를 취소할 스터디 게시글의 id를 입력합니다.", required = true) @DeleteMapping("/studies/{studyId}/posts/{postId}/likes") - public ApiResponse cancelPostLike( + public ApiResponse cancelPostLike( @PathVariable @ExistStudy Long studyId, @PathVariable @ExistStory Long postId) { - StudyPostResDTO.PostLikeNumDTO postLikeNumDTO = studyPostCommandService.cancelPostLike(studyId, postId); + StoryResDTO.PostLikeNumDTO postLikeNumDTO = storyCommandService.cancelPostLike(studyId, postId); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_DISLIKED, postLikeNumDTO); } @@ -160,11 +171,11 @@ public ApiResponse cancelPostLike( @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) @Parameter(name = "postId", description = "댓글을 작성할 스터디 게시글의 id를 입력합니다.", required = true) @PostMapping("/studies/{studyId}/posts/{postId}/comments") - public ApiResponse createComment( + public ApiResponse createComment( @PathVariable @ExistStudy Long studyId, @PathVariable @ExistStory Long postId, - @RequestBody @Valid StudyPostCommentRequestDTO.CommentDTO commentRequestDTO) { - StudyPostCommentResponseDTO.CommentDTO commentResponseDTO = studyPostCommandService.createComment(studyId, postId, commentRequestDTO); + @RequestBody @Valid StoryCommentRequestDTO.CommentDTO commentRequestDTO) { + StoryCommentResponseDTO.CommentDTO commentResponseDTO = storyCommandService.createComment(studyId, postId, commentRequestDTO); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_COMMENT_CREATED, commentResponseDTO); } @@ -177,12 +188,12 @@ public ApiResponse createComment( @Parameter(name = "postId", description = "스터디 게시글의 id를 입력합니다.", required = true) @Parameter(name = "commentId", description = "답글을 작성할 댓글의 id를 입력합니다.", required = true) @PostMapping("/studies/{studyId}/posts/{postId}/comments/{commentId}/replies") - public ApiResponse createReply( + public ApiResponse createReply( @PathVariable @ExistStudy Long studyId, @PathVariable @ExistStory Long postId, @PathVariable @ExistStoryComment Long commentId, - @RequestBody @Valid StudyPostCommentRequestDTO.CommentDTO commentRequestDTO) { - StudyPostCommentResponseDTO.CommentDTO commentResponseDTO = studyPostCommandService.createReply(studyId, postId, commentId, commentRequestDTO); + @RequestBody @Valid StoryCommentRequestDTO.CommentDTO commentRequestDTO) { + StoryCommentResponseDTO.CommentDTO commentResponseDTO = storyCommandService.createReply(studyId, postId, commentId, commentRequestDTO); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_COMMENT_CREATED, commentResponseDTO); } @@ -195,11 +206,11 @@ public ApiResponse createReply( @Parameter(name = "postId", description = "스터디 게시글의 id를 입력합니다.", required = true) @Parameter(name = "commentId", description = "삭제할 댓글의 id를 입력합니다.", required = true) @PatchMapping("/studies/{studyId}/posts/{postId}/comments/{commentId}") - public ApiResponse deleteComment( + public ApiResponse deleteComment( @PathVariable @ExistStudy Long studyId, @PathVariable @ExistStory Long postId, @PathVariable @ExistStoryComment Long commentId) { - StudyPostCommentResponseDTO.CommentIdDTO commentPreviewDTO = studyPostCommandService.deleteComment(studyId, postId, commentId); + StoryCommentResponseDTO.CommentIdDTO commentPreviewDTO = storyCommandService.deleteComment(studyId, postId, commentId); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_COMMENT_DELETED, commentPreviewDTO); } @@ -212,11 +223,11 @@ public ApiResponse deleteComment( @Parameter(name = "postId", description = "스터디 게시글의 id를 입력합니다.", required = true) @Parameter(name = "commentId", description = "좋아요를 누를 댓글의 id를 입력합니다.", required = true) @PostMapping("/studies/{studyId}/posts/{postId}/comments/{commentId}/likes") - public ApiResponse likeComment( + public ApiResponse likeComment( @PathVariable @ExistStudy Long studyId, @PathVariable @ExistStory Long postId, @PathVariable @ExistStoryComment Long commentId) { - StudyPostCommentResponseDTO.CommentPreviewDTO commentPreviewDTO = studyPostCommandService.likeComment(studyId, postId, commentId); + StoryCommentResponseDTO.CommentPreviewDTO commentPreviewDTO = storyCommandService.likeComment(studyId, postId, commentId); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_COMMENT_LIKED, commentPreviewDTO); } @@ -229,11 +240,11 @@ public ApiResponse likeComment( @Parameter(name = "postId", description = "스터디 게시글의 id를 입력합니다.", required = true) @Parameter(name = "commentId", description = "싫어요를 누를 댓글의 id를 입력합니다.", required = true) @PostMapping("/studies/{studyId}/posts/{postId}/comments/{commentId}/dislikes") - public ApiResponse dislikeComment( + public ApiResponse dislikeComment( @PathVariable @ExistStudy Long studyId, @PathVariable @ExistStory Long postId, @PathVariable @ExistStoryComment Long commentId) { - StudyPostCommentResponseDTO.CommentPreviewDTO commentPreviewDTO = studyPostCommandService.dislikeComment(studyId, postId, commentId); + StoryCommentResponseDTO.CommentPreviewDTO commentPreviewDTO = storyCommandService.dislikeComment(studyId, postId, commentId); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_COMMENT_DISLIKED, commentPreviewDTO); } @@ -246,11 +257,11 @@ public ApiResponse dislikeComment @Parameter(name = "postId", description = "스터디 게시글의 id를 입력합니다.", required = true) @Parameter(name = "commentId", description = "좋아요를 취소할 댓글의 id를 입력합니다.", required = true) @DeleteMapping("/studies/{studyId}/posts/{postId}/comments/{commentId}/likes") - public ApiResponse cancelCommentLike( + public ApiResponse cancelCommentLike( @PathVariable @ExistStudy Long studyId, @PathVariable @ExistStory Long postId, @PathVariable @ExistStoryComment Long commentId) { - StudyPostCommentResponseDTO.CommentPreviewDTO commentPreviewDTO = studyPostCommandService.cancelCommentLike(studyId, postId, commentId); + StoryCommentResponseDTO.CommentPreviewDTO commentPreviewDTO = storyCommandService.cancelCommentLike(studyId, postId, commentId); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_COMMENT_LIKE_CANCELED, commentPreviewDTO); } @@ -263,11 +274,11 @@ public ApiResponse cancelCommentL @Parameter(name = "postId", description = "스터디 게시글의 id를 입력합니다.", required = true) @Parameter(name = "commentId", description = "싫어요를 취소할 댓글의 id를 입력합니다.", required = true) @DeleteMapping("/studies/{studyId}/posts/{postId}/comments/{commentId}/dislikes") - public ApiResponse cancelCommentDislike( + public ApiResponse cancelCommentDislike( @PathVariable @ExistStudy Long studyId, @PathVariable @ExistStory Long postId, @PathVariable @ExistStoryComment Long commentId) { - StudyPostCommentResponseDTO.CommentPreviewDTO commentPreviewDTO = studyPostCommandService.cancelCommentDislike(studyId, postId, commentId); + StoryCommentResponseDTO.CommentPreviewDTO commentPreviewDTO = storyCommandService.cancelCommentDislike(studyId, postId, commentId); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_COMMENT_DISLIKE_CANCELED, commentPreviewDTO); } @@ -279,11 +290,28 @@ public ApiResponse cancelCommentD @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) @Parameter(name = "postId", description = "댓글을 불러올 스터디 게시글의 id를 입력합니다.", required = true) @GetMapping("/studies/{studyId}/posts/{postId}/comments") - public ApiResponse getAllComments( + public ApiResponse getAllComments( @PathVariable @ExistStudy Long studyId, @PathVariable @ExistStory Long postId) { - StudyPostCommentResponseDTO.CommentReplyListDTO commentReplyListDTO = studyPostQueryService.getAllComments(studyId, postId); + StoryCommentResponseDTO.CommentReplyListDTO commentReplyListDTO = storyQueryService.getAllComments(studyId, postId); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_COMMENT_FOUND, commentReplyListDTO); } +/* ----------------------------- 스터디 게시글 이미지 관련 API ------------------------------------- */ + + @Tag(name = "스터디 게시글 - 갤러리") + @Operation(summary = "[스터디 게시글 - 갤러리] 스터디 이미지 목록 불러오기", description = """ + ## [스터디 게시글] 내 스터디 > 스터디 > 갤러리 클릭, 로그인한 회원이 참여하는 스터디의 이미지 목록을 불러옵니다. + 스터디에 존재하는 모든 게시글의 이미지를 최신순으로 반환합니다. + """) + @Parameter(name = "studyId", description = "이미지 목록을 불러올 스터디의 id를 입력합니다.", required = true) + @GetMapping("/studies/{studyId}/images") + public ApiResponse getAllStudyImages( + @PathVariable @ExistStudy Long studyId, + @RequestParam @Min(0) Integer offset, + @RequestParam @Min(1) Integer limit) { + StudyImageResponseDTO.ImageListDTO imageListDTO = storyQueryService.getAllStudyImages(studyId, PageRequest.of(offset, limit)); + return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_IMAGES_FOUND, imageListDTO); + } + } diff --git a/src/main/java/com/example/spot/study/presentation/dto/request/StudyPostCommentRequestDTO.java b/src/main/java/com/example/spot/story/web/dto/request/StoryCommentRequestDTO.java similarity index 80% rename from src/main/java/com/example/spot/study/presentation/dto/request/StudyPostCommentRequestDTO.java rename to src/main/java/com/example/spot/story/web/dto/request/StoryCommentRequestDTO.java index 1aee8ab2..71d95c57 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/request/StudyPostCommentRequestDTO.java +++ b/src/main/java/com/example/spot/story/web/dto/request/StoryCommentRequestDTO.java @@ -1,4 +1,4 @@ -package com.example.spot.study.presentation.dto.request; +package com.example.spot.story.web.dto.request; import com.example.spot.common.presentation.validator.TextLength; import lombok.AllArgsConstructor; @@ -7,7 +7,7 @@ import lombok.NoArgsConstructor; @Getter -public class StudyPostCommentRequestDTO { +public class StoryCommentRequestDTO { @Getter @Builder diff --git a/src/main/java/com/example/spot/study/presentation/dto/request/StudyPostRequestDTO.java b/src/main/java/com/example/spot/story/web/dto/request/StoryRequestDTO.java similarity index 92% rename from src/main/java/com/example/spot/study/presentation/dto/request/StudyPostRequestDTO.java rename to src/main/java/com/example/spot/story/web/dto/request/StoryRequestDTO.java index 557334fa..419ec847 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/request/StudyPostRequestDTO.java +++ b/src/main/java/com/example/spot/story/web/dto/request/StoryRequestDTO.java @@ -1,4 +1,4 @@ -package com.example.spot.study.presentation.dto.request; +package com.example.spot.story.web.dto.request; import com.example.spot.story.domain.enums.StoryCategory; import com.example.spot.common.presentation.validator.TextLength; @@ -8,7 +8,7 @@ import org.springframework.web.multipart.MultipartFile; @Getter -public class StudyPostRequestDTO { +public class StoryRequestDTO { @Getter @Setter diff --git a/src/main/java/com/example/spot/study/presentation/dto/response/StudyPostCommentResponseDTO.java b/src/main/java/com/example/spot/story/web/dto/response/StoryCommentResponseDTO.java similarity index 96% rename from src/main/java/com/example/spot/study/presentation/dto/response/StudyPostCommentResponseDTO.java rename to src/main/java/com/example/spot/story/web/dto/response/StoryCommentResponseDTO.java index e7a8f5e6..c8c7b645 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/response/StudyPostCommentResponseDTO.java +++ b/src/main/java/com/example/spot/story/web/dto/response/StoryCommentResponseDTO.java @@ -1,8 +1,8 @@ -package com.example.spot.study.presentation.dto.response; +package com.example.spot.story.web.dto.response; import com.example.spot.member.domain.Member; -import com.example.spot.story.domain.aggregate.LikedStoryComment; -import com.example.spot.story.domain.aggregate.StoryComment; +import com.example.spot.story.domain.association.LikedStoryComment; +import com.example.spot.story.domain.association.StoryComment; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -12,7 +12,7 @@ import java.util.List; @Getter -public class StudyPostCommentResponseDTO { +public class StoryCommentResponseDTO { @Getter @RequiredArgsConstructor(access = AccessLevel.PRIVATE) diff --git a/src/main/java/com/example/spot/study/presentation/dto/response/StudyPostResDTO.java b/src/main/java/com/example/spot/story/web/dto/response/StoryResDTO.java similarity index 97% rename from src/main/java/com/example/spot/study/presentation/dto/response/StudyPostResDTO.java rename to src/main/java/com/example/spot/story/web/dto/response/StoryResDTO.java index 7d23ad32..2acca7c3 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/response/StudyPostResDTO.java +++ b/src/main/java/com/example/spot/story/web/dto/response/StoryResDTO.java @@ -1,16 +1,16 @@ -package com.example.spot.study.presentation.dto.response; +package com.example.spot.story.web.dto.response; import com.example.spot.member.domain.Member; import com.example.spot.story.domain.Story; import com.example.spot.story.domain.enums.StoryCategory; -import com.example.spot.story.domain.aggregate.StoryImage; +import com.example.spot.story.domain.association.StoryImage; import lombok.*; import java.time.LocalDateTime; import java.util.List; @Getter -public class StudyPostResDTO { +public class StoryResDTO { @Getter @RequiredArgsConstructor(access = AccessLevel.PRIVATE) diff --git a/src/main/java/com/example/spot/study/presentation/dto/response/StudyPostResponseDTO.java b/src/main/java/com/example/spot/story/web/dto/response/StoryResponseDTO.java similarity index 70% rename from src/main/java/com/example/spot/study/presentation/dto/response/StudyPostResponseDTO.java rename to src/main/java/com/example/spot/story/web/dto/response/StoryResponseDTO.java index a2ad28f2..a083cf47 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/response/StudyPostResponseDTO.java +++ b/src/main/java/com/example/spot/story/web/dto/response/StoryResponseDTO.java @@ -1,4 +1,4 @@ -package com.example.spot.study.presentation.dto.response; +package com.example.spot.story.web.dto.response; import lombok.AllArgsConstructor; @@ -10,7 +10,7 @@ @Getter @NoArgsConstructor @AllArgsConstructor -public class StudyPostResponseDTO { +public class StoryResponseDTO { private String title; private String content; } diff --git a/src/main/java/com/example/spot/study/application/MemberStudyCommandService.java b/src/main/java/com/example/spot/study/application/MemberStudyCommandService.java deleted file mode 100644 index e7c5d9e1..00000000 --- a/src/main/java/com/example/spot/study/application/MemberStudyCommandService.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.example.spot.study.application; - -import com.example.spot.member.presentation.dto.MemberResponseDTO; -import com.example.spot.study.presentation.dto.request.ScheduleRequestDTO; -import com.example.spot.study.presentation.dto.request.StudyHostWithdrawRequestDTO; -import com.example.spot.study.presentation.dto.request.StudyMemberReportDTO; -import com.example.spot.study.presentation.dto.request.StudyQuizRequestDTO; -import com.example.spot.study.presentation.dto.request.StudyVoteRequestDTO; -import com.example.spot.study.presentation.dto.response.ScheduleResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyPostResDTO; -import com.example.spot.study.presentation.dto.response.StudyQuizResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyTerminationResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyVoteResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyWithdrawalResponseDTO; -import com.example.spot.study.presentation.dto.request.ToDoListRequestDTO; -import com.example.spot.study.presentation.dto.response.ToDoListResponseDTO; -import com.example.spot.study.presentation.dto.response.ToDoListResponseDTO.ToDoListCreateResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyApplyResponseDTO; -import jakarta.validation.Valid; - -import java.time.LocalDate; - -public interface MemberStudyCommandService { - - StudyWithdrawalResponseDTO.WithdrawalDTO withdrawFromStudy(Long studyId); - StudyWithdrawalResponseDTO.WithdrawalDTO withdrawHostFromStudy(Long studyId, StudyHostWithdrawRequestDTO requestDTO); - - StudyTerminationResponseDTO.TerminationDTO terminateStudy(Long studyId, String performance); - - // 스터디 신청 수락 - StudyApplyResponseDTO acceptAndRejectStudyApply(Long memberId, Long studyId, boolean isAccept); - - StudyApplyResponseDTO acceptAndRejectStudyApplyForTest(Long memberId, Long studyId, boolean isAccept); - - // 일정 생성 - ScheduleResponseDTO.ScheduleDTO addSchedule(Long studyId, ScheduleRequestDTO.ScheduleDTO scheduleRequestDTO); - - // 일정 수정 - ScheduleResponseDTO.ScheduleDTO modSchedule(Long studyId, Long scheduleId, ScheduleRequestDTO.ScheduleDTO scheduleModDTO); - - // 스터디 퀴즈 생성 - StudyQuizResponseDTO.QuizDTO createAttendanceQuiz(Long studyId, Long scheduleId, StudyQuizRequestDTO.QuizDTO quizRequestDTO); - - // 스터디 출석 - StudyQuizResponseDTO.AttendanceDTO attendantStudy(Long studyId, Long scheduleId, StudyQuizRequestDTO.AttendanceDTO attendanceRequestDTO); - - // 스터디 퀴즈 삭제 - StudyQuizResponseDTO.QuizDTO deleteAttendanceQuiz(Long studyId, Long scheduleId, LocalDate date); - - // 스터디 투표 생성 - StudyVoteResponseDTO.VotePreviewDTO createVote(Long studyId, StudyVoteRequestDTO.VoteDTO voteDTO); - - // 스터디 투표 참여 - StudyVoteResponseDTO.VotedOptionDTO vote(Long studyId, Long voteId, StudyVoteRequestDTO.VotedOptionDTO votedOptionDTO); - - // 스터디 투표 수정 - StudyVoteResponseDTO.VotePreviewDTO updateVote(Long studyId, Long voteId, StudyVoteRequestDTO.VoteUpdateDTO voteDTO); - - // 스터디 투표 삭제 - StudyVoteResponseDTO.VotePreviewDTO deleteVote(Long studyId, Long voteId); - - // 스터디 회원 신고 - MemberResponseDTO.ReportedMemberDTO reportStudyMember(Long studyId, Long memberId, @Valid StudyMemberReportDTO studyMemberReportDTO); - - // 스터디 게시글 신고 - StudyPostResDTO.PostPreviewDTO reportStudyPost(Long studyId, Long postId); - - // 투두 리스트 생성 - ToDoListCreateResponseDTO createToDoList(Long studyId, ToDoListRequestDTO.ToDoListCreateDTO toDoListCreateDTO); - - // 투두 리스트 체크 - ToDoListResponseDTO.ToDoListUpdateResponseDTO checkToDoList(Long studyId, Long toDoListId); - - // 투두 리스트 수정 - ToDoListResponseDTO.ToDoListUpdateResponseDTO updateToDoList(Long studyId, Long toDoListId, ToDoListRequestDTO.ToDoListCreateDTO toDoListCreateDTO); - - // 투두 리스트 삭제 - ToDoListResponseDTO.ToDoListUpdateResponseDTO deleteToDoList(Long studyId, Long toDoListId); -} diff --git a/src/main/java/com/example/spot/study/application/MemberStudyCommandServiceImpl.java b/src/main/java/com/example/spot/study/application/MemberStudyCommandServiceImpl.java deleted file mode 100644 index a62438dc..00000000 --- a/src/main/java/com/example/spot/study/application/MemberStudyCommandServiceImpl.java +++ /dev/null @@ -1,1182 +0,0 @@ -package com.example.spot.study.application; - -import com.example.spot.common.api.code.status.ErrorStatus; -import com.example.spot.common.api.exception.GeneralException; -import com.example.spot.common.api.exception.handler.MemberHandler; -import com.example.spot.common.api.exception.handler.StudyHandler; -import com.example.spot.member.domain.Member; -import com.example.spot.report.domain.MemberReport; -import com.example.spot.notification.domain.Notification; -import com.example.spot.schedule.domain.Schedule; -import com.example.spot.schedule.domain.aggregate.Quiz; -import com.example.spot.schedule.domain.aggregate.QuizSubmission; -import com.example.spot.schedule.domain.repository.QuizRepository; -import com.example.spot.schedule.domain.repository.QuizSubmissionRepository; -import com.example.spot.schedule.domain.ScheduleRepository; -import com.example.spot.story.domain.Story; -import com.example.spot.story.domain.aggregate.StoryReport; -import com.example.spot.todo.domain.ToDo; -import com.example.spot.study.domain.enums.StudyApplicationStatus; -import com.example.spot.notification.domain.enums.NotifyType; -import com.example.spot.member.domain.enums.Status; -import com.example.spot.study.domain.aggregate.StudyMember; -import com.example.spot.study.domain.Study; -import com.example.spot.report.domain.MemberReportRepository; -import com.example.spot.member.domain.MemberRepository; -import com.example.spot.study.domain.repository.StudyMemberRepository; -import com.example.spot.notification.domain.NotificationRepository; -import com.example.spot.story.domain.repository.StoryReportRepository; -import com.example.spot.story.domain.StoryRepository; -import com.example.spot.study.domain.StudyRepository; -import com.example.spot.todo.domain.ToDoRepository; -import com.example.spot.study.presentation.dto.request.ScheduleRequestDTO; -import com.example.spot.study.presentation.dto.request.StudyHostWithdrawRequestDTO; -import com.example.spot.study.presentation.dto.request.StudyMemberReportDTO; -import com.example.spot.study.presentation.dto.request.StudyQuizRequestDTO; -import com.example.spot.study.presentation.dto.request.StudyVoteRequestDTO; -import com.example.spot.study.presentation.dto.response.ScheduleResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyPostResDTO; -import com.example.spot.study.presentation.dto.response.StudyQuizResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyTerminationResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyVoteResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyWithdrawalResponseDTO; -import com.example.spot.common.security.utils.SecurityUtils; -import com.example.spot.common.application.s3.S3ImageService; -import com.example.spot.member.presentation.dto.MemberResponseDTO; -import com.example.spot.study.presentation.dto.request.ToDoListRequestDTO.ToDoListCreateDTO; -import com.example.spot.study.presentation.dto.response.ToDoListResponseDTO.ToDoListCreateResponseDTO; -import com.example.spot.study.presentation.dto.response.ToDoListResponseDTO.ToDoListUpdateResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyWithdrawalResponseDTO.WithdrawalDTO; -import com.example.spot.study.presentation.dto.response.StudyApplyResponseDTO; - -import java.time.LocalDate; -import java.util.Objects; - -import com.example.spot.vote.domain.Vote; -import com.example.spot.vote.domain.aggregate.VoteOption; -import com.example.spot.vote.domain.aggregate.VoteParticipant; -import com.example.spot.vote.domain.repository.VoteOptionRepository; -import com.example.spot.vote.domain.repository.VoteParticipantRepository; -import com.example.spot.vote.domain.VoteRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.List; - - -@Service -@RequiredArgsConstructor -@Transactional -public class MemberStudyCommandServiceImpl implements MemberStudyCommandService { - - @Value("${image.post.anonymous.profile}") - private String defaultImage; - - private final MemberRepository memberRepository; - private final StudyRepository studyRepository; - private final ScheduleRepository scheduleRepository; - private final QuizRepository quizRepository; - private final VoteRepository voteRepository; - private final VoteOptionRepository voteOptionRepository; - private final MemberReportRepository memberReportRepository; - private final StoryReportRepository storyReportRepository; - - private final StudyMemberRepository studyMemberRepository; - private final QuizSubmissionRepository quizSubmissionRepository; - private final StoryRepository storyRepository; - private final VoteParticipantRepository voteParticipantRepository; - private final ToDoRepository toDoRepository; - private final NotificationRepository notificationRepository; - - // S3 Service - private final S3ImageService s3ImageService; - -/* ----------------------------- 진행중인 스터디 관련 API ------------------------------------- */ - - /** - * 진행중인 스터디에서 탈퇴하기 위한 메서드입니다. - * 스터디장은 스터디를 탈퇴할 수 없으며 스터디를 종료하고자 하는 경우 스터디 terminateStudy API를 호출해야 합니다. - * - * @param studyId 타겟 회원이 탈퇴하고자 하는 스터디의 아이디를 입력 받습니다. - * @return 탈퇴한 스터디의 아이디와 이름, 탈퇴한 회원의 아이디와 이름이 반환됩니다. - */ - public StudyWithdrawalResponseDTO.WithdrawalDTO withdrawFromStudy(Long studyId) { - - // Authorization - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - - StudyMember studyMember = studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - // 참여가 승인되지 않은 스터디는 탈퇴할 수 없음 - if (!studyMember.getStatus().equals(StudyApplicationStatus.APPROVED)) { - throw new StudyHandler(ErrorStatus._STUDY_NOT_APPROVED); - } - // 스터디장은 스터디를 탈퇴할 수 없음 - if (studyMember.getIsOwned()) { - throw new StudyHandler(ErrorStatus._STUDY_OWNER_CANNOT_WITHDRAW); - } - - studyMemberRepository.delete(studyMember); - - return StudyWithdrawalResponseDTO.WithdrawalDTO.toDTO(member, study); - } - - @Override - public WithdrawalDTO withdrawHostFromStudy(Long studyId, StudyHostWithdrawRequestDTO requestDTO) { - // Authorization - Long hostId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(hostId); - - StudyMember studyMember = studyMemberRepository.findByMemberIdAndStudyIdAndStatus(hostId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - if (!studyMember.getIsOwned()) { - throw new StudyHandler(ErrorStatus._STUDY_OWNER_ONLY_CAN_WITHDRAW); - } - - StudyMember newHostStudy = studyMemberRepository.findByMemberIdAndStudyIdAndStatus(requestDTO.getNewHostId(), studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_EXIST)); - - studyMemberRepository.delete(studyMember); - - newHostStudy.setIsOwned(true); - newHostStudy.setReason(requestDTO.getReason()); - - studyMemberRepository.save(newHostStudy); - - return StudyWithdrawalResponseDTO.WithdrawalDTO.toDTO(newHostStudy.getMember(), newHostStudy.getStudy()); - } - /** - * 운영중인 스터디를 종료하는 메서드입니다. 스터디장만 호출 가능합니다. - * - * @param studyId 종료할 스터디의 아이디를 입력 받습니다. - * @param performance 종료할 스터디의 성과를 입력 받습니다. - * @return 종료된 스터디의 아이디, 이름, 상태를 반환합니다. - */ - public StudyTerminationResponseDTO.TerminationDTO terminateStudy(Long studyId, String performance) { - - // Authorization - Long memberId = SecurityUtils.getCurrentUserId(); - memberRepository.findById(memberId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - StudyMember studyMember = studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - // 스터디장이 아니면 스터디를 종료할 수 없음 - if (studyMember.getIsOwned().equals(false)) { - throw new StudyHandler(ErrorStatus._STUDY_OWNER_ONLY_CAN_TERMINATE); - } - - // 이미 종료된 스터디는 종료할 수 없음 - if (study.getStatus().equals(Status.OFF)) { - throw new StudyHandler(ErrorStatus._STUDY_ALREADY_TERMINATED); - } - - study.terminateStudy(performance); - studyRepository.save(study); - - return StudyTerminationResponseDTO.TerminationDTO.toDTO(study); - } - - /** - * 스터디 신청을 처리합니다. isAccept가 true이면 승인, false이면 거절합니다. - * 이후 관련 알림을 생성합니다. 알림을 통해 최종 참여 승인을 해야 스터디에 참여할 수 있습니다. - * @param memberId 스터디에 신청한 회원 ID - * @param studyId 스터디 ID - * @param isAccept 승인 여부 - * @return 스터디 신청 처리 결과 및 처리 시간 - * @throws GeneralException 스터디 신청을 처리하는 회원이 스터디 소유자가 아닐 때 - * @throws GeneralException 스터디 소유자가 신청한 경우 - * @throws StudyHandler 스터디 신청자를 찾을 수 없을 때 - * @throws StudyHandler 스터디 신청이 이미 처리되었을 때 - * @throws MemberHandler 스터디 장을 찾을 수 없을 때 - */ - @Override - public StudyApplyResponseDTO acceptAndRejectStudyApply(Long memberId, Long studyId, - boolean isAccept) { - - // 신청을 처리하는 회원이 스터디 소유자인지 확인 - if (!isOwner(SecurityUtils.getCurrentUserId(), studyId)) - throw new GeneralException(ErrorStatus._ONLY_STUDY_OWNER_CAN_ACCESS_APPLICANTS); - - // 스터디 신청자 조회 - StudyMember studyMember = studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPLIED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_APPLICANT_NOT_FOUND)); - - // 스터디 소유자가 스터디 신청한 경우 - if (studyMember.getIsOwned()) - throw new GeneralException(ErrorStatus._STUDY_OWNER_CANNOT_APPLY); - - // 스터디 신청이 이미 처리되었을 때 - if (studyMember.getStatus() != StudyApplicationStatus.APPLIED) - throw new GeneralException(ErrorStatus._STUDY_APPLY_ALREADY_PROCESSED); - - // 스터디 장 조회 - Member owner = memberRepository.findById(SecurityUtils.getCurrentUserId()) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - - // 승인인 경우 - if (isAccept) { - // 스터디 참여 승인 최종 대기 - studyMember.setStatus(StudyApplicationStatus.AWAITING_SELF_APPROVAL); - - // 알림 생성 - Notification notification = Notification.builder() - .member(studyMember.getMember()) // 신청자 - .study(studyMember.getStudy()) - .notifierName(owner.getName()) // 스터디장 이름 - .type(NotifyType.STUDY_APPLY) - .isChecked(Boolean.FALSE) - .build(); - - notificationRepository.save(notification); - } - else { // 거절인 경우 - studyMember.setStatus(StudyApplicationStatus.REJECTED); - studyMemberRepository.delete(studyMember); - } - - // 스터디 신청 처리 결과 반환 - return StudyApplyResponseDTO.builder() - .status(studyMember.getStatus()) - .updatedAt(studyMember.getUpdatedAt()) - .build(); - } - - /** - * 스터디 신청을 처리합니다. isAccept가 true이면 승인, false이면 거절합니다. - * 이 메서드를 사용하면 알림 처리 없이 바로 스터디에 참여할 수 있습니다. - * @param memberId 스터디에 신청한 회원 ID - * @param studyId 스터디 ID - * @param isAccept 승인 여부 - * @return 스터디 신청 처리 결과 및 처리 시간 - * @throws GeneralException 스터디 신청을 처리하는 회원이 스터디 소유자가 아닐 때 - * @throws GeneralException 스터디 소유자가 신청한 경우 - * @throws StudyHandler 스터디 신청자를 찾을 수 없을 때 - * @throws StudyHandler 스터디 신청이 이미 처리되었을 때 - * @throws MemberHandler 스터디 장을 찾을 수 없을 때 - * - */ - @Override - public StudyApplyResponseDTO acceptAndRejectStudyApplyForTest(Long memberId, Long studyId, - boolean isAccept) { - - // 스터디 신청을 처리하는 회원이 스터디 소유자인지 확인 - if (!isOwner(SecurityUtils.getCurrentUserId(), studyId)) - throw new GeneralException(ErrorStatus._ONLY_STUDY_OWNER_CAN_ACCESS_APPLICANTS); - - // 스터디 신청자 조회 - StudyMember studyMember = studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPLIED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_APPLICANT_NOT_FOUND)); - - // 스터디 소유자가 스터디 신청한 경우 - if (studyMember.getIsOwned()) - throw new GeneralException(ErrorStatus._STUDY_OWNER_CANNOT_APPLY); - - // 스터디 신청이 이미 처리되었을 때 - if (studyMember.getStatus() != StudyApplicationStatus.APPLIED) - throw new GeneralException(ErrorStatus._STUDY_APPLY_ALREADY_PROCESSED); - - // 스터디 장 조회 - Member owner = memberRepository.findById(SecurityUtils.getCurrentUserId()) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - - // 승인인 경우 - if (isAccept) { - studyMember.setStatus(StudyApplicationStatus.APPROVED); - } - else { - studyMember.setStatus(StudyApplicationStatus.REJECTED); - studyMemberRepository.delete(studyMember); - } - - // 스터디 신청 처리 결과 반환 - return StudyApplyResponseDTO.builder() - .status(studyMember.getStatus()) - .updatedAt(studyMember.getUpdatedAt()) - .build(); - } - - /* ----------------------------- 스터디 일정 관련 API ------------------------------------- */ - - /** - * 스터디 일정을 추가하는 메서드입니다. - * @param studyId 타겟 스터디의 아이디를 입력 받습니다. - * @param scheduleRequestDTO 생성할 일정의 제목, 위치, 시작 일시, 종료 일시, 종일 진행 여부, 반복 여부를 입력 받습니다. - * @return 스터디 아이디와 생성된 일정의 아이디, 제목을 반환합니다. - */ - @Override - public ScheduleResponseDTO.ScheduleDTO addSchedule(Long studyId, ScheduleRequestDTO.ScheduleDTO scheduleRequestDTO) { - - //=== Exception ===// - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - //=== Feature ===// - - // Period 기반 시작일 종료일 제한 - checkStartAndFinishDate(scheduleRequestDTO); - - Schedule schedule = Schedule.builder() - .study(study) - .member(member) - .title(scheduleRequestDTO.getTitle()) - .location(scheduleRequestDTO.getLocation()) - .startedAt(scheduleRequestDTO.getStartedAt()) - .finishedAt(scheduleRequestDTO.getFinishedAt()) - .isAllDay(scheduleRequestDTO.getIsAllDay()) - .schedulePeriod(scheduleRequestDTO.getSchedulePeriod()) - .build(); - - // 알림 생성 - - // 스터디에 참여중인 회원들에게 알림 전송 위해 회원 조회 - List members = studyMemberRepository.findAllByStudyIdAndStatus(studyId, StudyApplicationStatus.APPROVED).stream() - .map(StudyMember::getMember) - .toList(); - - if (members.isEmpty()) - throw new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND); - - members.forEach(studyMember -> { - Notification notification = Notification.builder() - .member(studyMember) - .study(study) - .notifierName(member.getName()) // 일정 생성자 이름 - .type(NotifyType.SCHEDULE_UPDATE) - .isChecked(Boolean.FALSE) - .build(); - notificationRepository.save(notification); - }); - - scheduleRepository.save(schedule); - study.addSchedule(schedule); - member.addSchedule(schedule); - - return ScheduleResponseDTO.ScheduleDTO.toDTO(schedule); - } - - /** - * 스터디 일정을 변경하는 메서드입니다. - * @param studyId 타겟 스터디의 아이디를 입력 받습니다. - * @param scheduleId 변경할 일정의 아이디를 입력 받습니다. - * @param scheduleModDTO 변경된 일정의 제목, 위치, 시작 일시, 종료 일시, 종일 진행 여부, 반복 여부를 입력 받습니다. - * @return 스터디 아이디와 변경된 일정의 아이디, 제목을 반환합니다. - */ - @Override - public ScheduleResponseDTO.ScheduleDTO modSchedule(Long studyId, Long scheduleId, ScheduleRequestDTO.ScheduleDTO scheduleModDTO) { - - //=== Exception ===// - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - Schedule schedule = scheduleRepository.findById(scheduleId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND)); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - // 로그인한 회원이 일정 생성자인지 확인 - scheduleRepository.findByIdAndMemberId(scheduleId, memberId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._SCHEDULE_MOD_INVALID)); - - // 해당 스터디의 일정인지 확인 - scheduleRepository.findByIdAndStudyId(scheduleId, studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND)); - - //=== Feature ===// - - // Period 기반 시작일 종료일 제한 - checkStartAndFinishDate(scheduleModDTO); - - schedule.modSchedule(scheduleModDTO); - schedule = scheduleRepository.save(schedule); - - study.updateSchedule(schedule); - member.updateSchedule(schedule); - - return ScheduleResponseDTO.ScheduleDTO.toDTO(schedule); - } - - private static void checkStartAndFinishDate(ScheduleRequestDTO.ScheduleDTO scheduleRequestDTO) { - LocalDate startDate = scheduleRequestDTO.getStartedAt().toLocalDate(); - LocalDate finishDate = scheduleRequestDTO.getFinishedAt().toLocalDate(); - System.out.println(startDate); - System.out.println(finishDate); - switch (scheduleRequestDTO.getSchedulePeriod()) { - case DAILY : - // 시작일과 종료일이 일치해야 함 - if (finishDate.equals(startDate.plusDays(1)) || - finishDate.isAfter(startDate.plusDays(1))) { - throw new StudyHandler(ErrorStatus._STUDY_SCHEDULE_WRONG_FORMAT); - } - case WEEKLY : - // 시작일과 종료일이 일주일 이상 차이나지 않아야 함 - if (finishDate.equals(startDate.plusWeeks(1)) || - finishDate.isAfter(startDate.plusWeeks(1))) { - throw new StudyHandler(ErrorStatus._STUDY_SCHEDULE_WRONG_FORMAT); - } - case BIWEEKLY : - // 시작일과 종료일이 2주 이상 차이나지 않아야 함 - if (finishDate.equals(startDate.plusWeeks(2)) || - finishDate.isAfter(startDate.plusWeeks(2))) { - throw new StudyHandler(ErrorStatus._STUDY_SCHEDULE_WRONG_FORMAT); - } - case MONTHLY : - // 시작일과 종료일이 한 달 이상 차이나지 않아야 함 - if (finishDate.equals(startDate.plusMonths(1)) || - finishDate.isAfter(startDate.plusMonths(1))) { - throw new StudyHandler(ErrorStatus._STUDY_SCHEDULE_WRONG_FORMAT); - } - } - } - - - /* ----------------------------- 스터디 출석 관련 API ------------------------------------- */ - - /** - * 출석 퀴즈를 생성하는 메서드입니다. - * @param studyId 타겟 스터디의 아이디를 입력 받습니다. - * @param scheduleId 타겟 일정의 아이디를 입력 받습니다. - * @param quizRequestDTO 출석 퀴즈에 담길 질문과 정답을 입력 받습니다. - * @return 생성된 퀴즈의 아이디와 질문이 반환됩니다. - */ - @Override - public StudyQuizResponseDTO.QuizDTO createAttendanceQuiz(Long studyId, Long scheduleId, StudyQuizRequestDTO.QuizDTO quizRequestDTO) { - - //=== Exception ===// - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - Schedule schedule = scheduleRepository.findById(scheduleId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND)); - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - - // 해당 스터디에서 생성된 일정인지 확인 - if (!schedule.getStudy().equals(study)) { - throw new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND); - } - - // 로그인한 회원이 스터디장인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndIsOwned(memberId, studyId, Boolean.TRUE) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_QUIZ_CREATION_INVALID)); - - // 요청한 날짜에 이미 출석 퀴즈가 생성되었는지 확인 - LocalDateTime startOfDay = quizRequestDTO.getCreatedAt().withHour(0).withMinute(0).withSecond(0).withNano(0); - LocalDateTime endOfDay = quizRequestDTO.getCreatedAt().withHour(23).withMinute(59).withSecond(59).withNano(999_999_000); - List todayQuizzes = quizRepository.findAllByScheduleIdAndCreatedAtBetween(scheduleId, startOfDay, endOfDay); - if (!todayQuizzes.isEmpty()) { - throw new StudyHandler(ErrorStatus._STUDY_QUIZ_ALREADY_EXIST); - } - - //=== Feature ===// - Quiz quiz = Quiz.builder() - .schedule(schedule) - .member(member) - .question(quizRequestDTO.getQuestion()) - .answer(quizRequestDTO.getAnswer()) - .createdAt(quizRequestDTO.getCreatedAt()) - .build(); - - quiz = quizRepository.save(quiz); - schedule.addQuiz(quiz); - member.addQuiz(quiz); - - return StudyQuizResponseDTO.QuizDTO.toDTO(quiz); - } - - /** - * 출석 체크에 사용되는 메서드입니다. - * 메서드 내에서 퀴즈의 제한 시간과 시도 횟수를 확인하며, 조건을 충족하는 경우 회원 출석 정보를 저장합니다. - * @param studyId 타겟 스터디의 아이디를 입력 받습니다. - * @param scheduleId 출석을 체크할 일정을 입력 받습니다. - * @param attendanceRequestDTO 퀴즈에 대한 회원의 답변을 입력 받습니다. - * @return 회원 아이디, 퀴즈 아이디, 출석 아이디, 정답 여부, 시도 횟수, 출석 정보 생성 시각을 반환합니다. - */ - @Override - public StudyQuizResponseDTO.AttendanceDTO attendantStudy(Long studyId, Long scheduleId, StudyQuizRequestDTO.AttendanceDTO attendanceRequestDTO) { - - //=== Exception ===// - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - - // 요청한 날짜에 생성된 출석 퀴즈 조회 - LocalDateTime startOfDay = attendanceRequestDTO.getDateTime().withHour(0).withMinute(0).withSecond(0).withNano(0); - LocalDateTime endOfDay = attendanceRequestDTO.getDateTime().withHour(23).withMinute(59).withSecond(59).withNano(999_999_000); - List quizzes = quizRepository.findAllByScheduleIdAndCreatedAtBetween(scheduleId, startOfDay, endOfDay); - if (quizzes.isEmpty()) { - throw new StudyHandler(ErrorStatus._STUDY_QUIZ_NOT_FOUND); - } - Quiz quiz = quizzes.get(0); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(member.getId(), study.getId(), StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - // 퀴즈 제한시간 확인 - if (attendanceRequestDTO.getDateTime().isAfter(quiz.getCreatedAt().plusMinutes(5))) { - throw new StudyHandler(ErrorStatus._STUDY_QUIZ_NOT_VALID); - } - - // 이미 출석이 완료되었거나 시도 횟수를 초과하였는지 확인 - List attendanceList = quizSubmissionRepository.findByQuizIdAndMemberId(quiz.getId(), member.getId()); - int try_num = 0; - for (QuizSubmission attendance : attendanceList) { - if (attendance.getIsCorrect()) - throw new StudyHandler(ErrorStatus._STUDY_ATTENDANCE_ALREADY_EXIST); - else - try_num++; - } - if (try_num >= 3) { - throw new StudyHandler(ErrorStatus._STUDY_ATTENDANCE_ATTEMPT_LIMIT_EXCEEDED); - } - - //=== Feature ===// - Boolean isCorrect; - if (attendanceRequestDTO.getAnswer().equals(quiz.getAnswer())) { - isCorrect = Boolean.TRUE; - } else { - isCorrect = Boolean.FALSE; - } - - QuizSubmission quizSubmission = new QuizSubmission(isCorrect); - member.addMemberAttendance(quizSubmission); - quiz.addMemberAttendance(quizSubmission); - quizSubmission = quizSubmissionRepository.save(quizSubmission); - - return StudyQuizResponseDTO.AttendanceDTO.toDTO(quizSubmission, try_num+1); - } - - /** - * 출석 퀴즈를 삭제하는 메서드입니다. - * - * @param studyId 타겟 스터디의 아이디를 입력 받습니다. - * @param scheduleId 스터디 일정의 아이디를 입력 받습니다. - * @param date 출석 퀴즈가 생성된 날짜를 입력 받습니다. - * @return 삭제된 퀴즈의 아이디와 질문을 반환합니다. - */ - @Override - public StudyQuizResponseDTO.QuizDTO deleteAttendanceQuiz(Long studyId, Long scheduleId, LocalDate date) { - - //=== Exception ===// - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - - // 요청한 날짜에 생성된 출석 퀴즈 조회 - LocalDateTime startOfDay = date.atStartOfDay(); - LocalDateTime endOfDay = date.atStartOfDay().plusDays(1); - List todayQuizzes = quizRepository.findAllByScheduleIdAndCreatedAtBetween(scheduleId, startOfDay, endOfDay); - if (todayQuizzes.isEmpty()) { - throw new StudyHandler(ErrorStatus._STUDY_QUIZ_NOT_FOUND); - } - Quiz quiz = todayQuizzes.get(0); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(member.getId(), study.getId(), StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - // 로그인한 회원이 스터디장인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndIsOwned(memberId, studyId, Boolean.TRUE) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_QUIZ_DELETION_INVALID)); - - //=== Feature ===// - quizSubmissionRepository.findByQuizId(quiz.getId()) - .forEach(memberAttendance -> { - quiz.deleteMemberAttendance(memberAttendance); - quizSubmissionRepository.delete(memberAttendance); - }); - quizRepository.delete(quiz); - - return StudyQuizResponseDTO.QuizDTO.toDTO(quiz); - } - - -/* ----------------------------- 스터디 투표 관련 API ------------------------------------- */ - - /** - * 스터디 투표를 생성하는 메서드입니다. - * @param studyId 타겟 스터디의 아이디를 입력 받습니다. - * @param voteDTO 생성할 투표의 제목, 항목 목록, 중복 선택 가능 여부, 종료 일시를 입력 받습니다. - * @return 생성된 투표의 아이디와 제목을 반환합니다. - */ - @Override - public StudyVoteResponseDTO.VotePreviewDTO createVote(Long studyId, StudyVoteRequestDTO.VoteDTO voteDTO) { - - //=== Exception ===// - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member loginMember = memberRepository.findById(memberId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - //=== Feature ===// - Vote vote = Vote.builder() - .study(study) - .member(loginMember) - .title(voteDTO.getTitle()) - .isMultipleChoice(voteDTO.getIsMultipleChoice()) - .finishedAt(voteDTO.getFinishedAt()) - .build(); - - // Vote 저장 - vote = voteRepository.save(vote); - // Option 저장 - vote = createOption(vote, voteDTO); - // 연관관계 매핑 - loginMember.addVote(vote); - study.addVote(vote); - - return StudyVoteResponseDTO.VotePreviewDTO.toDTO(vote); - } - - /** - * 스터디 투표의 항목을 생성하는 메서드입니다. - * createVote 메서드 내부에서 사용되는 메서드입니다. - * @param vote 항목을 생성할 타겟 투표를 입력 받습니다. - * @param voteDTO 생성할 투표의 제목, 항목 목록, 중복 선택 가능 여부, 종료 일시를 입력 받습니다. - * @return 투표 객체를 반환합니다. - */ - private Vote createOption(Vote vote, StudyVoteRequestDTO.VoteDTO voteDTO) { - voteDTO.getOptions() - .forEach(stringOption -> { - VoteOption voteOption = VoteOption.builder() - .vote(vote) - .content(stringOption) - .build(); - voteOption = voteOptionRepository.save(voteOption); - vote.addOption(voteOption); - }); - return voteRepository.save(vote); - } - - /** - * 특정 항목에 투표하기 위한 메서드입니다. - * @param studyId 타겟 스터디의 아이디를 입력 받습니다. - * @param voteId 타겟 투표의 아이디를 입력 받습니다. - * @param votedOptionDTO 회원이 투표한 항목의 아이디 목록을 입력 받습니다. - * @return 투표 아이디, 회원 아이디, 투표한 항목 목록을 반환합니다. - */ - @Override - public StudyVoteResponseDTO.VotedOptionDTO vote(Long studyId, Long voteId, StudyVoteRequestDTO.VotedOptionDTO votedOptionDTO) { - - //=== Exception ===// - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member loginMember = memberRepository.findById(memberId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - Vote vote = voteRepository.findById(voteId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); - voteRepository.findByIdAndStudyId(voteId, studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - // 중복 선택이 허용되지 않는 투표는 여러 개의 option을 선택할 수 없음 - if (!vote.getIsMultipleChoice() && votedOptionDTO.getOptionIdList().size() > 1) { - throw new StudyHandler(ErrorStatus._STUDY_VOTE_MULTIPLE_CHOICE_NOT_VALID); - } - - // 한 번 참여한 투표는 다시 참여할 수 없음 - voteOptionRepository.findAllByVoteId(voteId) - .forEach(option -> { - if (voteParticipantRepository.existsByMemberIdAndVoteOptionId(loginMember.getId(), option.getId())) { - throw new StudyHandler(ErrorStatus._STUDY_VOTE_RE_PARTICIPATION_INVALID); - } - }); - - //=== Feature ===// - List voteParticipants = votedOptionDTO.getOptionIdList().stream() - .map(optionId -> { - VoteOption votedVoteOption = voteOptionRepository.findById(optionId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_OPTION_NOT_FOUND)); - voteOptionRepository.findByIdAndVoteId(optionId, voteId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_OPTION_NOT_FOUND)); - - VoteParticipant voteParticipant = VoteParticipant.builder() - .member(loginMember) - .voteOption(votedVoteOption) - .build(); - - voteParticipant = voteParticipantRepository.save(voteParticipant); - loginMember.addMemberVote(voteParticipant); - votedVoteOption.addMemberVote(voteParticipant); - - return voteParticipant; - }) - .toList(); - - return StudyVoteResponseDTO.VotedOptionDTO.toDTO(vote, loginMember, voteParticipants); - } - - /** - * 투표를 편집하는 메서드입니다. - * @param studyId 타겟 스터디의 아이디를 입력 받습니다. - * @param voteId 편집할 투표의 아이디를 입력 받습니다. - * @param voteDTO 편집된 투표의 제목, 항목 목록, 복수 선택 가능 여부, 종료 일시를 입력 받습니다. - * @return 편집된 투표의 아이디와 제목을 반환합니다. - */ - @Override - public StudyVoteResponseDTO.VotePreviewDTO updateVote(Long studyId, Long voteId, StudyVoteRequestDTO.VoteUpdateDTO voteDTO) { - - //=== Exception ===// - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member loginMember = memberRepository.findById(memberId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - Vote vote = voteRepository.findById(voteId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - // 로그인한 회원이 투표 생성자인지 확인 - if (!loginMember.equals(vote.getMember())) { - throw new StudyHandler(ErrorStatus._STUDY_VOTE_CREATOR_NOT_AUTHORIZED); - } - - // 한 명이라도 투표에 참여했으면 투표 편집 불가 - voteOptionRepository.findAllByVoteId(voteId) - .forEach(option -> { - if (voteParticipantRepository.existsByVoteOptionId(option.getId())) { - throw new StudyHandler(ErrorStatus._STUDY_VOTE_IS_IN_PROGRESS); - } - }); - - //=== Feature ===// - for (StudyVoteRequestDTO.OptionDTO optionDTO : voteDTO.getOptions()) { - VoteOption voteOption = voteOptionRepository.findById(optionDTO.getOptionId()) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_OPTION_NOT_FOUND)); - voteOption.setContent(optionDTO.getContent()); - voteOption = voteOptionRepository.save(voteOption); - vote.updateOption(voteOption); - } - - vote.updateVote(voteDTO.getTitle(), voteDTO.getIsMultipleChoice(), voteDTO.getFinishedAt()); - vote = voteRepository.save(vote); - loginMember.updateVote(vote); - study.updateVote(vote); - - return StudyVoteResponseDTO.VotePreviewDTO.toDTO(vote); - } - - /** - * 투표를 삭제하는 메서드입니다. - * @param studyId 타겟 스터디의 아이디를 입력 받습니다. - * @param voteId 삭제할 투표의 아이디를 입력 받습니다. - * @return 삭제된 투표의 아이디와 제목을 반환합니다. - */ - @Override - public StudyVoteResponseDTO.VotePreviewDTO deleteVote(Long studyId, Long voteId) { - - //=== Exception ===// - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member loginMember = memberRepository.findById(memberId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - Vote vote = voteRepository.findById(voteId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); - voteRepository.findByIdAndStudyId(voteId, studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - // 로그인한 회원이 투표 생성자인지 확인 - if (!loginMember.equals(vote.getMember())) { - throw new StudyHandler(ErrorStatus._STUDY_VOTE_CREATOR_NOT_AUTHORIZED); - } - - //=== Feature ===// - deleteOptions(voteId); - loginMember.deleteVote(vote); - study.deleteVote(vote); - voteRepository.delete(vote); - - return StudyVoteResponseDTO.VotePreviewDTO.toDTO(vote); - } - - /** - * 모든 투표 항목을 삭제하는 메서드입니다. - * deleteVote 메서드 내부에서 호출되는 메서드입니다. - * @param voteId 항목을 삭제할 타겟 투표의 아이디를 입력 받습니다. - */ - private void deleteOptions(Long voteId) { - List voteOptions = voteOptionRepository.findAllByVoteId(voteId); - voteOptions.forEach(option -> { - option.deleteAllMemberVotes(); - voteParticipantRepository.deleteAll(voteParticipantRepository.findAllByVoteOptionId(option.getId())); - voteOptionRepository.delete(option); - }); - } - - /** - * 회원이 스터디 장인지 확인합니다. - * @param memberId 확인 하려는 회원 ID - * @param studyId 확인 하려는 스터디 ID - * @return 스터디 장 여부를 반환합니다. - */ - private boolean isOwner(Long memberId, Long studyId) { - return studyMemberRepository.findByMemberIdAndStudyIdAndIsOwned(memberId, studyId, true).isPresent(); - } - - /** - * 회원이 스터디 구성원인지 확인합니다. - * @param memberId 확인 하려는 회원 ID - * @param studyId 확인 하려는 스터디 ID - * @return 스터디 참여 여부를 반환합니다. - */ - private boolean isMember(Long memberId, Long studyId) { - return studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED).isPresent(); - } - - /** - * 스터디원을 신고하고 신고 내역을 저장하는 메서드입니다. - * @param studyId 타겟 스터디의 아이디를 입력 받습니다. - * @param memberId 신고할 회원의 아이디를 입력 받습니다. - * @param studyMemberReportDTO 신고 사유를 입력 받습니다. - * @return 신고를 당한 회원의 아이디와 이름을 반환합니다. - */ - @Override - public MemberResponseDTO.ReportedMemberDTO reportStudyMember(Long studyId, Long memberId, StudyMemberReportDTO studyMemberReportDTO) { - - //=== Exception ===// - Long reporterId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(reporterId); - - Member reporter = memberRepository.findById(reporterId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(reporterId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - // 신고당한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - // 자기 자신을 신고할 수 없음 - if (reporterId.equals(memberId)) { - throw new StudyHandler(ErrorStatus._STUDY_MEMBER_REPORT_INVALID); - } - - - //=== Feature ===// - MemberReport memberReport = MemberReport.builder() - .content(studyMemberReportDTO.getContent()) - .member(member) - .build(); - - memberReport = memberReportRepository.save(memberReport); - member.addMemberReport(memberReport); - - return MemberResponseDTO.ReportedMemberDTO.toDTO(member); - } - - /** - * 스터디 게시글을 신고하고 신고 내역을 저장하는 메서드입니다. - * @param studyId 타겟 스터디의 아이디를 입력합니다. - * @param postId 신고할 게시글의 아이디를 입력합니다. - * @return 신고를 당한 스터디 게시글의 아이디와 제목을 반환합니다. - */ - @Override - public StudyPostResDTO.PostPreviewDTO reportStudyPost(Long studyId, Long postId) { - - //=== Exception ===// - Long reporterId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(reporterId); - - memberRepository.findById(reporterId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - Story story = storyRepository.findById(postId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(reporterId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - // 해당 스터디의 게시글인지 확인 - storyRepository.findByIdAndStudyId(postId, studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_POST_NOT_FOUND)); - - //=== Feature ===// - StoryReport storyReport = StoryReport.builder() - .story(story) - .build(); - - storyReport = storyReportRepository.save(storyReport); - story.addStudyPostReport(storyReport); - - return StudyPostResDTO.PostPreviewDTO.toDTO(story); - } - -/* ----------------------------- 스터디 To-Do List 관련 API ------------------------------------- */ - - /** - * To-Do List를 생성합니다. - * @param studyId 생성할 To-Do List가 속한 스터디 ID - * @param toDoListCreateDTO 생성할 To-Do List 정보 - * @return 생성된 To-Do List 정보 - * @throws StudyHandler 스터디를 찾을 수 없을 때 - * @throws StudyHandler To-Do List를 생성하는 회원이 스터디 회원이 아닐 때 - * @throws StudyHandler 해당 회원을 찾을 수 없을 때 - */ - @Override - public ToDoListCreateResponseDTO createToDoList(Long studyId, - ToDoListCreateDTO toDoListCreateDTO) { - - // 스터디 조회 - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - - // To-Do List를 생성하는 회원 ID 조회 - Long currentUserId = SecurityUtils.getCurrentUserId(); - - // To-Do List를 생성하는 회원이 스터디 회원인지 확인 - if (!isMember(currentUserId, studyId)) - throw new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND); - - // 회원 조회 - Member member = memberRepository.findById(currentUserId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - - // To-Do List 생성 - ToDo toDo = ToDo.builder() - .study(study) - .member(member) - .date(toDoListCreateDTO.getDate()) - .isDone(false) - .content(toDoListCreateDTO.getContent()) - .build(); - - // To-Do List 저장 - toDo.setToDoList(); - toDoRepository.save(toDo); - - // To-Do List 생성 DTO 반환 - return ToDoListCreateResponseDTO.builder() - .id(toDo.getId()) - .content(toDo.getContent()) - .createdAt(toDo.getCreatedAt()) - .build(); - } - - // studyId가 필요할까? - - /** - * To-Do List에 작성한 할 일의 체크 상태를 변경 합니다. 체크 상태를 변경 하면 해당 스터디에 참여하고 있는 모든 회원에게 알림이 전송됩니다. - * @param studyId 스터디 ID - * @param toDoListId 변경할 To-Do List ID - * @return To-Do List 변경 여부와 변경 시간 - * @throws StudyHandler To-Do List를 찾을 수 없을 때 - * @throws StudyHandler To-Do List가 스터디에 속하지 않을 때 - * @throws StudyHandler To-Do List를 변경하는 회원이 스터디 회원이 아닐 때 - * @throws StudyHandler 알림 생성 할 스터디 회원을 찾을 수 없을 때 - */ - @Override - public ToDoListUpdateResponseDTO checkToDoList(Long studyId, Long toDoListId) { - - // To-Do List 조회 - ToDo toDo = toDoRepository.findById(toDoListId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_TODO_NOT_FOUND)); - - // To-Do List가 속한 스터디가 아니면 예외 처리 - if (!Objects.equals(toDo.getStudy().getId(), studyId)) - throw new StudyHandler(ErrorStatus._STUDY_TODO_IS_NOT_BELONG_TO_STUDY); - - // To-Do List를 변경하는 회원이 스터디 회원이 아니면 예외 처리 - Long currentUserId = SecurityUtils.getCurrentUserId(); - if (!toDo.getMember().getId().equals(currentUserId)) - throw new StudyHandler(ErrorStatus._STUDY_TODO_NOT_AUTHORIZED); - - // To-Do List 체크 상태 변경 - toDo.check(); - - // 스터디 회원의 To-Do List 중 하나가 완료 되면, 해당 스터디의 모든 회원에게 알림 전송 - if (toDo.isDone()){ - List members = studyMemberRepository.findAllByStudyIdAndStatus(studyId, StudyApplicationStatus.APPROVED).stream() - .map(StudyMember::getMember) - .toList(); - - // 알림을 생성할 회원이 없으면 알림 생성하지 않음 - if (members.isEmpty()){ - return ToDoListUpdateResponseDTO.builder() - .id(toDo.getId()) - .isDone(toDo.isDone()) - .updatedAt(toDo.getUpdatedAt()) - .build(); - } - - // 알림 생성 - members.forEach(studyMember -> { - Notification notification = Notification.builder() - .member(studyMember) - .notifierName(toDo.getMember().getName()) // To-Do 완료한 회원 이름 - .study(toDo.getStudy()) - .type(NotifyType.TO_DO_UPDATE) - .isChecked(Boolean.FALSE) - .build(); - notificationRepository.save(notification); - }); - } - - // To-Do List 저장 - toDoRepository.save(toDo); - - // To-Do List 변경 DTO 반환 - return ToDoListUpdateResponseDTO.builder() - .id(toDo.getId()) - .isDone(toDo.isDone()) - .updatedAt(toDo.getUpdatedAt()) - .build(); - } - - - /** - * To-Do List 내용을 수정합니다. - * @param studyId 수정할 To-Do List가 속한 스터디 ID - * @param toDoListId 수정할 To-Do List ID - * @param toDoListCreateDTO 수정할 To-Do List 정보 - * @return 수정된 To-Do List 정보 - * @throws StudyHandler To-Do List를 찾을 수 없을 때 - * @throws StudyHandler To-Do List가 스터디에 속하지 않을 때 - * @throws StudyHandler To-Do List를 수정하는 회원이 스터디 회원이 아닐 때 - */ - @Override - public ToDoListUpdateResponseDTO updateToDoList(Long studyId, Long toDoListId, - ToDoListCreateDTO toDoListCreateDTO) { - - // To-Do List 조회 - ToDo toDo = toDoRepository.findById(toDoListId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_TODO_NOT_FOUND)); - - // To-Do List가 속한 스터디가 아니면 예외 처리 - if (!Objects.equals(toDo.getStudy().getId(), studyId)) - throw new StudyHandler(ErrorStatus._STUDY_TODO_IS_NOT_BELONG_TO_STUDY); - - // To-Do List를 수정하는 회원이 스터디 회원이 아니면 예외 처리 - Long currentUserId = SecurityUtils.getCurrentUserId(); - if (!toDo.getMember().getId().equals(currentUserId)) - throw new StudyHandler(ErrorStatus._STUDY_TODO_NOT_AUTHORIZED); - - // To-Do List 수정 - toDo.update(toDoListCreateDTO.getContent(), toDoListCreateDTO.getDate()); - - // To-Do List 저장 - toDoRepository.save(toDo); - - // To-Do List 변경 DTO 반환 - return ToDoListUpdateResponseDTO.builder() - .id(toDo.getId()) - .isDone(toDo.isDone()) - .updatedAt(toDo.getUpdatedAt()) - .build(); - } - - /** - * To-Do List를 삭제합니다. - * @param studyId 삭제할 To-Do List가 속한 스터디 ID - * @param toDoListId 삭제할 To-Do List ID - * @return 삭제된 To-Do List 정보 - * @throws StudyHandler To-Do List를 찾을 수 없을 때 - * @throws StudyHandler To-Do List가 스터디에 속하지 않을 때 - * @throws StudyHandler To-Do List를 삭제하는 회원이 스터디 회원이 아닐 때 - */ - @Override - public ToDoListUpdateResponseDTO deleteToDoList(Long studyId, Long toDoListId) { - - // 로그인 중인 회원 ID 조회 - Long currentUserId = SecurityUtils.getCurrentUserId(); - - // To-Do List를 삭제하는 회원이 스터디 회원인지 확인 - if (!isMember(currentUserId, studyId)) - throw new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND); - - // To-Do List 조회 - ToDo toDo = toDoRepository.findById(toDoListId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_TODO_NOT_FOUND)); - - // To-Do List가 속한 스터디가 아니면 예외 처리 - if (!Objects.equals(toDo.getStudy().getId(), studyId)) - throw new StudyHandler(ErrorStatus._STUDY_TODO_IS_NOT_BELONG_TO_STUDY); - - // To-Do List를 삭제하는 회원의 ID와 To-Do List를 생성한 회원의 ID가 다르면 예외 처리 - if (!toDo.getMember().getId().equals(currentUserId)) - throw new StudyHandler(ErrorStatus._STUDY_TODO_NOT_AUTHORIZED); - - // To-Do List 삭제 - toDoRepository.deleteById(toDoListId); - - // To-Do List 삭제 DTO 반환 - return ToDoListUpdateResponseDTO.builder() - .id(toDo.getId()) - .isDone(toDo.isDone()) - .updatedAt(toDo.getUpdatedAt()) - .build(); - } - -} diff --git a/src/main/java/com/example/spot/study/application/MemberStudyQueryService.java b/src/main/java/com/example/spot/study/application/MemberStudyQueryService.java deleted file mode 100644 index fb9a19ba..00000000 --- a/src/main/java/com/example/spot/study/application/MemberStudyQueryService.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.example.spot.study.application; - -import com.example.spot.study.presentation.dto.response.ToDoListResponseDTO; -import com.example.spot.study.presentation.dto.response.ScheduleResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyImageResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyMemberResDTO; -import com.example.spot.study.presentation.dto.response.StudyQuizResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyVoteResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyPostResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyScheduleResponseDTO; - - -import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO.StudyApplicantDTO; -import java.time.LocalDate; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; - -public interface MemberStudyQueryService { - - ScheduleResponseDTO.MonthlyScheduleListDTO getMonthlySchedules(Long studyId, int year, int month); - - ScheduleResponseDTO.MonthlyScheduleDTO getSchedule(Long studyId, Long scheduleId); - - // 스터디 공지 게시글 불러오기 - StudyPostResponseDTO findStudyAnnouncementPost(Long studyId); - - // 스터디 다가오는 모임 일정 불러오기 - StudyScheduleResponseDTO findStudySchedule(Long studyId, Pageable pageable); - - // 참여하는 회원 목록 불러오기 - StudyMemberResponseDTO findStudyMembers(Long studyId); - - // 스터디 별 신청 회원 목록 조회하기 - StudyMemberResponseDTO findStudyApplicants(Long studyId); - - // 스터디 호스트 조회하기 - StudyMemberResDTO.StudyHostDTO getStudyHost(Long studyId); - - // 스터디 신청 정보 가져오기 - StudyMemberResponseDTO.StudyApplyMemberDTO findStudyApplication(Long studyId, Long memberId); - - // 스터디 신청 여부 확인 - StudyApplicantDTO isApplied(Long studyId); - - // 금일 회원 출석 여부 불러오기 - StudyQuizResponseDTO.AttendanceListDTO getAllAttendances(Long studyId, Long scheduleId, LocalDate date); - - // 스터디 출석퀴즈 조회 - StudyQuizResponseDTO.QuizDTO getAttendanceQuiz(Long studyId, Long scheduleId, LocalDate date); - - // 스터디 투표 목록 조회 - StudyVoteResponseDTO.VoteListDTO getAllVotes(Long studyId); - - // 스터디 투표 마감 여부 조회 - Boolean getIsCompleted(Long voteId); - - // 스터디 투표(진행중) 조회 - StudyVoteResponseDTO.VoteDTO getVoteInProgress(Long studyId, Long voteId); - - // 스터디 투표(마감) 조회 - StudyVoteResponseDTO.CompletedVoteDTO getVoteInCompletion(Long studyId, Long voteId); - - // 스터디 투표 현황 조회 - StudyVoteResponseDTO.CompletedVoteDetailDTO getCompletedVoteDetail(Long studyId, Long voteId); - - // 스터디 이미지 목록 조회 - StudyImageResponseDTO.ImageListDTO getAllStudyImages(Long studyId, PageRequest pageRequest); - - // 내 투두 리스트 조회 - ToDoListResponseDTO.ToDoListSearchResponseDTO getToDoList(Long studyId, LocalDate date, PageRequest pageRequest); - - // 스터디 원 투두 리스트 조회 - ToDoListResponseDTO.ToDoListSearchResponseDTO getMemberToDoList(Long studyId, Long memberId, LocalDate date, PageRequest pageRequest); - -} diff --git a/src/main/java/com/example/spot/study/application/MemberStudyQueryServiceImpl.java b/src/main/java/com/example/spot/study/application/MemberStudyQueryServiceImpl.java deleted file mode 100644 index 3ae1e882..00000000 --- a/src/main/java/com/example/spot/study/application/MemberStudyQueryServiceImpl.java +++ /dev/null @@ -1,827 +0,0 @@ -package com.example.spot.study.application; - -import com.example.spot.common.api.code.status.ErrorStatus; -import com.example.spot.common.api.exception.handler.MemberHandler; -import com.example.spot.common.api.exception.handler.StudyHandler; -import com.example.spot.member.domain.Member; -import com.example.spot.schedule.domain.Schedule; -import com.example.spot.schedule.domain.aggregate.Quiz; -import com.example.spot.schedule.domain.aggregate.QuizSubmission; -import com.example.spot.schedule.domain.repository.QuizRepository; -import com.example.spot.schedule.domain.repository.QuizSubmissionRepository; -import com.example.spot.schedule.domain.ScheduleRepository; -import com.example.spot.story.domain.Story; -import com.example.spot.study.domain.aggregate.StudyMember; -import com.example.spot.study.domain.enums.StudyApplicationStatus; -import com.example.spot.schedule.domain.enums.SchedulePeriod; -import com.example.spot.study.domain.Study; -import com.example.spot.todo.domain.ToDo; -import com.example.spot.member.domain.MemberRepository; -import com.example.spot.study.domain.repository.StudyMemberRepository; -import com.example.spot.story.domain.StoryRepository; -import com.example.spot.study.domain.StudyRepository; -import com.example.spot.todo.domain.ToDoRepository; -import com.example.spot.study.presentation.dto.response.ScheduleResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyImageResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyMemberResDTO; -import com.example.spot.study.presentation.dto.response.StudyQuizResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyVoteResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyPostResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyScheduleResponseDTO; - -import com.example.spot.common.security.utils.SecurityUtils; -import com.example.spot.study.presentation.dto.response.ToDoListResponseDTO.ToDoListSearchResponseDTO; -import com.example.spot.study.presentation.dto.response.ToDoListResponseDTO.ToDoListSearchResponseDTO.ToDoListDTO; - -import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO.StudyApplicantDTO; -import com.example.spot.vote.domain.Vote; -import com.example.spot.vote.domain.aggregate.VoteOption; -import com.example.spot.vote.domain.repository.VoteOptionRepository; -import com.example.spot.vote.domain.repository.VoteParticipantRepository; -import com.example.spot.vote.domain.VoteRepository; -import lombok.RequiredArgsConstructor; -import com.example.spot.common.api.exception.GeneralException; -import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO.StudyApplyMemberDTO; -import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO.StudyMemberDTO; -import com.example.spot.study.presentation.dto.response.StudyScheduleResponseDTO.StudyScheduleDTO; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.*; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; - - -@Service -@Transactional(readOnly = true) -@RequiredArgsConstructor -public class MemberStudyQueryServiceImpl implements MemberStudyQueryService { - - @Value("${image.post.anonymous.profile}") - private String defaultImage; - - private final MemberRepository memberRepository; - private final StudyRepository studyRepository; - private final StoryRepository storyRepository; - private final ScheduleRepository scheduleRepository; - private final StudyMemberRepository studyMemberRepository; - private final QuizSubmissionRepository quizSubmissionRepository; - private final QuizRepository quizRepository; - private final VoteRepository voteRepository; - private final VoteOptionRepository voteOptionRepository; - private final VoteParticipantRepository voteParticipantRepository; - private final ToDoRepository toDoRepository; - - - /** - * 스터디 최근 공지사항을 1개 조회합니다. - * @param studyId 스터디 ID - * @return 제목과 내용을 반환합니다. - * @throws GeneralException 스터디 공지사항이 존재하지 않는 경우 - * @throws GeneralException 스터디 멤버가 아닌 경우 - */ - @Override - public StudyPostResponseDTO findStudyAnnouncementPost(Long studyId) { - - // 로그인한 회원이 해당 스터디 회원인지 확인 - if (!isMember(SecurityUtils.getCurrentUserId(), studyId)) - throw new GeneralException(ErrorStatus._ONLY_STUDY_MEMBER_CAN_ACCESS_ANNOUNCEMENT_POST); - - // 스터디 공지사항 조회 - Story story = storyRepository.findByStudyIdAndIsAnnouncement( - studyId, true).orElseThrow(() -> new GeneralException(ErrorStatus._STUDY_POST_NOT_FOUND)); - - // DTO로 변환하여 반환 - return StudyPostResponseDTO.builder() - .title(story.getTitle()) - .content(story.getContent()).build(); - } - - /** - * 로그인한 회원이 참여하는 특정 스터디의 다가오는 모임 목록을 페이징 조회 합니다. - * @param studyId 스터디 ID - * @param pageable 페이징 정보 - * @return 다가오는 모임 목록을 반환합니다. - * @throws GeneralException 스터디 일정이 존재하지 않는 경우 - * @throws GeneralException 스터디 멤버가 아닌 경우 - */ - @Override - public StudyScheduleResponseDTO findStudySchedule(Long studyId, Pageable pageable) { - - // 로그인한 회원이 해당 스터디 회원인지 확인 - if (!isMember(SecurityUtils.getCurrentUserId(), studyId)) - throw new GeneralException(ErrorStatus._ONLY_STUDY_MEMBER_CAN_ACCESS_SCHEDULE); - - // 스터디 일정 조회 - List schedules = scheduleRepository.findAllByStudyId(studyId, pageable); - - // 스터디 일정이 존재하지 않는 경우 - if (schedules.isEmpty()) - throw new GeneralException(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND); - - // DTO로 변환하여 반환 - List scheduleDTOS = schedules.stream().map(schedule -> StudyScheduleDTO.builder() - .title(schedule.getTitle()) - .location(schedule.getLocation()) - .startedAt(schedule.getStartedAt()) - .finishedAt(schedule.getFinishedAt()) - .build()).toList(); - - // 페이징 처리 - return new StudyScheduleResponseDTO(new PageImpl<>(scheduleDTOS, pageable, schedules.size()), scheduleDTOS, schedules.size()); - } - - /** - * 특정 스터디의 회원 목록을 전체 조회 합니다. 가입된 스터디가 아니더라도 회원 목록을 조회할 수 있습니다. - * @param studyId 스터디 ID - * @return 스터디에 참여하는 회원 목록을 반환합니다. - * @throws GeneralException 스터디 할 일이 존재하지 않는 경우 - * @throws GeneralException 스터디 멤버가 아닌 경우 - */ - @Override - public StudyMemberResponseDTO findStudyMembers(Long studyId) { -// // 스터디에 가입하지 않은 사람도 회원 목록은 볼 수 있어야 함. -// if (!isMember(SecurityUtils.getCurrentUserId(), studyId)) -// throw new GeneralException(ErrorStatus._ONLY_STUDY_MEMBER_CAN_ACCESS_MEMBERS); - - // 스터디 멤버 조회 - List memberStudies = studyMemberRepository.findAllByStudyIdAndStatus(studyId, StudyApplicationStatus.APPROVED); - - // 스터디 멤버가 존재하지 않는 경우 - if (memberStudies.isEmpty()) - throw new GeneralException(ErrorStatus._STUDY_MEMBER_NOT_FOUND); - - // DTO로 변환하여 반환 - List memberDTOS = memberStudies.stream().map(memberStudy -> StudyMemberDTO.builder() - .memberId(memberStudy.getMember().getId()) - .nickname(memberStudy.getMember().getName()) - .profileImage(memberStudy.getMember().getProfileImage()) - .build()).toList(); - // DTO로 변환하여 반환 - return new StudyMemberResponseDTO(memberDTOS); - } - - - /** - * 회원이 모집중인 스터디에 신청한 회원 목록을 불러옵니다. - * @param studyId 스터디 ID - * @return 스터디 신청자 목록을 반환합니다. - * @throws GeneralException 스터디 신청자가 존재 하지 않는 경우 - * @throws GeneralException 조회 하는 회원이 스터디 장이 아닌 경우 - */ - @Override - public StudyMemberResponseDTO findStudyApplicants(Long studyId) { - - // 로그인한 회원이 해당 스터디 장인지 확인 - if (!isOwner(SecurityUtils.getCurrentUserId(), studyId)) - throw new GeneralException(ErrorStatus._ONLY_STUDY_OWNER_CAN_ACCESS_APPLICANTS); - - // 스터디 신청자 조회 - List memberStudies = studyMemberRepository.findAllByStudyIdAndStatus(studyId, StudyApplicationStatus.APPLIED); - - // 스터디 신청자가 존재하지 않는 경우 - if (memberStudies.isEmpty()) - throw new GeneralException(ErrorStatus._STUDY_APPLICANT_NOT_FOUND); - - // DTO로 변환하여 반환 - List memberDTOS = memberStudies.stream().map(memberStudy -> StudyMemberDTO.builder() - .memberId(memberStudy.getMember().getId()) - .nickname(memberStudy.getMember().getName()) - .profileImage(memberStudy.getMember().getProfileImage()) - .build()).toList(); - - // DTO로 변환하여 반환 - return new StudyMemberResponseDTO(memberDTOS); - } - - @Override - public StudyMemberResDTO.StudyHostDTO getStudyHost(Long studyId) { - - // Authorization - Long memberId = SecurityUtils.getCurrentUserId(); - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); - - // 스터디 호스트 찾기 - StudyMember studyHost = studyMemberRepository.findByStudyIdAndIsOwned(studyId, true) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_OWNER_NOT_FOUND)); - - // 로그인한 회원이 호스트인지 확인 - if (studyHost.getMember().getId().equals(memberId)) { - return StudyMemberResDTO.StudyHostDTO.toDTO(true, member); - } else { - return StudyMemberResDTO.StudyHostDTO.toDTO(false, studyHost.getMember()); - } - } - - /** - * 스터디 신청자의 정보를 조회합니다. - * @param studyId 스터디 ID - * @param memberId 회원 ID - * @return 스터디 신청자 정보를 반환합니다. - * @throws GeneralException 스터디 신청자가 존재하지 않는 경우 - * @throws GeneralException 조회 하는 회원이 스터디 장이 아닌 경우 - * @throws GeneralException 스터디 장은 스터디에 신청할 수 없음 - */ - @Override - public StudyApplyMemberDTO findStudyApplication(Long studyId, Long memberId) { - - // 로그인한 회원이 해당 스터디 장인지 확인 - if (!isOwner(SecurityUtils.getCurrentUserId(), studyId)) - throw new GeneralException(ErrorStatus._ONLY_STUDY_OWNER_CAN_ACCESS_APPLICANTS); - - // 스터디 신청자 조회 - StudyMember studyMember = studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPLIED) - .orElseThrow(() -> new GeneralException(ErrorStatus._STUDY_APPLICANT_NOT_FOUND)); - - // 스터디 장은 스터디에 신청할 수 없음 - if (studyMember.getIsOwned()) - throw new GeneralException(ErrorStatus._STUDY_OWNER_CANNOT_APPLY); - - // DTO로 변환하여 반환 - return StudyApplyMemberDTO.builder() - .memberId(studyMember.getMember().getId()) - .studyId(studyMember.getStudy().getId()) - .introduction(studyMember.getIntroduction()) - .nickname(studyMember.getMember().getName()) - .profileImage(studyMember.getMember().getProfileImage()) - .build(); - } - - /** - * 해당 스터디 신청 여부를 조회합니다. true: 신청, false: 미신청 - * @param studyId 스터디 ID - * @return 신청 여부와 스터디 ID를 반환합니다. - * @throws GeneralException 이미 스터디 멤버인 경우 - */ - @Override - public StudyApplicantDTO isApplied(Long studyId) { - // 로그인한 회원 ID 조회 - Long currentUserId = SecurityUtils.getCurrentUserId(); - - // 이미 스터디 멤버인 경우 - if (isMember(currentUserId, studyId)) - throw new GeneralException(ErrorStatus._ALREADY_STUDY_MEMBER); - - // DTO로 변환하여 반환 - return StudyApplicantDTO.builder() - .isApplied(studyMemberRepository.existsByMemberIdAndStudyIdAndStatus(currentUserId, studyId, StudyApplicationStatus.APPLIED)) - .studyId(studyId) - .build(); - - } - - /* ----------------------------- 스터디 출석 관련 API ------------------------------------- */ - - /** - * 금일 모든 스터디 회원의 출석 정보를 불러옵니다. - * @param studyId 출석 정보를 불러올 스터디의 아이디를 입력 받습니다. - * @param scheduleId 스터디 일정의 아이디를 입력 받습니다. - * @param date - * @return 모든 스터디 회원에 대한 정보와 출석 여부를 담은 리스트를 반환합니다. - */ - @Override - public StudyQuizResponseDTO.AttendanceListDTO getAllAttendances(Long studyId, Long scheduleId, LocalDate date) { - - //=== Exception ===// - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); - studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - - // 요청한 날짜에 생성된 출석 퀴즈 조회 - LocalDateTime startOfDay = date.atStartOfDay(); - LocalDateTime endOfDay = date.atStartOfDay().plusDays(1); - List todayQuizzes = quizRepository.findAllByScheduleIdAndCreatedAtBetween(scheduleId, startOfDay, endOfDay); - if (todayQuizzes.isEmpty()) { - throw new StudyHandler(ErrorStatus._STUDY_QUIZ_NOT_FOUND); - } - Quiz quiz = todayQuizzes.get(0); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - //=== Feature ===// - List studyMembers = studyMemberRepository.findAllByStudyIdAndStatus(studyId, StudyApplicationStatus.APPROVED).stream() - .map(memberStudy -> { - List attendanceList = quizSubmissionRepository.findByQuizIdAndMemberId(quiz.getId(), memberStudy.getMember().getId()); - for (QuizSubmission attendance : attendanceList) { - // MemberAttendance에 퀴즈에 대한 정답이 저장되어 있으면 금일 출석 성공 - if (attendance.getIsCorrect()) - return StudyQuizResponseDTO.StudyMemberDTO.toDTO(memberStudy, Boolean.TRUE); - } - // 퀴즈를 풀지 않았거나 MemberAttendance에 오답만 저장되어 있으면 금일 출석 실패 - return StudyQuizResponseDTO.StudyMemberDTO.toDTO(memberStudy, Boolean.FALSE); - }) - .toList(); - - return StudyQuizResponseDTO.AttendanceListDTO.toDTO(quiz, studyMembers); - - } - - @Override - public StudyQuizResponseDTO.QuizDTO getAttendanceQuiz(Long studyId, Long scheduleId, LocalDate date) { - - // Authorization - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - Schedule schedule = scheduleRepository.findById(scheduleId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND)); - - // 해당 스터디에서 생성된 일정인지 확인 - if (!schedule.getStudy().equals(study)) { - throw new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND); - } - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - // 해당 날짜에 생성된 스터디 퀴즈 조회 - LocalDateTime startOfDay = date.atStartOfDay(); - LocalDateTime endOfDay = date.atStartOfDay().plusDays(1); - List todayQuizzes = quizRepository.findAllByScheduleIdAndCreatedAtBetween(scheduleId, startOfDay, endOfDay); - if (todayQuizzes.isEmpty()) { - throw new StudyHandler(ErrorStatus._STUDY_QUIZ_NOT_FOUND); - } - Quiz quiz = todayQuizzes.get(0); - - return StudyQuizResponseDTO.QuizDTO.toDTO(quiz); - } - - /* ----------------------------- 스터디 일정 관련 API ------------------------------------- */ - - /** - * 특정 연/월의 일정을 불러오는 메서드입니다. - * @param studyId 일정을 불러올 스터디의 아이디를 입력 받습니다. - * @param year 일정을 불러올 기준 연도를 입력 받습니다. - * @param month 일정을 불러올 달을 입력 받습니다. - * @return 스터디 아이디와 해당 스터디의 월별 일정 목록을 반환합니다. - */ - @Override - public ScheduleResponseDTO.MonthlyScheduleListDTO getMonthlySchedules(Long studyId, int year, int month) { - - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - - // 로그인한 회원이 스터디 회원인지 확인 - boolean isStudyMember; - if (studyMemberRepository.existsByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED)) { - isStudyMember = true; - } else { - isStudyMember = false; - } - - List monthlyScheduleDTOS = new ArrayList<>(); - - study.getSchedules().forEach(schedule -> { - if (schedule.getSchedulePeriod().equals(SchedulePeriod.NONE)) { - addSchedule(schedule, year, month, monthlyScheduleDTOS, isStudyMember); - } else { - addPeriodSchedules(schedule, year, month, monthlyScheduleDTOS, isStudyMember); - } - }); - - return ScheduleResponseDTO.MonthlyScheduleListDTO.toDTO(study, monthlyScheduleDTOS); - } - - /** - * 하나의 일정에 대한 상세 정보를 불러오는 메서드입니다. - * @param studyId 일정을 불러올 스터디의 아이디를 입력 받습니다. - * @param scheduleId 상세 정보를 물러올 일정의 아이디를 입력 받습니다. - * @return 일정 아이디, 제목, 위치, 시작 일시, 종료 일시, 매일 진행 여부, 주기를 반환합니다. - */ - @Override - public ScheduleResponseDTO.MonthlyScheduleDTO getSchedule(Long studyId, Long scheduleId) { - - // Exception - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - Schedule schedule = scheduleRepository.findById(scheduleId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND)); - - // 로그인한 회원이 스터디 회원인지 확인 - boolean isStudyMember; - if (studyMemberRepository.existsByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED)) { - isStudyMember = true; - } else { - isStudyMember = false; - } - - // 해당 스터디의 일정인지 확인 - scheduleRepository.findByIdAndStudyId(scheduleId, studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND)); - - return ScheduleResponseDTO.MonthlyScheduleDTO.toDTO(schedule, isStudyMember); - } - - /** - * 월별 일정 리스트에 주기가 정해져 있지 않은 일정을 추가하기 위한 메서드입니다. - * 일정의 시작일이 기준 연월과 일치하는 경우 월별 일정 리스트에 추가합니다. - * getMonthlySchedules API에서 호출되는 내부 메서드입니다. - * @param schedule 리스트에 추가할 일정 정보를 입력 받습니다. - * @param year 기준 연도를 입력 받습니다. - * @param month 기준 월을 입력 받습니다. - * @param monthlyScheduleDTOS 일정을 추가할 월별 일정 리스트를 입력 받습니다. - * @param isStudyMember 스터디 회원 여부를 입력 받습니다. - */ - private void addSchedule(Schedule schedule, int year, int month, List monthlyScheduleDTOS, boolean isStudyMember) { - if (schedule.getStartedAt().getYear() == year && schedule.getStartedAt().getMonthValue() == month) { - monthlyScheduleDTOS.add(ScheduleResponseDTO.MonthlyScheduleDTO.toDTO(schedule, isStudyMember)); - } - } - - /** - * 월별 일정 리스트에 반복적인 일정을 추가하기 위한 메서드입니다. - * 일정의 시작일이 기준 연월 내에 있는 경우에만 일정을 추가하며, 주기에 따라 하나의 일정이라도 여러 번 추가될 수 있습니다. - * 예를 들어 기준 연월이 2024년 8월이고, 2024년 8월 2일부터 시작되는 WEEKLY 일정이 있다고 가정 - * 1. 이 일정은 기준 연월 내에서 2024년 8월 2일, 8월 9일, 8월 16일, 8월 23일, 8월 30일에 시행 - * 2. 따라서 monthlyScheduleDTOS에 추가되는 일정은 총 5개 - * @param schedule 리스트에 추가할 일정 정보를 입력 받습니다. - * @param year 기준 연도를 입력 받습니다. - * @param month 기준 월을 입력 받습니다. - * @param monthlyScheduleDTOS 일정을 추가할 월별 일정 리스트를 입력 받습니다. - * @param isStudyMember 스터디 회원 여부를 입력 받습니다. - */ - private void addPeriodSchedules(Schedule schedule, int year, int month, List monthlyScheduleDTOS, boolean isStudyMember) { - - LocalDateTime startedAt = schedule.getStartedAt(); - LocalDateTime finishedAt = schedule.getFinishedAt(); - - YearMonth yearMonth = YearMonth.of(year, month); // 탐색 연월 - LocalDateTime endOfMonth = yearMonth.atEndOfMonth().atTime(23, 59, 59); // 탐색 연월의 마지막 날 - - // 일정 시작일이 탐색 연월 내에 있는 경우만 반복 - while (startedAt.isBefore(endOfMonth)) { - // 업데이트된 일정 시작일의 month가 탐색 month와 일치하면 추가 - if (startedAt.getMonthValue() == month) { - monthlyScheduleDTOS.add(ScheduleResponseDTO.MonthlyScheduleDTO.toDTOWithDate(schedule, startedAt, finishedAt, isStudyMember)); - } - - if (schedule.getSchedulePeriod().equals(SchedulePeriod.DAILY)) { - startedAt = startedAt.plusDays(1); - finishedAt = finishedAt.plusDays(1); - } else if (schedule.getSchedulePeriod().equals(SchedulePeriod.WEEKLY)) { - startedAt = startedAt.plusWeeks(1); - finishedAt = finishedAt.plusWeeks(1); - } else if (schedule.getSchedulePeriod().equals(SchedulePeriod.BIWEEKLY)) { - startedAt = startedAt.plusWeeks(2); - finishedAt = finishedAt.plusWeeks(2); - } else if (schedule.getSchedulePeriod().equals(SchedulePeriod.MONTHLY)) { - startedAt = startedAt.plusMonths(1); - finishedAt = finishedAt.plusMonths(1); - } - } - } - -/* ----------------------------- 스터디 투표 관련 API ------------------------------------- */ - - /** - * 스터디에 생성된 모든 투표 목록을 불러옵니다. - * @param studyId 투표 목록을 불러올 타겟 스터디의 아이디를 입력 받습니다. - * @return 스터디 아이디와 해당 스터디에서 진행중인 투표 목록, 마감된 투표 목록을 반환합니다. - */ - @Override - public StudyVoteResponseDTO.VoteListDTO getAllVotes(Long studyId) { - - //=== Exception ===// - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - //=== Feature ===// - - // 진행중인 투표 목록 - List votesInProgress = voteRepository.findAllByStudyIdAndFinishedAtAfter(studyId, LocalDateTime.now()).stream() - .map(vote -> { - boolean isParticipated = isParticipated(vote, member); - return StudyVoteResponseDTO.VoteInfoDTO.toDTO(vote, isParticipated); - }) - .toList(); - - // 마감된 투표 목록 - List votesInCompletion = voteRepository.findAllByStudyIdAndFinishedAtBefore(studyId, LocalDateTime.now()).stream() - .map(vote -> { - boolean isParticipated = isParticipated(vote, member); - return StudyVoteResponseDTO.VoteInfoDTO.toDTO(vote, isParticipated); - }) - .toList(); - - return StudyVoteResponseDTO.VoteListDTO.toDTO(studyId, votesInProgress, votesInCompletion); - } - - /** - * 스터디 회원의 투표 참여 여부를 확인하는 메서드입니다. - * getAllVotes에서 사용되는 내부 메서드입니다. - * @param vote 스터디에서 생성한 투표의 아이디를 입력 받습니다. - * @param loginMember 로그인한 회원의 정보를 입력 받습니다. - * @return 투표 참여 여부를 true or false로 반환합니다. - */ - private boolean isParticipated(Vote vote, Member loginMember) { - // 투표 참여 여부 확인 - boolean isParticipated = false; - for (VoteOption voteOption : vote.getVoteOptions()) { - if (voteParticipantRepository.existsByMemberIdAndVoteOptionId(loginMember.getId(), voteOption.getId())) { - isParticipated = true; - } - } - return isParticipated; - } - - /** - * 입력 받은 스터디 투표가 종료되었는지 확인하는 메서드입니다. - * (클라이언트에서 투표 불러오기 API를 호출할 때 스터디 종료 여부에 따라 Response DTO가 바뀌어야 하기 때문에 필요한 메서드입니다) - * @param voteId 스터디에서 생성한 투표의 아이디를 입력 받습니다. - * @return 투표 종료 여부를 true or false로 반환합니다. - */ - @Override - public Boolean getIsCompleted(Long voteId) { - return voteRepository.existsByIdAndFinishedAtBefore(voteId, LocalDateTime.now()); - } - - /** - * 종료된 투표의 정보를 불러오는 메서드입니다. - * @param studyId 타겟 스터디의 아이디를 입력 받습니다. - * @param voteId 스터디에서 생성한 투표의 아이디를 입력 받습니다. - * @return 종료된 투표의 아이디, 생성자, 제목, 항목별 투표 인원수, 전체 참여자 수, 종료 일시를 반환합니다. - */ - @Override - public StudyVoteResponseDTO.CompletedVoteDTO getVoteInCompletion(Long studyId, Long voteId) { - - //=== Exception ===// - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - memberRepository.findById(memberId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); - studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - Vote vote = voteRepository.findById(voteId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - // 해당 스터디의 투표인지 확인 - voteRepository.findByIdAndStudyId(voteId, studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); - - //=== Feature ===// - return StudyVoteResponseDTO.CompletedVoteDTO.toDTO(vote); - - } - - /** - * 진행중인 투표의 정보를 불러오는 메서드입니다. - * @param studyId 타겟 스터디의 아이디를 입력 받습니다. - * @param voteId 스터디에서 생성한 투표의 아이디를 입력 받습니다. - * @return 진행중인 투표의 아이디, 생성자, 제목, 항목 리스트, 복수 선택 가능 여부, 종료 일시, 로그인한 회원의 참여 여부를 반환합니다. - */ - @Override - public StudyVoteResponseDTO.VoteDTO getVoteInProgress(Long studyId, Long voteId) { - - //=== Exception ===// - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); - studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - Vote vote = voteRepository.findById(voteId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); - - // 해당 스터디의 투표인지 확인 - voteRepository.findByIdAndStudyId(voteId, studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - //=== Feature ===// - return StudyVoteResponseDTO.VoteDTO.toDTO(vote, member); - } - - /** - * 마감된 투표에 대해 항목별 투표 현황을 불러오는 메서드입니다. - * @param studyId 타겟 스터디의 아이디를 입력 받습니다. - * @param voteId 마감된 스터디 투표의 아이디를 입력 받습니다. - * @return 마감된 투표의 아이디와 제목, 항목별 투표 회원 목록을 반환합니다. - */ - @Override - public StudyVoteResponseDTO.CompletedVoteDetailDTO getCompletedVoteDetail(Long studyId, Long voteId) { - - //=== Exception ===// - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - - memberRepository.findById(memberId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); - Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - Vote vote = voteRepository.findById(voteId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); - - // 해당 스터디의 투표인지 확인 - voteRepository.findByIdAndStudyId(voteId, studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - // 마감된 투표인지 확인 - if (!voteRepository.existsByIdAndFinishedAtBefore(voteId, LocalDateTime.now())) { - throw new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_COMPLETED); - } - - //=== Feature ===// - return StudyVoteResponseDTO.CompletedVoteDetailDTO.toDTO(vote); - } - - /** - * 회원이 스터디 장인지 확인합니다. - * @param memberId 확인 하려는 회원 ID - * @param studyId 확인 하려는 스터디 ID - * @return 스터디 장 여부를 반환합니다. - */ - private boolean isOwner(Long memberId, Long studyId) { - return studyMemberRepository.findByMemberIdAndStudyIdAndIsOwned(memberId, studyId, true).isPresent(); - } - - /** - * 회원이 스터디 구성원인지 확인합니다. - * @param memberId 확인 하려는 회원 ID - * @param studyId 확인 하려는 스터디 ID - * @return 스터디 참여 여부를 반환합니다. - */ - private boolean isMember(Long memberId, Long studyId) { - return studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED).isPresent(); - } -/* ----------------------------- 스터디 갤러리 관련 API ------------------------------------- */ - - /** - * 스터디 게시판에 업로드한 이미지 목록을 불러오는 메서드입니다. - * @param studyId 타겟 스터디의 아이디를 입력 받습니다. - * @param pageRequest 페이징에 필요한 페이지 번호와 크기를 입력 받습니다. - * @return 스터디 아이디와 해당 스터디에 업로드된 이미지 목록을 반환합니다. - */ - @Override - public StudyImageResponseDTO.ImageListDTO getAllStudyImages(Long studyId, PageRequest pageRequest) { - - //=== Exception ===// - Long memberId = SecurityUtils.getCurrentUserId(); - SecurityUtils.verifyUserId(memberId); - studyRepository.findById(studyId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - memberRepository.findById(memberId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); - - // 로그인한 회원이 스터디 회원인지 확인 - studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); - - //=== Feature ===// - List images = storyRepository.findAllByStudyId(studyId, pageRequest) - .stream() - .sorted(Comparator.comparing(Story::getCreatedAt).reversed()) - .flatMap(studyPost -> studyPost.getImages().stream()) - .map(StudyImageResponseDTO.ImageDTO::toDTO) - .toList(); - - return StudyImageResponseDTO.ImageListDTO.toDTO(studyId, images); - - } - -/* ----------------------------- To-do list 관련 API ------------------------------------- */ - - /** - * 특정 스터디에 저장된 내 To-Do List를 날짜 별로 페이징 조회합니다. - * @param studyId 스터디 ID - * @param date 조회하려는 날짜 - * @param pageRequest 페이징 정보 - * @return To-Do List 목록을 반환합니다. - * @throws GeneralException 스터디 멤버가 아닌 경우 - * @throws GeneralException 스터디 할 일이 존재하지 않는 경우 - */ - @Override - public ToDoListSearchResponseDTO getToDoList(Long studyId, LocalDate date, PageRequest pageRequest) { - // 로그인 중인 회원 ID 조회 - Long memberId = SecurityUtils.getCurrentUserId(); - - // 로그인한 회원이 스터디 회원인지 확인 - if (!isMember(memberId, studyId)) - throw new GeneralException(ErrorStatus._ONLY_STUDY_MEMBER_CAN_ACCESS_TODO_LIST); - - // 페이징 처리 - List toDos = toDoRepository.findByStudyIdAndMemberIdAndDateOrderByCreatedAtDesc( - studyId, memberId, date, pageRequest); - - // 스터디 투 두 리스트가 존재하지 않는 경우 - if (toDos.isEmpty()) - throw new GeneralException(ErrorStatus._STUDY_TODO_NOT_FOUND); - - // 투 두 리스트 갯수 조회 - long totalElements = toDoRepository.countByStudyIdAndMemberIdAndDate(studyId, memberId, date); - - // DTO로 변환 - List toDoListDTOS = getToDoListDTOS(toDos); - - return new ToDoListSearchResponseDTO( - new PageImpl<>(toDoListDTOS, pageRequest, totalElements), toDoListDTOS, totalElements); - } - - /** - * 특정 스터디에 저장된 다른 스터디원의 To-Do List를 날짜 별로 페이징 조회합니다. - * @param studyId 스터디 ID - * @param memberId 조회하려는 회원 ID - * @param date 조회하려는 날짜 - * @param pageRequest 페이징 정보 - * @return To-Do List 목록을 반환합니다. - * @throws GeneralException 스터디 멤버가 아닌 경우 - * @throws GeneralException 조회하려는 회원이 스터디 멤버가 아닌 경우 - * @throws GeneralException 스터디 할 일이 존재하지 않는 경우 - */ - @Override - public ToDoListSearchResponseDTO getMemberToDoList(Long studyId, Long memberId, LocalDate date, - PageRequest pageRequest) { - - // 로그인 중인 회원이 스터디 회원인지 확인 - if (!isMember(SecurityUtils.getCurrentUserId(), studyId)) - throw new GeneralException(ErrorStatus._ONLY_STUDY_MEMBER_CAN_ACCESS_TODO_LIST); - - // 조회하려는 회원이 스터디 회원인지 확인 - if (!isMember(memberId, studyId)) - throw new GeneralException(ErrorStatus._TODO_LIST_MEMBER_NOT_FOUND); - - // 조회하려는 회원의 투 두 리스트 조회 - List toDos = toDoRepository.findByStudyIdAndMemberIdAndDateOrderByCreatedAtDesc( - studyId, memberId, date, pageRequest); - - // 투 두 리스트가 존재하지 않는 경우 - if (toDos.isEmpty()) - throw new GeneralException(ErrorStatus._STUDY_TODO_NOT_FOUND); - - // 투 두 리스트 갯수 조회 - long totalElements = toDoRepository.countByStudyIdAndMemberIdAndDate(studyId, memberId, date); - - // DTO로 변환 - List toDoListDTOS = getToDoListDTOS(toDos); - - return new ToDoListSearchResponseDTO( - new PageImpl<>(toDoListDTOS, pageRequest, totalElements), toDoListDTOS, totalElements); - } - - /** - * 투 두 리스트를 DTO로 변환합니다. - * @param toDos 투 두 리스트 - * @return 투 두 리스트 DTO 목록을 반환합니다. - */ - private static List getToDoListDTOS(List toDos) { - List toDoListDTOS = toDos.stream() - .map(toDoList -> ToDoListDTO.builder() - .id(toDoList.getId()) - .content(toDoList.getContent()) - .date(toDoList.getDate()) - .isDone(toDoList.isDone()) - .build()) - .toList(); - return toDoListDTOS; - } - -} diff --git a/src/main/java/com/example/spot/study/application/StudyCommandService.java b/src/main/java/com/example/spot/study/application/StudyCommandService.java index ee5737a3..06c5274d 100644 --- a/src/main/java/com/example/spot/study/application/StudyCommandService.java +++ b/src/main/java/com/example/spot/study/application/StudyCommandService.java @@ -8,11 +8,12 @@ public interface StudyCommandService { + // 스터디 참여 신청하기 StudyJoinResponseDTO.JoinDTO applyToStudy(Long studyId, StudyJoinRequestDTO.StudyJoinDTO studyJoinRequestDTO); + // 스터디 등록하기 StudyRegisterResponseDTO.RegisterDTO registerStudy(StudyRegisterRequestDTO.RegisterDTO studyRegisterRequestDTO); - // TODO 스터디 좋아요 Member 도메인 하위로 옮기기 StudyLikeResponseDTO likeStudy(Long memberId, Long studyId); diff --git a/src/main/java/com/example/spot/study/application/StudyCommandServiceImpl.java b/src/main/java/com/example/spot/study/application/StudyCommandServiceImpl.java index cc8d01f2..d7f8f2e0 100644 --- a/src/main/java/com/example/spot/study/application/StudyCommandServiceImpl.java +++ b/src/main/java/com/example/spot/study/application/StudyCommandServiceImpl.java @@ -1,19 +1,19 @@ package com.example.spot.study.application; -import com.example.spot.study.domain.aggregate.StudyRegion; +import com.example.spot.study.domain.association.StudyRegion; import com.example.spot.common.api.code.status.ErrorStatus; import com.example.spot.common.api.exception.handler.MemberHandler; import com.example.spot.common.api.exception.handler.StudyHandler; import com.example.spot.member.domain.Member; -import com.example.spot.study.domain.aggregate.Region; -import com.example.spot.study.domain.aggregate.StudyMember; -import com.example.spot.study.domain.aggregate.Theme; +import com.example.spot.study.domain.association.Region; +import com.example.spot.study.domain.association.StudyMember; +import com.example.spot.study.domain.association.Theme; import com.example.spot.study.domain.enums.StudyApplicationStatus; import com.example.spot.member.domain.enums.Status; import com.example.spot.study.domain.enums.StudyLikeStatus; import com.example.spot.study.domain.enums.StudyState; import com.example.spot.member.domain.association.PreferredStudy; -import com.example.spot.study.domain.aggregate.StudyTheme; +import com.example.spot.study.domain.association.StudyTheme; import com.example.spot.study.domain.Study; import com.example.spot.member.domain.MemberRepository; import com.example.spot.study.domain.repository.StudyMemberRepository; diff --git a/src/main/java/com/example/spot/study/application/StudyMemberCommandService.java b/src/main/java/com/example/spot/study/application/StudyMemberCommandService.java new file mode 100644 index 00000000..ad50e46d --- /dev/null +++ b/src/main/java/com/example/spot/study/application/StudyMemberCommandService.java @@ -0,0 +1,33 @@ +package com.example.spot.study.application; + +import com.example.spot.member.presentation.dto.MemberResponseDTO; +import com.example.spot.study.presentation.dto.request.StudyHostWithdrawRequestDTO; +import com.example.spot.study.presentation.dto.request.StudyMemberReportDTO; +import com.example.spot.vote.presentation.dto.request.StudyVoteRequestDTO; +import com.example.spot.story.web.dto.response.StoryResDTO; +import com.example.spot.study.presentation.dto.response.StudyTerminationResponseDTO; +import com.example.spot.vote.presentation.dto.response.StudyVoteResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyWithdrawalResponseDTO; +import com.example.spot.todo.presentation.dto.request.ToDoListRequestDTO; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO.ToDoListCreateResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyApplyResponseDTO; +import jakarta.validation.Valid; + +public interface StudyMemberCommandService { + + // 스터디 탈퇴 + StudyWithdrawalResponseDTO.WithdrawalDTO withdrawFromStudy(Long studyId); + + // 스터디 호스트 탈퇴 + StudyWithdrawalResponseDTO.WithdrawalDTO withdrawHostFromStudy(Long studyId, StudyHostWithdrawRequestDTO requestDTO); + + // 스터디 종료 + StudyTerminationResponseDTO.TerminationDTO terminateStudy(Long studyId, String performance); + + // 스터디 신청 수락 + StudyApplyResponseDTO acceptAndRejectStudyApply(Long memberId, Long studyId, boolean isAccept); + + // 스터디 승인 거절 테스트 + StudyApplyResponseDTO acceptAndRejectStudyApplyForTest(Long memberId, Long studyId, boolean isAccept); +} diff --git a/src/main/java/com/example/spot/study/application/StudyMemberCommandServiceImpl.java b/src/main/java/com/example/spot/study/application/StudyMemberCommandServiceImpl.java new file mode 100644 index 00000000..c7660a3f --- /dev/null +++ b/src/main/java/com/example/spot/study/application/StudyMemberCommandServiceImpl.java @@ -0,0 +1,276 @@ +package com.example.spot.study.application; + +import com.example.spot.common.api.code.status.ErrorStatus; +import com.example.spot.common.api.exception.GeneralException; +import com.example.spot.common.api.exception.handler.MemberHandler; +import com.example.spot.common.api.exception.handler.StudyHandler; +import com.example.spot.member.domain.Member; +import com.example.spot.notification.domain.Notification; +import com.example.spot.study.domain.enums.StudyApplicationStatus; +import com.example.spot.notification.domain.enums.NotifyType; +import com.example.spot.member.domain.enums.Status; +import com.example.spot.study.domain.association.StudyMember; +import com.example.spot.study.domain.Study; +import com.example.spot.report.domain.MemberReportRepository; +import com.example.spot.member.domain.MemberRepository; +import com.example.spot.study.domain.repository.StudyMemberRepository; +import com.example.spot.notification.domain.NotificationRepository; +import com.example.spot.report.domain.StoryReportRepository; +import com.example.spot.story.domain.StoryRepository; +import com.example.spot.study.domain.StudyRepository; +import com.example.spot.study.presentation.dto.request.StudyHostWithdrawRequestDTO; +import com.example.spot.study.presentation.dto.response.StudyTerminationResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyWithdrawalResponseDTO; +import com.example.spot.common.security.utils.SecurityUtils; +import com.example.spot.common.application.s3.S3ImageService; +import com.example.spot.study.presentation.dto.response.StudyWithdrawalResponseDTO.WithdrawalDTO; +import com.example.spot.study.presentation.dto.response.StudyApplyResponseDTO; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + + +@Service +@RequiredArgsConstructor +@Transactional +public class StudyMemberCommandServiceImpl implements StudyMemberCommandService { + + private final MemberRepository memberRepository; + private final StudyMemberRepository studyMemberRepository; + private final StudyRepository studyRepository; + private final StoryRepository storyRepository; + private final NotificationRepository notificationRepository; + private final MemberReportRepository memberReportRepository; + private final StoryReportRepository storyReportRepository; + + + // S3 Service + private final S3ImageService s3ImageService; + +/* ----------------------------- 진행중인 스터디 관련 API ------------------------------------- */ + + /** + * 진행중인 스터디에서 탈퇴하기 위한 메서드입니다. + * 스터디장은 스터디를 탈퇴할 수 없으며 스터디를 종료하고자 하는 경우 스터디 terminateStudy API를 호출해야 합니다. + * + * @param studyId 타겟 회원이 탈퇴하고자 하는 스터디의 아이디를 입력 받습니다. + * @return 탈퇴한 스터디의 아이디와 이름, 탈퇴한 회원의 아이디와 이름이 반환됩니다. + */ + public StudyWithdrawalResponseDTO.WithdrawalDTO withdrawFromStudy(Long studyId) { + + // Authorization + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + + StudyMember studyMember = studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 참여가 승인되지 않은 스터디는 탈퇴할 수 없음 + if (!studyMember.getStatus().equals(StudyApplicationStatus.APPROVED)) { + throw new StudyHandler(ErrorStatus._STUDY_NOT_APPROVED); + } + // 스터디장은 스터디를 탈퇴할 수 없음 + if (studyMember.getIsOwned()) { + throw new StudyHandler(ErrorStatus._STUDY_OWNER_CANNOT_WITHDRAW); + } + + studyMemberRepository.delete(studyMember); + + return StudyWithdrawalResponseDTO.WithdrawalDTO.toDTO(member, study); + } + + @Override + public WithdrawalDTO withdrawHostFromStudy(Long studyId, StudyHostWithdrawRequestDTO requestDTO) { + // Authorization + Long hostId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(hostId); + + StudyMember studyMember = studyMemberRepository.findByMemberIdAndStudyIdAndStatus(hostId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + if (!studyMember.getIsOwned()) { + throw new StudyHandler(ErrorStatus._STUDY_OWNER_ONLY_CAN_WITHDRAW); + } + + StudyMember newHostStudy = studyMemberRepository.findByMemberIdAndStudyIdAndStatus(requestDTO.getNewHostId(), studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_EXIST)); + + studyMemberRepository.delete(studyMember); + + newHostStudy.setIsOwned(true); + newHostStudy.setReason(requestDTO.getReason()); + + studyMemberRepository.save(newHostStudy); + + return StudyWithdrawalResponseDTO.WithdrawalDTO.toDTO(newHostStudy.getMember(), newHostStudy.getStudy()); + } + /** + * 운영중인 스터디를 종료하는 메서드입니다. 스터디장만 호출 가능합니다. + * + * @param studyId 종료할 스터디의 아이디를 입력 받습니다. + * @param performance 종료할 스터디의 성과를 입력 받습니다. + * @return 종료된 스터디의 아이디, 이름, 상태를 반환합니다. + */ + public StudyTerminationResponseDTO.TerminationDTO terminateStudy(Long studyId, String performance) { + + // Authorization + Long memberId = SecurityUtils.getCurrentUserId(); + memberRepository.findById(memberId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + StudyMember studyMember = studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 스터디장이 아니면 스터디를 종료할 수 없음 + if (studyMember.getIsOwned().equals(false)) { + throw new StudyHandler(ErrorStatus._STUDY_OWNER_ONLY_CAN_TERMINATE); + } + + // 이미 종료된 스터디는 종료할 수 없음 + if (study.getStatus().equals(Status.OFF)) { + throw new StudyHandler(ErrorStatus._STUDY_ALREADY_TERMINATED); + } + + study.terminateStudy(performance); + studyRepository.save(study); + + return StudyTerminationResponseDTO.TerminationDTO.toDTO(study); + } + + /** + * 스터디 신청을 처리합니다. isAccept가 true이면 승인, false이면 거절합니다. + * 이후 관련 알림을 생성합니다. 알림을 통해 최종 참여 승인을 해야 스터디에 참여할 수 있습니다. + * @param memberId 스터디에 신청한 회원 ID + * @param studyId 스터디 ID + * @param isAccept 승인 여부 + * @return 스터디 신청 처리 결과 및 처리 시간 + * @throws GeneralException 스터디 신청을 처리하는 회원이 스터디 소유자가 아닐 때 + * @throws GeneralException 스터디 소유자가 신청한 경우 + * @throws StudyHandler 스터디 신청자를 찾을 수 없을 때 + * @throws StudyHandler 스터디 신청이 이미 처리되었을 때 + * @throws MemberHandler 스터디 장을 찾을 수 없을 때 + */ + @Override + public StudyApplyResponseDTO acceptAndRejectStudyApply(Long memberId, Long studyId, + boolean isAccept) { + + // 신청을 처리하는 회원이 스터디 소유자인지 확인 + if (!isOwner(SecurityUtils.getCurrentUserId(), studyId)) + throw new GeneralException(ErrorStatus._ONLY_STUDY_OWNER_CAN_ACCESS_APPLICANTS); + + // 스터디 신청자 조회 + StudyMember studyMember = studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPLIED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_APPLICANT_NOT_FOUND)); + + // 스터디 소유자가 스터디 신청한 경우 + if (studyMember.getIsOwned()) + throw new GeneralException(ErrorStatus._STUDY_OWNER_CANNOT_APPLY); + + // 스터디 신청이 이미 처리되었을 때 + if (studyMember.getStatus() != StudyApplicationStatus.APPLIED) + throw new GeneralException(ErrorStatus._STUDY_APPLY_ALREADY_PROCESSED); + + // 스터디 장 조회 + Member owner = memberRepository.findById(SecurityUtils.getCurrentUserId()) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + + // 승인인 경우 + if (isAccept) { + // 스터디 참여 승인 최종 대기 + studyMember.setStatus(StudyApplicationStatus.AWAITING_SELF_APPROVAL); + + // 알림 생성 + Notification notification = Notification.builder() + .member(studyMember.getMember()) // 신청자 + .study(studyMember.getStudy()) + .notifierName(owner.getName()) // 스터디장 이름 + .type(NotifyType.STUDY_APPLY) + .isChecked(Boolean.FALSE) + .build(); + + notificationRepository.save(notification); + } + else { // 거절인 경우 + studyMember.setStatus(StudyApplicationStatus.REJECTED); + studyMemberRepository.delete(studyMember); + } + + // 스터디 신청 처리 결과 반환 + return StudyApplyResponseDTO.builder() + .status(studyMember.getStatus()) + .updatedAt(studyMember.getUpdatedAt()) + .build(); + } + + /** + * 스터디 신청을 처리합니다. isAccept가 true이면 승인, false이면 거절합니다. + * 이 메서드를 사용하면 알림 처리 없이 바로 스터디에 참여할 수 있습니다. + * @param memberId 스터디에 신청한 회원 ID + * @param studyId 스터디 ID + * @param isAccept 승인 여부 + * @return 스터디 신청 처리 결과 및 처리 시간 + * @throws GeneralException 스터디 신청을 처리하는 회원이 스터디 소유자가 아닐 때 + * @throws GeneralException 스터디 소유자가 신청한 경우 + * @throws StudyHandler 스터디 신청자를 찾을 수 없을 때 + * @throws StudyHandler 스터디 신청이 이미 처리되었을 때 + * @throws MemberHandler 스터디 장을 찾을 수 없을 때 + * + */ + @Override + public StudyApplyResponseDTO acceptAndRejectStudyApplyForTest(Long memberId, Long studyId, + boolean isAccept) { + + // 스터디 신청을 처리하는 회원이 스터디 소유자인지 확인 + if (!isOwner(SecurityUtils.getCurrentUserId(), studyId)) + throw new GeneralException(ErrorStatus._ONLY_STUDY_OWNER_CAN_ACCESS_APPLICANTS); + + // 스터디 신청자 조회 + StudyMember studyMember = studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPLIED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_APPLICANT_NOT_FOUND)); + + // 스터디 소유자가 스터디 신청한 경우 + if (studyMember.getIsOwned()) + throw new GeneralException(ErrorStatus._STUDY_OWNER_CANNOT_APPLY); + + // 스터디 신청이 이미 처리되었을 때 + if (studyMember.getStatus() != StudyApplicationStatus.APPLIED) + throw new GeneralException(ErrorStatus._STUDY_APPLY_ALREADY_PROCESSED); + + // 스터디 장 조회 + Member owner = memberRepository.findById(SecurityUtils.getCurrentUserId()) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + + // 승인인 경우 + if (isAccept) { + studyMember.setStatus(StudyApplicationStatus.APPROVED); + } + else { + studyMember.setStatus(StudyApplicationStatus.REJECTED); + studyMemberRepository.delete(studyMember); + } + + // 스터디 신청 처리 결과 반환 + return StudyApplyResponseDTO.builder() + .status(studyMember.getStatus()) + .updatedAt(studyMember.getUpdatedAt()) + .build(); + } + + /** + * 회원이 스터디 장인지 확인합니다. + * @param memberId 확인 하려는 회원 ID + * @param studyId 확인 하려는 스터디 ID + * @return 스터디 장 여부를 반환합니다. + */ + private boolean isOwner(Long memberId, Long studyId) { + return studyMemberRepository.findByMemberIdAndStudyIdAndIsOwned(memberId, studyId, true).isPresent(); + } +} diff --git a/src/main/java/com/example/spot/study/application/StudyMemberQueryService.java b/src/main/java/com/example/spot/study/application/StudyMemberQueryService.java new file mode 100644 index 00000000..c925c5f5 --- /dev/null +++ b/src/main/java/com/example/spot/study/application/StudyMemberQueryService.java @@ -0,0 +1,36 @@ +package com.example.spot.study.application; + +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO; +import com.example.spot.schedule.presentation.dto.response.ScheduleResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyImageResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyMemberResDTO; +import com.example.spot.schedule.presentation.dto.response.StudyQuizResponseDTO; +import com.example.spot.vote.presentation.dto.response.StudyVoteResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO; +import com.example.spot.story.web.dto.response.StoryResponseDTO; +import com.example.spot.schedule.presentation.dto.response.StudyScheduleResponseDTO; + + +import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO.StudyApplicantDTO; +import java.time.LocalDate; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +public interface StudyMemberQueryService { + + // 참여하는 회원 목록 불러오기 + StudyMemberResponseDTO findStudyMembers(Long studyId); + + // 스터디 별 신청 회원 목록 조회하기 + StudyMemberResponseDTO findStudyApplicants(Long studyId); + + // 스터디 호스트 조회하기 + StudyMemberResDTO.StudyHostDTO getStudyHost(Long studyId); + + // 스터디 신청 정보 가져오기 + StudyMemberResponseDTO.StudyApplyMemberDTO findStudyApplication(Long studyId, Long memberId); + + // 스터디 신청 여부 확인 + StudyApplicantDTO isApplied(Long studyId); + +} diff --git a/src/main/java/com/example/spot/study/application/StudyMemberQueryServiceImpl.java b/src/main/java/com/example/spot/study/application/StudyMemberQueryServiceImpl.java new file mode 100644 index 00000000..69368081 --- /dev/null +++ b/src/main/java/com/example/spot/study/application/StudyMemberQueryServiceImpl.java @@ -0,0 +1,207 @@ +package com.example.spot.study.application; + +import com.example.spot.common.api.code.status.ErrorStatus; +import com.example.spot.common.api.exception.handler.MemberHandler; +import com.example.spot.common.api.exception.handler.StudyHandler; +import com.example.spot.member.domain.Member; +import com.example.spot.story.domain.Story; +import com.example.spot.study.domain.association.StudyMember; +import com.example.spot.study.domain.enums.StudyApplicationStatus; +import com.example.spot.member.domain.MemberRepository; +import com.example.spot.study.domain.repository.StudyMemberRepository; +import com.example.spot.story.domain.StoryRepository; +import com.example.spot.study.domain.StudyRepository; +import com.example.spot.study.presentation.dto.response.StudyImageResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyMemberResDTO; +import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO; +import com.example.spot.story.web.dto.response.StoryResponseDTO; + +import com.example.spot.common.security.utils.SecurityUtils; + +import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO.StudyApplicantDTO; +import lombok.RequiredArgsConstructor; +import com.example.spot.common.api.exception.GeneralException; +import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO.StudyApplyMemberDTO; +import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO.StudyMemberDTO; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Comparator; +import java.util.List; + + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class StudyMemberQueryServiceImpl implements StudyMemberQueryService { + + private final MemberRepository memberRepository; + private final StudyRepository studyRepository; + private final StoryRepository storyRepository; + private final StudyMemberRepository studyMemberRepository; + + /** + * 특정 스터디의 회원 목록을 전체 조회 합니다. 가입된 스터디가 아니더라도 회원 목록을 조회할 수 있습니다. + * + * @param studyId 스터디 ID + * @return 스터디에 참여하는 회원 목록을 반환합니다. + * @throws GeneralException 스터디 할 일이 존재하지 않는 경우 + * @throws GeneralException 스터디 멤버가 아닌 경우 + */ + @Override + public StudyMemberResponseDTO findStudyMembers(Long studyId) { + + // 스터디 멤버 조회 + List memberStudies = studyMemberRepository.findAllByStudyIdAndStatus(studyId, StudyApplicationStatus.APPROVED); + + // 스터디 멤버가 존재하지 않는 경우 + if (memberStudies.isEmpty()) + throw new GeneralException(ErrorStatus._STUDY_MEMBER_NOT_FOUND); + + // DTO로 변환하여 반환 + List memberDTOS = memberStudies.stream().map(memberStudy -> StudyMemberDTO.builder() + .memberId(memberStudy.getMember().getId()) + .nickname(memberStudy.getMember().getName()) + .profileImage(memberStudy.getMember().getProfileImage()) + .build()).toList(); + // DTO로 변환하여 반환 + return new StudyMemberResponseDTO(memberDTOS); + } + + + /** + * 회원이 모집중인 스터디에 신청한 회원 목록을 불러옵니다. + * + * @param studyId 스터디 ID + * @return 스터디 신청자 목록을 반환합니다. + * @throws GeneralException 스터디 신청자가 존재 하지 않는 경우 + * @throws GeneralException 조회 하는 회원이 스터디 장이 아닌 경우 + */ + @Override + public StudyMemberResponseDTO findStudyApplicants(Long studyId) { + + // 로그인한 회원이 해당 스터디 장인지 확인 + if (!isOwner(SecurityUtils.getCurrentUserId(), studyId)) + throw new GeneralException(ErrorStatus._ONLY_STUDY_OWNER_CAN_ACCESS_APPLICANTS); + + // 스터디 신청자 조회 + List memberStudies = studyMemberRepository.findAllByStudyIdAndStatus(studyId, StudyApplicationStatus.APPLIED); + + // 스터디 신청자가 존재하지 않는 경우 + if (memberStudies.isEmpty()) + throw new GeneralException(ErrorStatus._STUDY_APPLICANT_NOT_FOUND); + + // DTO로 변환하여 반환 + List memberDTOS = memberStudies.stream().map(memberStudy -> StudyMemberDTO.builder() + .memberId(memberStudy.getMember().getId()) + .nickname(memberStudy.getMember().getName()) + .profileImage(memberStudy.getMember().getProfileImage()) + .build()).toList(); + + // DTO로 변환하여 반환 + return new StudyMemberResponseDTO(memberDTOS); + } + + @Override + public StudyMemberResDTO.StudyHostDTO getStudyHost(Long studyId) { + + // Authorization + Long memberId = SecurityUtils.getCurrentUserId(); + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + + // 스터디 호스트 찾기 + StudyMember studyHost = studyMemberRepository.findByStudyIdAndIsOwned(studyId, true) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_OWNER_NOT_FOUND)); + + // 로그인한 회원이 호스트인지 확인 + if (studyHost.getMember().getId().equals(memberId)) { + return StudyMemberResDTO.StudyHostDTO.toDTO(true, member); + } else { + return StudyMemberResDTO.StudyHostDTO.toDTO(false, studyHost.getMember()); + } + } + + /** + * 스터디 신청자의 정보를 조회합니다. + * + * @param studyId 스터디 ID + * @param memberId 회원 ID + * @return 스터디 신청자 정보를 반환합니다. + * @throws GeneralException 스터디 신청자가 존재하지 않는 경우 + * @throws GeneralException 조회 하는 회원이 스터디 장이 아닌 경우 + * @throws GeneralException 스터디 장은 스터디에 신청할 수 없음 + */ + @Override + public StudyApplyMemberDTO findStudyApplication(Long studyId, Long memberId) { + + // 로그인한 회원이 해당 스터디 장인지 확인 + if (!isOwner(SecurityUtils.getCurrentUserId(), studyId)) + throw new GeneralException(ErrorStatus._ONLY_STUDY_OWNER_CAN_ACCESS_APPLICANTS); + + // 스터디 신청자 조회 + StudyMember studyMember = studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPLIED) + .orElseThrow(() -> new GeneralException(ErrorStatus._STUDY_APPLICANT_NOT_FOUND)); + + // 스터디 장은 스터디에 신청할 수 없음 + if (studyMember.getIsOwned()) + throw new GeneralException(ErrorStatus._STUDY_OWNER_CANNOT_APPLY); + + // DTO로 변환하여 반환 + return StudyApplyMemberDTO.builder() + .memberId(studyMember.getMember().getId()) + .studyId(studyMember.getStudy().getId()) + .introduction(studyMember.getIntroduction()) + .nickname(studyMember.getMember().getName()) + .profileImage(studyMember.getMember().getProfileImage()) + .build(); + } + + /** + * 해당 스터디 신청 여부를 조회합니다. true: 신청, false: 미신청 + * + * @param studyId 스터디 ID + * @return 신청 여부와 스터디 ID를 반환합니다. + * @throws GeneralException 이미 스터디 멤버인 경우 + */ + @Override + public StudyApplicantDTO isApplied(Long studyId) { + // 로그인한 회원 ID 조회 + Long currentUserId = SecurityUtils.getCurrentUserId(); + + // 이미 스터디 멤버인 경우 + if (isMember(currentUserId, studyId)) + throw new GeneralException(ErrorStatus._ALREADY_STUDY_MEMBER); + + // DTO로 변환하여 반환 + return StudyApplicantDTO.builder() + .isApplied(studyMemberRepository.existsByMemberIdAndStudyIdAndStatus(currentUserId, studyId, StudyApplicationStatus.APPLIED)) + .studyId(studyId) + .build(); + + } + + /** + * 회원이 스터디 장인지 확인합니다. + * + * @param memberId 확인 하려는 회원 ID + * @param studyId 확인 하려는 스터디 ID + * @return 스터디 장 여부를 반환합니다. + */ + private boolean isOwner(Long memberId, Long studyId) { + return studyMemberRepository.findByMemberIdAndStudyIdAndIsOwned(memberId, studyId, true).isPresent(); + } + + /** + * 회원이 스터디 구성원인지 확인합니다. + * + * @param memberId 확인 하려는 회원 ID + * @param studyId 확인 하려는 스터디 ID + * @return 스터디 참여 여부를 반환합니다. + */ + private boolean isMember(Long memberId, Long studyId) { + return studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED).isPresent(); + } + +} diff --git a/src/main/java/com/example/spot/study/application/StudyPostCommandService.java b/src/main/java/com/example/spot/study/application/StudyPostCommandService.java deleted file mode 100644 index ce696120..00000000 --- a/src/main/java/com/example/spot/study/application/StudyPostCommandService.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.example.spot.study.application; - -import com.example.spot.study.presentation.dto.request.StudyPostCommentRequestDTO; -import com.example.spot.study.presentation.dto.request.StudyPostRequestDTO; -import com.example.spot.study.presentation.dto.response.StudyPostCommentResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyPostResDTO; - -public interface StudyPostCommandService { - - // 스터디 게시글 생성 - StudyPostResDTO.PostPreviewDTO createPost(Long studyId, StudyPostRequestDTO.PostDTO postRequestDTO); - - // 스터디 게시글 편집 - StudyPostResDTO.PostPreviewDTO updatePost(Long studyId, Long postId, StudyPostRequestDTO.PostDTO postDTO); - - // 스터디 게시글 삭제 - StudyPostResDTO.PostPreviewDTO deletePost(Long studyId, Long postId); - - // 스터디 게시글 좋아요 - StudyPostResDTO.PostLikeNumDTO likePost(Long studyId, Long postId); - - // 스터디 게시글 좋아요 취소 - StudyPostResDTO.PostLikeNumDTO cancelPostLike(Long studyId, Long postId); - - // 스터디 게시글 댓글 생성 - StudyPostCommentResponseDTO.CommentDTO createComment(Long studyId, Long postId, StudyPostCommentRequestDTO.CommentDTO commentRequestDTO); - - // 스터디 게시글 답글 생성 - StudyPostCommentResponseDTO.CommentDTO createReply(Long studyId, Long postId, Long commentId, StudyPostCommentRequestDTO.CommentDTO commentRequestDTO); - - // 스터디 게시글 댓글 삭제 (댓/답글 구분 X) - StudyPostCommentResponseDTO.CommentIdDTO deleteComment(Long studyId, Long postId, Long commentId); - - // 스터디 게시글 댓글 좋아요 - StudyPostCommentResponseDTO.CommentPreviewDTO likeComment(Long studyId, Long postId, Long commentId); - - // 스터디 게시글 댓글 싫어요 - StudyPostCommentResponseDTO.CommentPreviewDTO dislikeComment(Long studyId, Long postId, Long commentId); - - // 스터디 게시글 댓글 좋아요 취소 - StudyPostCommentResponseDTO.CommentPreviewDTO cancelCommentLike(Long studyId, Long postId, Long commentId); - - // 스터디 게시글 댓글 싫어요 취소 - StudyPostCommentResponseDTO.CommentPreviewDTO cancelCommentDislike(Long studyId, Long postId, Long commentId); - -} diff --git a/src/main/java/com/example/spot/study/application/StudyPostQueryService.java b/src/main/java/com/example/spot/study/application/StudyPostQueryService.java deleted file mode 100644 index 094cdd4c..00000000 --- a/src/main/java/com/example/spot/study/application/StudyPostQueryService.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.spot.study.application; - -import com.example.spot.story.domain.enums.StoryCategoryQuery; -import com.example.spot.study.presentation.dto.response.StudyPostCommentResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyPostResDTO; -import org.springframework.data.domain.PageRequest; - -public interface StudyPostQueryService { - - // 스터디 게시글 목록 불러오기 - StudyPostResDTO.PostListDTO getAllPosts(PageRequest pageRequest, Long studyId, StoryCategoryQuery storyCategoryQuery); - - // 스터디 게시글 불러오기 - StudyPostResDTO.PostDetailDTO getPost(Long studyId, Long postId, Boolean likeOrScrap); - - // 스터디 게시글 댓글 목록 불러오기 - StudyPostCommentResponseDTO.CommentReplyListDTO getAllComments(Long studyId, Long postId); -} diff --git a/src/main/java/com/example/spot/study/application/StudyQueryService.java b/src/main/java/com/example/spot/study/application/StudyQueryService.java index c74cdf21..6c281a23 100644 --- a/src/main/java/com/example/spot/study/application/StudyQueryService.java +++ b/src/main/java/com/example/spot/study/application/StudyQueryService.java @@ -19,16 +19,19 @@ public interface StudyQueryService { // 스터디 정보 조회 StudyInfoResponseDTO.StudyInfoDTO getStudyInfo(Long studyId); - // 마이페이지 용 스터디 갯수 조회 + // 마이페이지 용 스터디 개수 조회 MyPageDTO getMyPageStudyCount(Long memberId); + // 스터디 목록 조회 StudyPreviewDTO findStudies(Pageable pageable, StudySortBy sortBy); + // 조건별 스터디 목록 조회 StudyPreviewDTO findStudiesByConditions(Pageable pageable, SearchRequestStudyDTO request, StudySortBy sortBy); // 내 추천 스터디 조회 StudyPreviewDTO findRecommendStudies(Long memberId); + // 회원별 관심 Best 스터디 3개 조회 StudyPreviewDTO findInterestedStudies(Long memberId); // 내 관심사 스터디 페이징 조회 @@ -36,8 +39,7 @@ StudyPreviewDTO findInterestStudiesByConditionsAll(Pageable pageable, Long membe SearchRequestStudyDTO request, StudySortBy sortBy); // 내 특정 관심사 스터디 페이징 조회 - StudyPreviewDTO findInterestStudiesByConditionsSpecific(Pageable pageable, Long memberId, - SearchRequestStudyDTO request, ThemeType theme, StudySortBy sortBy); + StudyPreviewDTO findInterestStudiesByConditionsSpecific(Pageable pageable, Long memberId, SearchRequestStudyDTO request, ThemeType theme, StudySortBy sortBy); // 내 관심 지역 스터디 페이징 조회 StudyPreviewDTO findInterestRegionStudiesByConditionsAll( diff --git a/src/main/java/com/example/spot/study/application/StudyQueryServiceImpl.java b/src/main/java/com/example/spot/study/application/StudyQueryServiceImpl.java index 5e89066a..072dbd73 100644 --- a/src/main/java/com/example/spot/study/application/StudyQueryServiceImpl.java +++ b/src/main/java/com/example/spot/study/application/StudyQueryServiceImpl.java @@ -5,9 +5,9 @@ import com.example.spot.common.api.exception.handler.MemberHandler; import com.example.spot.common.api.exception.handler.StudyHandler; import com.example.spot.member.domain.Member; -import com.example.spot.study.domain.aggregate.Region; -import com.example.spot.study.domain.aggregate.StudyMember; -import com.example.spot.study.domain.aggregate.Theme; +import com.example.spot.study.domain.association.Region; +import com.example.spot.study.domain.association.StudyMember; +import com.example.spot.study.domain.association.Theme; import com.example.spot.study.domain.enums.StudyApplicationStatus; import com.example.spot.member.domain.enums.Gender; import com.example.spot.member.domain.enums.Status; @@ -17,8 +17,8 @@ import com.example.spot.member.domain.association.MemberTheme; import com.example.spot.member.domain.association.PreferredRegion; import com.example.spot.member.domain.association.PreferredStudy; -import com.example.spot.study.domain.aggregate.StudyRegion; -import com.example.spot.study.domain.aggregate.StudyTheme; +import com.example.spot.study.domain.association.StudyRegion; +import com.example.spot.study.domain.association.StudyTheme; import com.example.spot.study.domain.Study; import com.example.spot.member.domain.MemberRepository; import com.example.spot.study.domain.repository.StudyMemberRepository; diff --git a/src/main/java/com/example/spot/study/domain/Study.java b/src/main/java/com/example/spot/study/domain/Study.java index 330c67c9..33717be2 100644 --- a/src/main/java/com/example/spot/study/domain/Study.java +++ b/src/main/java/com/example/spot/study/domain/Study.java @@ -6,9 +6,9 @@ import com.example.spot.member.domain.enums.Status; import com.example.spot.schedule.domain.Schedule; import com.example.spot.story.domain.Story; -import com.example.spot.study.domain.aggregate.StudyMember; -import com.example.spot.study.domain.aggregate.StudyRegion; -import com.example.spot.study.domain.aggregate.StudyTheme; +import com.example.spot.study.domain.association.StudyMember; +import com.example.spot.study.domain.association.StudyRegion; +import com.example.spot.study.domain.association.StudyTheme; import com.example.spot.todo.domain.ToDo; import com.example.spot.vote.domain.Vote; import com.example.spot.study.domain.enums.StudyState; diff --git a/src/main/java/com/example/spot/study/domain/StudyRepositoryCustom.java b/src/main/java/com/example/spot/study/domain/StudyRepositoryCustom.java index 9ee345ba..90923ca7 100644 --- a/src/main/java/com/example/spot/study/domain/StudyRepositoryCustom.java +++ b/src/main/java/com/example/spot/study/domain/StudyRepositoryCustom.java @@ -1,10 +1,10 @@ package com.example.spot.study.domain; import com.example.spot.member.domain.enums.Status; -import com.example.spot.study.domain.aggregate.StudyMember; +import com.example.spot.study.domain.association.StudyMember; import com.example.spot.study.domain.enums.StudySortBy; -import com.example.spot.study.domain.aggregate.StudyRegion; -import com.example.spot.study.domain.aggregate.StudyTheme; +import com.example.spot.study.domain.association.StudyRegion; +import com.example.spot.study.domain.association.StudyTheme; import java.util.List; import java.util.Map; diff --git a/src/main/java/com/example/spot/study/domain/StudyRepositoryCustomImpl.java b/src/main/java/com/example/spot/study/domain/StudyRepositoryCustomImpl.java index 47479eb0..982ce92e 100644 --- a/src/main/java/com/example/spot/study/domain/StudyRepositoryCustomImpl.java +++ b/src/main/java/com/example/spot/study/domain/StudyRepositoryCustomImpl.java @@ -3,15 +3,15 @@ import static com.example.spot.study.domain.QStudy.*; -import com.example.spot.study.domain.aggregate.Region; -import com.example.spot.study.domain.aggregate.StudyRegion; +import com.example.spot.study.domain.association.Region; +import com.example.spot.study.domain.association.StudyRegion; import com.example.spot.member.domain.enums.Gender; import com.example.spot.member.domain.enums.Status; -import com.example.spot.study.domain.aggregate.StudyMember; +import com.example.spot.study.domain.association.StudyMember; import com.example.spot.study.domain.enums.StudySortBy; import com.example.spot.study.domain.enums.StudyState; import com.example.spot.study.domain.enums.ThemeType; -import com.example.spot.study.domain.aggregate.StudyTheme; +import com.example.spot.study.domain.association.StudyTheme; import com.querydsl.core.BooleanBuilder; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; diff --git a/src/main/java/com/example/spot/study/domain/aggregate/Region.java b/src/main/java/com/example/spot/study/domain/association/Region.java similarity index 96% rename from src/main/java/com/example/spot/study/domain/aggregate/Region.java rename to src/main/java/com/example/spot/study/domain/association/Region.java index ca10c1be..8941f1f3 100644 --- a/src/main/java/com/example/spot/study/domain/aggregate/Region.java +++ b/src/main/java/com/example/spot/study/domain/association/Region.java @@ -1,4 +1,4 @@ -package com.example.spot.study.domain.aggregate; +package com.example.spot.study.domain.association; import com.example.spot.common.entity.BaseEntity; import com.example.spot.member.domain.association.PreferredRegion; diff --git a/src/main/java/com/example/spot/study/domain/aggregate/StudyMember.java b/src/main/java/com/example/spot/study/domain/association/StudyMember.java similarity index 97% rename from src/main/java/com/example/spot/study/domain/aggregate/StudyMember.java rename to src/main/java/com/example/spot/study/domain/association/StudyMember.java index 835d07ba..6adb8e38 100644 --- a/src/main/java/com/example/spot/study/domain/aggregate/StudyMember.java +++ b/src/main/java/com/example/spot/study/domain/association/StudyMember.java @@ -1,4 +1,4 @@ -package com.example.spot.study.domain.aggregate; +package com.example.spot.study.domain.association; import com.example.spot.member.domain.Member; import com.example.spot.common.entity.BaseEntity; diff --git a/src/main/java/com/example/spot/study/domain/aggregate/StudyRegion.java b/src/main/java/com/example/spot/study/domain/association/StudyRegion.java similarity index 95% rename from src/main/java/com/example/spot/study/domain/aggregate/StudyRegion.java rename to src/main/java/com/example/spot/study/domain/association/StudyRegion.java index 167f0c50..39ae9c8a 100644 --- a/src/main/java/com/example/spot/study/domain/aggregate/StudyRegion.java +++ b/src/main/java/com/example/spot/study/domain/association/StudyRegion.java @@ -1,4 +1,4 @@ -package com.example.spot.study.domain.aggregate; +package com.example.spot.study.domain.association; import com.example.spot.common.entity.BaseEntity; import com.example.spot.study.domain.Study; diff --git a/src/main/java/com/example/spot/study/domain/aggregate/StudyTheme.java b/src/main/java/com/example/spot/study/domain/association/StudyTheme.java similarity index 95% rename from src/main/java/com/example/spot/study/domain/aggregate/StudyTheme.java rename to src/main/java/com/example/spot/study/domain/association/StudyTheme.java index 2f91d062..c70f71c3 100644 --- a/src/main/java/com/example/spot/study/domain/aggregate/StudyTheme.java +++ b/src/main/java/com/example/spot/study/domain/association/StudyTheme.java @@ -1,4 +1,4 @@ -package com.example.spot.study.domain.aggregate; +package com.example.spot.study.domain.association; import com.example.spot.common.entity.BaseEntity; import com.example.spot.study.domain.Study; diff --git a/src/main/java/com/example/spot/study/domain/aggregate/Theme.java b/src/main/java/com/example/spot/study/domain/association/Theme.java similarity index 97% rename from src/main/java/com/example/spot/study/domain/aggregate/Theme.java rename to src/main/java/com/example/spot/study/domain/association/Theme.java index 4e295dc6..9ee98da8 100644 --- a/src/main/java/com/example/spot/study/domain/aggregate/Theme.java +++ b/src/main/java/com/example/spot/study/domain/association/Theme.java @@ -1,4 +1,4 @@ -package com.example.spot.study.domain.aggregate; +package com.example.spot.study.domain.association; import com.example.spot.common.entity.BaseEntity; import com.example.spot.study.domain.enums.ThemeType; diff --git a/src/main/java/com/example/spot/study/domain/repository/RegionRepository.java b/src/main/java/com/example/spot/study/domain/repository/RegionRepository.java index 80054ece..03be7a95 100644 --- a/src/main/java/com/example/spot/study/domain/repository/RegionRepository.java +++ b/src/main/java/com/example/spot/study/domain/repository/RegionRepository.java @@ -1,6 +1,6 @@ package com.example.spot.study.domain.repository; -import com.example.spot.study.domain.aggregate.Region; +import com.example.spot.study.domain.association.Region; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/spot/study/domain/repository/StudyMemberRepository.java b/src/main/java/com/example/spot/study/domain/repository/StudyMemberRepository.java index d5f2b922..1ef69da4 100644 --- a/src/main/java/com/example/spot/study/domain/repository/StudyMemberRepository.java +++ b/src/main/java/com/example/spot/study/domain/repository/StudyMemberRepository.java @@ -1,6 +1,6 @@ package com.example.spot.study.domain.repository; -import com.example.spot.study.domain.aggregate.StudyMember; +import com.example.spot.study.domain.association.StudyMember; import com.example.spot.study.domain.enums.StudyApplicationStatus; import com.example.spot.member.domain.enums.Status; diff --git a/src/main/java/com/example/spot/study/domain/repository/StudyRegionRepository.java b/src/main/java/com/example/spot/study/domain/repository/StudyRegionRepository.java index 82955bf8..10da169a 100644 --- a/src/main/java/com/example/spot/study/domain/repository/StudyRegionRepository.java +++ b/src/main/java/com/example/spot/study/domain/repository/StudyRegionRepository.java @@ -2,8 +2,8 @@ import java.util.List; -import com.example.spot.study.domain.aggregate.Region; -import com.example.spot.study.domain.aggregate.StudyRegion; +import com.example.spot.study.domain.association.Region; +import com.example.spot.study.domain.association.StudyRegion; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/spot/study/domain/repository/StudyThemeRepository.java b/src/main/java/com/example/spot/study/domain/repository/StudyThemeRepository.java index 6f4088fe..bf7e7dbe 100644 --- a/src/main/java/com/example/spot/study/domain/repository/StudyThemeRepository.java +++ b/src/main/java/com/example/spot/study/domain/repository/StudyThemeRepository.java @@ -2,8 +2,8 @@ import java.util.List; -import com.example.spot.study.domain.aggregate.StudyTheme; -import com.example.spot.study.domain.aggregate.Theme; +import com.example.spot.study.domain.association.StudyTheme; +import com.example.spot.study.domain.association.Theme; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/spot/study/domain/repository/ThemeRepository.java b/src/main/java/com/example/spot/study/domain/repository/ThemeRepository.java index f5ef6c60..ad072298 100644 --- a/src/main/java/com/example/spot/study/domain/repository/ThemeRepository.java +++ b/src/main/java/com/example/spot/study/domain/repository/ThemeRepository.java @@ -1,6 +1,6 @@ package com.example.spot.study.domain.repository; -import com.example.spot.study.domain.aggregate.Theme; +import com.example.spot.study.domain.association.Theme; import com.example.spot.study.domain.enums.ThemeType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/spot/common/presentation/validator/ExistRegion.java b/src/main/java/com/example/spot/study/domain/validation/annotation/ExistRegion.java similarity index 82% rename from src/main/java/com/example/spot/common/presentation/validator/ExistRegion.java rename to src/main/java/com/example/spot/study/domain/validation/annotation/ExistRegion.java index 1d85b8af..a9911863 100644 --- a/src/main/java/com/example/spot/common/presentation/validator/ExistRegion.java +++ b/src/main/java/com/example/spot/study/domain/validation/annotation/ExistRegion.java @@ -1,6 +1,6 @@ -package com.example.spot.common.presentation.validator; +package com.example.spot.study.domain.validation.annotation; -import com.example.spot.common.infrastructure.ExistRegionValidator; +import com.example.spot.study.domain.validation.validator.ExistRegionValidator; import jakarta.validation.Constraint; import jakarta.validation.Payload; diff --git a/src/main/java/com/example/spot/common/infrastructure/ExistRegionValidator.java b/src/main/java/com/example/spot/study/domain/validation/validator/ExistRegionValidator.java similarity index 91% rename from src/main/java/com/example/spot/common/infrastructure/ExistRegionValidator.java rename to src/main/java/com/example/spot/study/domain/validation/validator/ExistRegionValidator.java index b4acbb5a..4c6410e3 100644 --- a/src/main/java/com/example/spot/common/infrastructure/ExistRegionValidator.java +++ b/src/main/java/com/example/spot/study/domain/validation/validator/ExistRegionValidator.java @@ -1,8 +1,8 @@ -package com.example.spot.common.infrastructure; +package com.example.spot.study.domain.validation.validator; import com.example.spot.common.api.code.status.ErrorStatus; import com.example.spot.study.domain.repository.RegionRepository; -import com.example.spot.common.presentation.validator.ExistRegion; +import com.example.spot.study.domain.validation.annotation.ExistRegion; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; diff --git a/src/main/java/com/example/spot/study/presentation/controller/MemberStudyController.java b/src/main/java/com/example/spot/study/presentation/controller/MemberStudyController.java new file mode 100644 index 00000000..22efb976 --- /dev/null +++ b/src/main/java/com/example/spot/study/presentation/controller/MemberStudyController.java @@ -0,0 +1,206 @@ +package com.example.spot.study.presentation.controller; + +import com.example.spot.common.api.ApiResponse; +import com.example.spot.common.api.code.status.SuccessStatus; +import com.example.spot.study.application.StudyMemberCommandService; +import com.example.spot.study.application.StudyMemberQueryService; +import com.example.spot.member.domain.validation.annotation.ExistMember; +import com.example.spot.study.domain.validation.annotation.ExistStudy; +import com.example.spot.story.domain.validation.annotation.ExistStory; +import com.example.spot.vote.domain.validation.annotation.ExistVote; +import com.example.spot.common.presentation.validator.TextLength; +import com.example.spot.study.presentation.dto.request.StudyHostWithdrawRequestDTO; +import com.example.spot.study.presentation.dto.request.StudyMemberReportDTO; +import com.example.spot.vote.presentation.dto.request.StudyVoteRequestDTO; +import com.example.spot.study.presentation.dto.response.StudyImageResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyMemberResDTO; +import com.example.spot.story.web.dto.response.StoryResDTO; +import com.example.spot.study.presentation.dto.response.StudyTerminationResponseDTO; +import com.example.spot.vote.presentation.dto.response.StudyVoteResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyWithdrawalResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyApplyResponseDTO; +import com.example.spot.story.web.dto.response.StoryResponseDTO; +import com.example.spot.member.presentation.dto.MemberResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO.StudyApplicantDTO; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/spot") +@Validated +public class MemberStudyController { + + private final StudyMemberQueryService studyMemberQueryService; + private final StudyMemberCommandService studyMemberCommandService; + + /* ----------------------------- 진행중인 스터디 관련 API ------------------------------------- */ + + + @Tag(name = "진행중인 스터디") + @Operation(summary = "[진행중인 스터디] 스터디 탈퇴하기", description = """ + ## [진행중인 스터디] 마이페이지 > 진행중 > 진행중인 스터디의 메뉴 클릭, 로그인한 회원이 현재 진행중인 스터디에서 탈퇴합니다. + 로그인한 회원이 참여하는 특정 스터디에 대해 member_study 튜플을 삭제합니다. + """) + @DeleteMapping("/studies/{studyId}/withdrawal") + public ApiResponse withdrawFromStudy(@PathVariable Long studyId) { + StudyWithdrawalResponseDTO.WithdrawalDTO withdrawalDTO = studyMemberCommandService.withdrawFromStudy(studyId); + return ApiResponse.onSuccess(SuccessStatus._STUDY_MEMBER_DELETED, withdrawalDTO); + } + + @Tag(name = "진행중인 스터디") + @Operation(summary = "[진행중인 스터디] 스터디 호스트 탈퇴", + description = """ + ## [진행중인 스터디] 특정 스터디의 호스트가 해당 스터디에서 탈퇴합니다. + 탈퇴 시, 호스트 권한이 회수되며 스터디에서 제외됩니다. + 요청 시, 새로운 호스트의 아이디와 임명 사유를 입력해야 합니다. + """) + @DeleteMapping("/studies/{studyId}/hosts/withdrawal") + public ApiResponse withdrawHostFromStudy( + @PathVariable Long studyId, + @RequestBody StudyHostWithdrawRequestDTO requestDTO) { + StudyWithdrawalResponseDTO.WithdrawalDTO withdrawalDTO = + studyMemberCommandService.withdrawHostFromStudy(studyId, requestDTO); + return ApiResponse.onSuccess(SuccessStatus._STUDY_MEMBER_DELETED, withdrawalDTO); + } + + + @Tag(name = "진행중인 스터디") + @Operation(summary = "[진행중인 스터디] 스터디 끝내기", description = """ + ## [진행중인 스터디] 마이페이지 > 진행중 > 진행중인 스터디의 메뉴 클릭, 로그인한 회원이 운영중인 스터디를 끝냅니다. + * 로그인한 회원이 운영하는 특정 스터디에 대해 study status OFF로 전환합니다. + * 스터디 성과를 입력받아 DB에 저장합니다. + """) + @PatchMapping("/studies/{studyId}/termination") + public ApiResponse terminateStudy( + @PathVariable @ExistStudy Long studyId, + @RequestParam @TextLength(min = 1, max = 30) String performance + ) { + StudyTerminationResponseDTO.TerminationDTO terminationDTO = studyMemberCommandService.terminateStudy(studyId, performance); + return ApiResponse.onSuccess(SuccessStatus._STUDY_TERMINATED, terminationDTO); + } + + + /* ----------------------------- 모집중인 스터디 관련 API ------------------------------------- */ + + @Tag(name = "모집중인 스터디") + @Operation(summary = "[모집중인 스터디] 스터디 별 신청 여부 조회하기", description = """ + ## [모집중인 스터디] 로그인한 회원이 모집중인 스터디에 대해 신청 여부를 조회합니다. + 로그인한 회원이 참여하는 특정 스터디에 대해 member_study의 application_status가 APPLIED인지 확인합니다. + 반환 값은 boolean으로, 신청 여부를 나타냅니다. + true: 신청한 상태, false: 신청하지 않은 상태 + """) + @GetMapping("/studies/{studyId}/is-applied") + @Parameter(name = "studyId", description = "모집중인 스터디의 ID를 입력 받습니다.", required = true) + public ApiResponse getIsApplied(@PathVariable @ExistStudy Long studyId) { + return ApiResponse.onSuccess(SuccessStatus._STUDY_APPLICANT_FOUND, + studyMemberQueryService.isApplied(studyId)); + } + + @Tag(name = "모집중인 스터디") + @Operation(summary = "[모집중인 스터디] 스터디별 신청 회원 목록 불러오기", description = """ + ## [모집중인 스터디] 마이페이지 > 모집중 > 스터디 클릭, 로그인한 회원이 모집중인 스터디에 신청한 회원 목록을 불러옵니다. + 로그인한 회원이 모집중인 특정 스터디에 대해 member_study의 application_status가 APPLIED인 회원 목록이 반환됩니다. + """) + @GetMapping("/studies/{studyId}/applicants") + @Parameter(name = "studyId", description = "모집중인 스터디의 ID를 입력 받습니다.", required = true) + public ApiResponse getAllApplicants(@PathVariable @ExistStudy Long studyId) { + return ApiResponse.onSuccess(SuccessStatus._STUDY_APPLICANT_FOUND, + studyMemberQueryService.findStudyApplicants(studyId)); + } + + @Tag(name = "모집중인 스터디") + @Operation(summary = "[모집중인 스터디] 스터디 신청 정보(이름, 자기소개) 불러오기", description = """ + ## [모집중인 스터디] 마이페이지 > 모집중 > 스터디 > 신청 회원 클릭, 로그인한 회원이 모집중인 스터디에 신청한 회원의 정보를 불러옵니다. + 로그인한 회원이 모집중인 특정 스터디에 신청한 회원의 정보(member.name & member_study.introduction)가 반환됩니다. + """) + @GetMapping("/studies/{studyId}/applicants/{applicantId}") + @Parameter(name = "studyId", description = "모집중인 스터디의 ID를 입력 받습니다.", required = true) + @Parameter(name = "applicantId", description = "신청자의 ID를 입력 받습니다.", required = true) + public ApiResponse getApplicantInfo( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistMember Long applicantId) { + return ApiResponse.onSuccess(SuccessStatus._STUDY_APPLICANT_FOUND, + studyMemberQueryService.findStudyApplication(studyId, applicantId)); + } + + @Tag(name = "모집중인 스터디") + @Operation(summary = "[모집중인 스터디] 스터디 신청 처리하기", description = """ + ## [모집중인 스터디] 마이페이지 > 모집중 > 스터디 > 신청 회원 > 거절 클릭, 로그인한 회원이 모집중인 스터디에 신청한 회원을 처리합니다. + isAccept가 true인 경우 member_study에서 application_status를 AWAITING_SELF_APPROVAL 수정합니다. -> 참가 희망하는 회원이 알림을 통해 스스로 승인 해야 스터디 참여가 완료됩니다. + isAccept가 false인 경우 member_study에서 application_status를 REJECTED로 수정합니다. + 스터디 신청 처리 결과를 응답으로 반환합니다. + """) + @PostMapping("/studies/{studyId}/applicants/{applicantId}") + @Parameter(name = "studyId", description = "모집중인 스터디의 ID를 입력 받습니다.", required = true) + @Parameter(name = "applicantId", description = "신청자의 ID를 입력 받습니다.", required = true) + public ApiResponse rejectApplicant( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistMember Long applicantId, + @RequestParam boolean isAccept) { + return ApiResponse.onSuccess(SuccessStatus._STUDY_APPLICANT_UPDATED, + studyMemberCommandService.acceptAndRejectStudyApply(applicantId, studyId, isAccept)); + } + + @Tag(name = "테스트 용 API", description = "테스트 용 API") + @Operation(summary = "!테스트 용! [모집중인 스터디] 스터디 신청 처리하기", description = """ + ## [모집중인 스터디] 빠른 API 적용를 위해 스터디 신청 처리를 즉시 수행합니다. + 위 API와 동일한 기능을 수행하지만, 실제 알림이 발송되지 않습니다. + 또한 스터디 신청 승인 시, 회원의 상태가 AWAITING_SELF_APPROVAL가 아닌 APPROVED로 변경됩니다. + + 즉, 바로 스터디 참여가 완료됩니다. 스터디 회원 조회 시, 바로 승인된 회원으로 조회됩니다. + + isAccept가 true인 경우 member_study에서 application_status를 APPROVED로 수정합니다. + isAccept가 false인 경우 member_study에서 application_status를 REJECTED로 수정합니다. + 스터디 신청 처리 결과를 응답으로 반환합니다. + """) + @PostMapping("/studies/{studyId}/applicants/{applicantId}/test") + @Parameter(name = "studyId", description = "모집중인 스터디의 ID를 입력 받습니다.", required = true) + @Parameter(name = "applicantId", description = "신청자의 ID를 입력 받습니다.", required = true) + public ApiResponse rejectApplicantForTest( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistMember Long applicantId, + @RequestParam boolean isAccept) { + return ApiResponse.onSuccess(SuccessStatus._STUDY_APPLICANT_UPDATED, + studyMemberCommandService.acceptAndRejectStudyApplyForTest(applicantId, studyId, isAccept)); + } + + + /* ----------------------------- 스터디 상세 정보 관련 API ------------------------------------- */ + + @Tag(name = "스터디 상세 정보") + @Operation(summary = "[스터디 상세 정보] 스터디에 참여하는 회원 목록 불러오기", description = """ + ## [스터디 상세 정보] 로그인한 회원이 참여하는 특정 스터디의 회원 목록을 전체 조회 합니다. + member_study에서 application_status=APPROVED인 회원의 목록(이름, 프로필 사진 포함)이 반환됩니다. + """) + @GetMapping("/studies/{studyId}/members") + public ApiResponse getStudyMembers( + @PathVariable @ExistStudy Long studyId) { + StudyMemberResponseDTO studyMemberResponseDTO = studyMemberQueryService.findStudyMembers(studyId); + return ApiResponse.onSuccess(SuccessStatus._STUDY_MEMBER_FOUND, studyMemberResponseDTO); + } + + @Tag(name = "스터디 상세 정보") + @Operation(summary = "[스터디 상세 정보] 스터디 호스트 정보 불러오기", description = """ + ## [스터디 상세 정보] 로그인한 회원이 참여하는 특정 스터디의 호스트 정보를 조회합니다. + * isOwned : 로그인한 회원이 호스트인지 true or false로 반환 + * host : 호스트의 id와 nickname 반환 + """) + @GetMapping("/studies/{studyId}/host") + public ApiResponse getStudyHost( + @PathVariable @ExistStudy Long studyId) { + StudyMemberResDTO.StudyHostDTO studyHostDTO = studyMemberQueryService.getStudyHost(studyId); + return ApiResponse.onSuccess(SuccessStatus._STUDY_HOST_FOUND, studyHostDTO); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/spot/study/presentation/controller/StudyMemberController.java b/src/main/java/com/example/spot/study/presentation/controller/StudyMemberController.java deleted file mode 100644 index 50c976f3..00000000 --- a/src/main/java/com/example/spot/study/presentation/controller/StudyMemberController.java +++ /dev/null @@ -1,689 +0,0 @@ -package com.example.spot.study.presentation.controller; - -import com.example.spot.common.api.ApiResponse; -import com.example.spot.common.api.code.status.SuccessStatus; -import com.example.spot.study.application.MemberStudyCommandService; -import com.example.spot.study.application.MemberStudyQueryService; -import com.example.spot.member.domain.validation.annotation.ExistMember; -import com.example.spot.schedule.domain.validation.annotation.ExistSchedule; -import com.example.spot.study.domain.validation.annotation.ExistStudy; -import com.example.spot.story.domain.validation.annotation.ExistStory; -import com.example.spot.todo.domain.validation.annotation.ExistToDo; -import com.example.spot.vote.domain.validation.annotation.ExistVote; -import com.example.spot.common.presentation.validator.IntSize; -import com.example.spot.common.presentation.validator.TextLength; -import com.example.spot.study.presentation.dto.request.ScheduleRequestDTO; -import com.example.spot.study.presentation.dto.request.StudyHostWithdrawRequestDTO; -import com.example.spot.study.presentation.dto.request.StudyMemberReportDTO; -import com.example.spot.study.presentation.dto.request.StudyQuizRequestDTO; -import com.example.spot.study.presentation.dto.request.StudyVoteRequestDTO; -import com.example.spot.study.presentation.dto.response.ScheduleResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyImageResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyMemberResDTO; -import com.example.spot.study.presentation.dto.response.StudyPostResDTO; -import com.example.spot.study.presentation.dto.response.StudyQuizResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyTerminationResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyVoteResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyWithdrawalResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyApplyResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyPostResponseDTO; -import com.example.spot.member.presentation.dto.MemberResponseDTO; -import com.example.spot.study.presentation.dto.request.ToDoListRequestDTO; -import com.example.spot.study.presentation.dto.response.ToDoListResponseDTO.ToDoListCreateResponseDTO; -import com.example.spot.study.presentation.dto.response.ToDoListResponseDTO.ToDoListSearchResponseDTO; -import com.example.spot.study.presentation.dto.response.ToDoListResponseDTO.ToDoListUpdateResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO.StudyApplicantDTO; -import com.example.spot.study.presentation.dto.response.StudyScheduleResponseDTO; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import jakarta.validation.constraints.Min; -import java.time.LocalDate; - -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/spot") -@Validated -public class StudyMemberController { - - private final MemberStudyQueryService memberStudyQueryService; - private final MemberStudyCommandService memberStudyCommandService; - -/* ----------------------------- 진행중인 스터디 관련 API ------------------------------------- */ - - - @Tag(name = "진행중인 스터디") - @Operation(summary = "[진행중인 스터디] 스터디 탈퇴하기", description = """ - ## [진행중인 스터디] 마이페이지 > 진행중 > 진행중인 스터디의 메뉴 클릭, 로그인한 회원이 현재 진행중인 스터디에서 탈퇴합니다. - 로그인한 회원이 참여하는 특정 스터디에 대해 member_study 튜플을 삭제합니다. - """) - @DeleteMapping("/studies/{studyId}/withdrawal") - public ApiResponse withdrawFromStudy(@PathVariable Long studyId) { - StudyWithdrawalResponseDTO.WithdrawalDTO withdrawalDTO = memberStudyCommandService.withdrawFromStudy(studyId); - return ApiResponse.onSuccess(SuccessStatus._STUDY_MEMBER_DELETED, withdrawalDTO); - } - - @Tag(name = "진행중인 스터디") - @Operation(summary = "[진행중인 스터디] 스터디 호스트 탈퇴", - description = """ - ## [진행중인 스터디] 특정 스터디의 호스트가 해당 스터디에서 탈퇴합니다. - 탈퇴 시, 호스트 권한이 회수되며 스터디에서 제외됩니다. - 요청 시, 새로운 호스트의 아이디와 임명 사유를 입력해야 합니다. - """) - @DeleteMapping("/studies/{studyId}/hosts/withdrawal") - public ApiResponse withdrawHostFromStudy( - @PathVariable Long studyId, - @RequestBody StudyHostWithdrawRequestDTO requestDTO) { - StudyWithdrawalResponseDTO.WithdrawalDTO withdrawalDTO = - memberStudyCommandService.withdrawHostFromStudy(studyId, requestDTO); - return ApiResponse.onSuccess(SuccessStatus._STUDY_MEMBER_DELETED, withdrawalDTO); - } - - - @Tag(name = "진행중인 스터디") - @Operation(summary = "[진행중인 스터디] 스터디 끝내기", description = """ - ## [진행중인 스터디] 마이페이지 > 진행중 > 진행중인 스터디의 메뉴 클릭, 로그인한 회원이 운영중인 스터디를 끝냅니다. - * 로그인한 회원이 운영하는 특정 스터디에 대해 study status OFF로 전환합니다. - * 스터디 성과를 입력받아 DB에 저장합니다. - """) - @PatchMapping("/studies/{studyId}/termination") - public ApiResponse terminateStudy( - @PathVariable @ExistStudy Long studyId, - @RequestParam @TextLength(min=1, max=30) String performance - ) { - StudyTerminationResponseDTO.TerminationDTO terminationDTO = memberStudyCommandService.terminateStudy(studyId, performance); - return ApiResponse.onSuccess(SuccessStatus._STUDY_TERMINATED, terminationDTO); - } - - -/* ----------------------------- 모집중인 스터디 관련 API ------------------------------------- */ - - @Tag(name = "모집중인 스터디") - @Operation(summary = "[모집중인 스터디] 스터디 별 신청 여부 조회하기", description = """ - ## [모집중인 스터디] 로그인한 회원이 모집중인 스터디에 대해 신청 여부를 조회합니다. - 로그인한 회원이 참여하는 특정 스터디에 대해 member_study의 application_status가 APPLIED인지 확인합니다. - 반환 값은 boolean으로, 신청 여부를 나타냅니다. - true: 신청한 상태, false: 신청하지 않은 상태 - """) - @GetMapping("/studies/{studyId}/is-applied") - @Parameter(name = "studyId", description = "모집중인 스터디의 ID를 입력 받습니다.", required = true) - public ApiResponse getIsApplied(@PathVariable @ExistStudy Long studyId) { - return ApiResponse.onSuccess(SuccessStatus._STUDY_APPLICANT_FOUND, - memberStudyQueryService.isApplied(studyId)); - } - - @Tag(name = "모집중인 스터디") - @Operation(summary = "[모집중인 스터디] 스터디별 신청 회원 목록 불러오기", description = """ - ## [모집중인 스터디] 마이페이지 > 모집중 > 스터디 클릭, 로그인한 회원이 모집중인 스터디에 신청한 회원 목록을 불러옵니다. - 로그인한 회원이 모집중인 특정 스터디에 대해 member_study의 application_status가 APPLIED인 회원 목록이 반환됩니다. - """) - @GetMapping("/studies/{studyId}/applicants") - @Parameter(name = "studyId", description = "모집중인 스터디의 ID를 입력 받습니다.", required = true) - public ApiResponse getAllApplicants(@PathVariable @ExistStudy Long studyId) { - return ApiResponse.onSuccess(SuccessStatus._STUDY_APPLICANT_FOUND, - memberStudyQueryService.findStudyApplicants(studyId)); - } - @Tag(name = "모집중인 스터디") - @Operation(summary = "[모집중인 스터디] 스터디 신청 정보(이름, 자기소개) 불러오기", description = """ - ## [모집중인 스터디] 마이페이지 > 모집중 > 스터디 > 신청 회원 클릭, 로그인한 회원이 모집중인 스터디에 신청한 회원의 정보를 불러옵니다. - 로그인한 회원이 모집중인 특정 스터디에 신청한 회원의 정보(member.name & member_study.introduction)가 반환됩니다. - """) - @GetMapping("/studies/{studyId}/applicants/{applicantId}") - @Parameter(name = "studyId", description = "모집중인 스터디의 ID를 입력 받습니다.", required = true) - @Parameter(name = "applicantId", description = "신청자의 ID를 입력 받습니다.", required = true) - public ApiResponse getApplicantInfo( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistMember Long applicantId) { - return ApiResponse.onSuccess(SuccessStatus._STUDY_APPLICANT_FOUND, - memberStudyQueryService.findStudyApplication(studyId, applicantId)); - } - @Tag(name = "모집중인 스터디") - @Operation(summary = "[모집중인 스터디] 스터디 신청 처리하기", description = """ - ## [모집중인 스터디] 마이페이지 > 모집중 > 스터디 > 신청 회원 > 거절 클릭, 로그인한 회원이 모집중인 스터디에 신청한 회원을 처리합니다. - isAccept가 true인 경우 member_study에서 application_status를 AWAITING_SELF_APPROVAL 수정합니다. -> 참가 희망하는 회원이 알림을 통해 스스로 승인 해야 스터디 참여가 완료됩니다. - isAccept가 false인 경우 member_study에서 application_status를 REJECTED로 수정합니다. - 스터디 신청 처리 결과를 응답으로 반환합니다. - """) - @PostMapping("/studies/{studyId}/applicants/{applicantId}") - @Parameter(name = "studyId", description = "모집중인 스터디의 ID를 입력 받습니다.", required = true) - @Parameter(name = "applicantId", description = "신청자의 ID를 입력 받습니다.", required = true) - public ApiResponse rejectApplicant( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistMember Long applicantId, - @RequestParam boolean isAccept) { - return ApiResponse.onSuccess(SuccessStatus._STUDY_APPLICANT_UPDATED, - memberStudyCommandService.acceptAndRejectStudyApply(applicantId, studyId, isAccept)); - } - - @Tag(name = "테스트 용 API", description = "테스트 용 API") - @Operation(summary = "!테스트 용! [모집중인 스터디] 스터디 신청 처리하기", description = """ - ## [모집중인 스터디] 빠른 API 적용를 위해 스터디 신청 처리를 즉시 수행합니다. - 위 API와 동일한 기능을 수행하지만, 실제 알림이 발송되지 않습니다. - 또한 스터디 신청 승인 시, 회원의 상태가 AWAITING_SELF_APPROVAL가 아닌 APPROVED로 변경됩니다. - - 즉, 바로 스터디 참여가 완료됩니다. 스터디 회원 조회 시, 바로 승인된 회원으로 조회됩니다. - - isAccept가 true인 경우 member_study에서 application_status를 APPROVED로 수정합니다. - isAccept가 false인 경우 member_study에서 application_status를 REJECTED로 수정합니다. - 스터디 신청 처리 결과를 응답으로 반환합니다. - """) - @PostMapping("/studies/{studyId}/applicants/{applicantId}/test") - @Parameter(name = "studyId", description = "모집중인 스터디의 ID를 입력 받습니다.", required = true) - @Parameter(name = "applicantId", description = "신청자의 ID를 입력 받습니다.", required = true) - public ApiResponse rejectApplicantForTest( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistMember Long applicantId, - @RequestParam boolean isAccept) { - return ApiResponse.onSuccess(SuccessStatus._STUDY_APPLICANT_UPDATED, - memberStudyCommandService.acceptAndRejectStudyApplyForTest(applicantId, studyId, isAccept)); - } - - -/* ----------------------------- 스터디 상세 정보 관련 API ------------------------------------- */ - - @Tag(name = "스터디 상세 정보") - @Operation(summary = "[스터디 상세 정보] 스터디 최근 공지 1개 불러오기", description = """ - ## [스터디 상세 정보] 내 스터디 > 스터디 클릭, 로그인한 회원이 참여하는 특정 스터디의 최근 공지 1개를 불러옵니다. - study_post의 announced_at이 가장 최근인 공지 1개가 반환됩니다. - """) - @GetMapping("/studies/{studyId}/announce") - public ApiResponse getRecentAnnouncement(@PathVariable @ExistStudy Long studyId) { - StudyPostResponseDTO studyPostResponseDTO = memberStudyQueryService.findStudyAnnouncementPost(studyId); - return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_FOUND, studyPostResponseDTO); - } - @Tag(name = "스터디 상세 정보") - @Operation(summary = "[스터디 상세 정보] 다가오는 모임 목록 불러오기", description = """ - ## [스터디 상세 정보] 내 스터디 > 스터디 클릭, 로그인한 회원이 참여하는 특정 스터디의 다가오는 모임 목록을 페이징 조회 합니다. - 현재 시점 이후에 진행되는 모임 일정의 목록을 schedule에서 반환합니다. - """) - @GetMapping("/studies/{studyId}/upcoming-schedules") - public ApiResponse getUpcomingSchedules( - @PathVariable @ExistStudy Long studyId, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "1") int size){ - StudyScheduleResponseDTO studyScheduleResponseDTO = memberStudyQueryService.findStudySchedule(studyId, PageRequest.of(page, size)); - return ApiResponse.onSuccess(SuccessStatus._STUDY_SCHEDULE_FOUND, studyScheduleResponseDTO); - } - @Tag(name = "스터디 상세 정보") - @Operation(summary = "[스터디 상세 정보] 스터디에 참여하는 회원 목록 불러오기", description = """ - ## [스터디 상세 정보] 로그인한 회원이 참여하는 특정 스터디의 회원 목록을 전체 조회 합니다. - member_study에서 application_status=APPROVED인 회원의 목록(이름, 프로필 사진 포함)이 반환됩니다. - """) - @GetMapping("/studies/{studyId}/members") - public ApiResponse getStudyMembers( - @PathVariable @ExistStudy Long studyId){ - StudyMemberResponseDTO studyMemberResponseDTO = memberStudyQueryService.findStudyMembers(studyId); - return ApiResponse.onSuccess(SuccessStatus._STUDY_MEMBER_FOUND, studyMemberResponseDTO); - } - - @Tag(name = "스터디 상세 정보") - @Operation(summary = "[스터디 상세 정보] 스터디 호스트 정보 불러오기", description = """ - ## [스터디 상세 정보] 로그인한 회원이 참여하는 특정 스터디의 호스트 정보를 조회합니다. - * isOwned : 로그인한 회원이 호스트인지 true or false로 반환 - * host : 호스트의 id와 nickname 반환 - """) - @GetMapping("/studies/{studyId}/host") - public ApiResponse getStudyHost( - @PathVariable @ExistStudy Long studyId) - { - StudyMemberResDTO.StudyHostDTO studyHostDTO = memberStudyQueryService.getStudyHost(studyId); - return ApiResponse.onSuccess(SuccessStatus._STUDY_HOST_FOUND, studyHostDTO); - } - - -/* ----------------------------- 스터디 일정 관련 API ------------------------------------- */ - - @Tag(name = "스터디 일정") - @Operation(summary = "[스터디 일정] 월별 일정 불러오기", description = """ - ## [스터디 일정] 내 스터디 > 스터디 > 캘린더 클릭, 로그인한 회원이 참여하는 특정 스터디의 일정을 월 단위로 불러옵니다. - 처음 캘린더를 클릭하면 오늘 날짜가 포함된 연/월에 해당하는 일정 목록이 schedule에서 반환됩니다. - 캘린더를 넘기면 해당 연/월에 해당하는 일정 목록이 schedule에서 반환됩니다. - """) - @Parameter(name = "studyId", description = "일정을 불러올 스터디의 id를 입력합니다.", required = true) - @GetMapping("/studies/{studyId}/schedules") - public ApiResponse getMonthlySchedules( - @PathVariable @ExistStudy Long studyId, - @RequestParam @IntSize(min = 1) Integer year, - @RequestParam @IntSize(min = 1, max= 12) Integer month) { - ScheduleResponseDTO.MonthlyScheduleListDTO monthlyScheduleDTO = memberStudyQueryService.getMonthlySchedules(studyId, year, month); - return ApiResponse.onSuccess(SuccessStatus._STUDY_SCHEDULE_FOUND, monthlyScheduleDTO); - } - - @Tag(name = "스터디 일정") - @Operation(summary = "[스터디 일정] 상세 일정 불러오기", description = """ - ## [스터디 일정] 내 스터디 > 스터디 > 캘린더 > 일정 클릭, 로그인한 회원이 참여하는 특정 스터디의 상세 일정을 불러옵니다. - 스터디의 일정 정보를 상세하게 불러옵니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "scheduleId", description = "불러올 스터디 일정의 id를 입력합니다.", required = true) - @GetMapping("/studies/{studyId}/schedules/{scheduleId}") - public ApiResponse getSchedule( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistSchedule Long scheduleId) { - ScheduleResponseDTO.MonthlyScheduleDTO scheduleDTO = memberStudyQueryService.getSchedule(studyId, scheduleId); - return ApiResponse.onSuccess(SuccessStatus._STUDY_SCHEDULE_FOUND, scheduleDTO); - } - - @Tag(name = "스터디 일정") - @Operation(summary = "[스터디 일정] 일정 추가하기", description = """ - ## [스터디 일정] 내 스터디 > 스터디 > 캘린더 > 추가 버튼 클릭, 로그인한 회원이 운영하는 특정 스터디에 일정을 추가합니다. - 로그인한 회원이 owner인 경우 schedule에 새로운 일정을 등록합니다. - - period에는 [NONE, DAILY, WEEKLY, BIWEEKLY, MONTHLY] 중 하나를 입력해야 합니다. - """) - @Parameter(name = "studyId", description = "일정을 추가할 스터디의 id를 입력합니다.", required = true) - @PostMapping("/studies/{studyId}/schedules") - public ApiResponse addSchedule( - @PathVariable @ExistStudy Long studyId, - @RequestBody @Valid ScheduleRequestDTO.ScheduleDTO scheduleRequestDTO) { - ScheduleResponseDTO.ScheduleDTO scheduleResponseDTO = memberStudyCommandService.addSchedule(studyId, scheduleRequestDTO); - return ApiResponse.onSuccess(SuccessStatus._STUDY_SCHEDULE_CREATED, scheduleResponseDTO); - } - - @Tag(name = "스터디 일정") - @Operation(summary = "[스터디 일정] 일정 변경하기", description = """ - ## [스터디 일정] 내 스터디 > 스터디 > 캘린더 > 일정 클릭, 로그인한 회원이 특정 스터디에 등록한 일정을 수정합니다. - 로그인한 회원이 owner인 경우 schedule에 등록한 일정을 수정할 수 있습니다. - - period에는 [NONE, DAILY, WEEKLY, BIWEEKLY, MONTHLY] 중 하나를 입력해야 합니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "scheduleId", description = "변경할 일정의 id를 입력합니다.", required = true) - @PatchMapping("/studies/{studyId}/schedules/{scheduleId}") - public ApiResponse modSchedule( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistSchedule Long scheduleId, - @RequestBody @Valid ScheduleRequestDTO.ScheduleDTO scheduleModDTO) { - ScheduleResponseDTO.ScheduleDTO scheduleResponseDTO = memberStudyCommandService.modSchedule(studyId, scheduleId, scheduleModDTO); - return ApiResponse.onSuccess(SuccessStatus._STUDY_SCHEDULE_UPDATED, scheduleResponseDTO); - } - - -/* ----------------------------- 스터디 투표 관련 API ------------------------------------- */ - - @Tag(name = "스터디 투표") - @Operation(summary = "[스터디 투표] 투표 생성하기", description = """ - ## [스터디 투표] 내 스터디 > 스터디 > 투표 > 작성 버튼 클릭, 로그인한 회원이 참여하는 특정 스터디에서 새로운 투표를 등록합니다. - 스터디에 참여하는 회원이 생성한 투표를 vote에 저장합니다. - """) - @Parameter(name = "studyId", description = "투표를 생성할 스터디의 id를 입력합니다.", required = true) - @PostMapping("/studies/{studyId}/votes") - public ApiResponse createVote( - @PathVariable @ExistStudy Long studyId, - @RequestBody @Valid StudyVoteRequestDTO.VoteDTO voteDTO) { - StudyVoteResponseDTO.VotePreviewDTO votePreviewDTO = memberStudyCommandService.createVote(studyId, voteDTO); - return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_CREATED, votePreviewDTO); - } - - @Tag(name = "스터디 투표") - @Operation(summary = "[스터디 투표] 투표하기", description = """ - ## [스터디 투표] 내 스터디 > 스터디 > 투표 > 특정 투표 클릭, 로그인한 회원이 참여하는 스터디에서 특정 항목에 투표합니다. - member_vote에 투표 정보를 저장합니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "voteId", description = "참여할 스터디 투표의 id를 입력합니다.") - @PostMapping("/studies/{studyId}/votes/{voteId}/options") - public ApiResponse vote( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistVote Long voteId, - @RequestBody @Valid StudyVoteRequestDTO.VotedOptionDTO votedOptionDTO) { - StudyVoteResponseDTO.VotedOptionDTO votedOptionResDTO = memberStudyCommandService.vote(studyId, voteId, votedOptionDTO); - return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_PARTICIPATED, votedOptionResDTO); - } - - @Tag(name = "스터디 투표") - @Operation(summary = "[스터디 투표] 투표 편집하기", description = """ - ## [스터디 투표] 내 스터디 > 스터디 > 투표 > 편집하기 버튼 클릭, 로그인한 회원이 참여하는 특정 스터디에서 투표 정보를 수정합니다. - 스터디에 참여하는 회원이 생성한 투표를 vote에 저장합니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "voteId", description = "편집할 스터디 투표의 id를 입력합니다.") - @PatchMapping("/studies/{studyId}/votes/{voteId}") - public ApiResponse updateVote( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistVote Long voteId, - @RequestBody @Valid StudyVoteRequestDTO.VoteUpdateDTO voteDTO) { - StudyVoteResponseDTO.VotePreviewDTO votePreviewDTO = memberStudyCommandService.updateVote(studyId, voteId, voteDTO); - return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_UPDATED, votePreviewDTO); - } - - @Tag(name = "스터디 투표") - @Operation(summary = "[스터디 투표] 투표 삭제하기", description = """ - ## [스터디 투표] 내 스터디 > 스터디 > 투표 > 삭제하기 버튼 클릭, 로그인한 회원이 참여하는 특정 스터디에서 투표를 삭제합니다. - 스터디에 참여하는 회원이 생성한 투표를 vote에 저장합니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "voteId", description = "삭제할 스터디 투표의 id를 입력합니다.") - @DeleteMapping("/studies/{studyId}/votes/{voteId}") - public ApiResponse deleteVote( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistVote Long voteId) { - StudyVoteResponseDTO.VotePreviewDTO votePreviewDTO = memberStudyCommandService.deleteVote(studyId, voteId); - return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_DELETED, votePreviewDTO); - } - - @Tag(name = "스터디 투표") - @Operation(summary = "[스터디 투표] 투표 목록 불러오기", description = """ - ## [스터디 투표] 내 스터디 > 스터디 > 투표 클릭, 로그인한 회원이 참여하는 특정 스터디의 투표 목록을 불러옵니다. - 진행 중(finished_at 이전)인 투표 목록과 마감(finished_at 이후)된 투표 목록을 구분하여 반환합니다. - """) - @Parameter(name = "studyId", description = "투표 목록을 불러올 스터디의 id를 입력합니다.", required = true) - @GetMapping("/studies/{studyId}/votes") - public ApiResponse getAllVotes( - @PathVariable @ExistStudy Long studyId) { - StudyVoteResponseDTO.VoteListDTO voteListDTO = memberStudyQueryService.getAllVotes(studyId); - return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_FOUND, voteListDTO); - } - - @Tag(name = "스터디 투표") - @Operation(summary = "[스터디 투표] 투표 불러오기", description = """ - ## [스터디 투표] 내 스터디 > 스터디 > 투표 > 특정 투표 클릭, 로그인한 회원이 참여하는 특정 스터디의 투표를 불러옵니다. - 진행중인 투표 : 진행중인 투표에 대한 항목 및 기본 정보가 반환됩니다. - 마감된 투표 : 마감된 투표에 대한 항목과 투표 인원수가 반환됩니다. - (진행중인 투표인지 마감된 투표인지에 따라 Response DTO가 서로 다릅니다.) - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "voteId", description = "불러올 스터디 투표의 id를 입력합니다.") - @GetMapping("/studies/{studyId}/votes/{voteId}") - public ApiResponse getVote( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistVote Long voteId) { - Boolean isCompleted = memberStudyQueryService.getIsCompleted(voteId); - if (isCompleted) { - StudyVoteResponseDTO.CompletedVoteDTO completedVoteDTO = memberStudyQueryService.getVoteInCompletion(studyId, voteId); - return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_FOUND, completedVoteDTO); - } else { - StudyVoteResponseDTO.VoteDTO voteDTO = memberStudyQueryService.getVoteInProgress(studyId, voteId); - return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_FOUND, voteDTO); - } - } - - @Tag(name = "스터디 투표") - @Operation(summary = "[스터디 투표] 마감된 투표 현황 불러오기", description = """ - ## [스터디 투표] 내 스터디 > 스터디 > 투표 > 마감된 투표 > n명 참여 클릭, 로그인한 회원이 참여하는 특정 스터디의 투표를 불러옵니다. - 마감된 투표에 대하여 항목별 투표 회원 목록을 반환합니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "voteId", description = "마감된 스터디 투표의 id를 입력합니다.") - @GetMapping("/studies/{studyId}/votes/{voteId}/details") - public ApiResponse getCompletedVoteDetail( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistVote Long voteId) { - StudyVoteResponseDTO.CompletedVoteDetailDTO completedVoteDetailDTO = memberStudyQueryService.getCompletedVoteDetail(studyId, voteId); - return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_DETAIL_STATUS_FOUND, completedVoteDetailDTO); - } - -/* ----------------------------- 스터디 갤러리 관련 API ------------------------------------- */ - @Tag(name = "스터디 이미지") - @Operation(summary = "[스터디 갤러리] 스터디 이미지 목록 불러오기", description = """ - ## [스터디 갤러리] 내 스터디 > 스터디 > 갤러리 클릭, 로그인한 회원이 참여하는 스터디의 이미지 목록을 불러옵니다. - study_post에 존재하는 모든 게시글의 이미지를 최신순으로 반환합니다. - """) - @Parameter(name = "studyId", description = "이미지 목록을 불러올 스터디의 id를 입력합니다.", required = true) - @GetMapping("/studies/{studyId}/images") - public ApiResponse getAllStudyImages( - @PathVariable @ExistStudy Long studyId, - @RequestParam @Min(0) Integer offset, - @RequestParam @Min(1) Integer limit) { - StudyImageResponseDTO.ImageListDTO imageListDTO = memberStudyQueryService.getAllStudyImages(studyId, PageRequest.of(offset, limit)); - return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_IMAGES_FOUND, imageListDTO); - } - -/* ----------------------------- 스터디 출석체크 관련 API ------------------------------------- */ - - @Tag(name = "스터디 출석체크") - @Operation(summary = "[스터디 출석체크] 출석 퀴즈 생성하기", description = """ - ## [스터디 출석체크] 내 스터디 > 스터디 > 캘린더 > 출석체크 > 퀴즈 만들기 클릭, 로그인한 회원이 운영하는 스터디에 퀴즈를 생성합니다. - * 로그인한 회원이 스터디장인 경우 quiz에 새로운 퀴즈를 생성합니다. - * createdAt에는 출석 퀴즈를 생성할 날짜를 입력합니다. - """) - @Parameter(name = "studyId", description = "출석 퀴즈를 생성할 스터디의 id를 입력합니다.", required = true) - @Parameter(name = "scheduleId", description = "출석 퀴즈를 생성할 일정의 id를 입력합니다.", required = true) - @PostMapping("/studies/{studyId}/schedules/{scheduleId}/quiz") - public ApiResponse createAttendanceQuiz( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistSchedule Long scheduleId, - @RequestBody @Valid StudyQuizRequestDTO.QuizDTO quizRequestDTO) { - StudyQuizResponseDTO.QuizDTO quizResponseDTO = memberStudyCommandService.createAttendanceQuiz(studyId, scheduleId, quizRequestDTO); - return ApiResponse.onSuccess(SuccessStatus._STUDY_QUIZ_CREATED, quizResponseDTO); - } - - @Tag(name = "스터디 출석체크") - @Operation(summary = "[스터디 출석체크] 출석 퀴즈 불러오기", description = """ - ## [스터디 출석체크] 내 스터디 > 스터디 > 캘린더 > 출석체크, 로그인한 회원이 참여하는 스터디의 퀴즈를 불러옵니다. - * 날짜에 해당하는 퀴즈의 아이디와 질문이 반환됩니다. - * date에는 출석 퀴즈를 불러올 날짜를 입력합니다. - """) - @Parameter(name = "studyId", description = "출석 퀴즈를 불러올 스터디의 id를 입력합니다.", required = true) - @Parameter(name = "scheduleId", description = "출석 퀴즈를 불러올 일정의 id를 입력합니다.", required = true) - @GetMapping("/studies/{studyId}/schedules/{scheduleId}/quiz") - public ApiResponse getAttendanceQuiz( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistSchedule Long scheduleId, - @RequestParam LocalDate date) { - StudyQuizResponseDTO.QuizDTO quizDTO = memberStudyQueryService.getAttendanceQuiz(studyId, scheduleId, date); - return ApiResponse.onSuccess(SuccessStatus._STUDY_QUIZ_FOUND, quizDTO); - } - - - @Tag(name = "스터디 출석체크") - @Operation(summary = "[스터디 출석체크] 출석 체크하기", description = """ - ## [스터디 출석체크] 내 스터디 > 스터디 > 캘린더 > 이미지 클릭, 로그인한 회원이 참여하는 스터디에서 오늘의 퀴즈를 풀어 출석을 체크합니다. - * 특정 시점의 quiz에 대해 member_attendance 튜플을 추가합니다. - * dateTime에는 출석을 체크할 날짜와 시간을 입력합니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "scheduleId", description = "일정의 id를 입력합니다.", required = true) - @PostMapping("/studies/{studyId}/schedules/{scheduleId}/attendance") - public ApiResponse attendantStudy( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistSchedule Long scheduleId, - @RequestBody @Valid StudyQuizRequestDTO.AttendanceDTO attendanceRequestDTO) { - StudyQuizResponseDTO.AttendanceDTO attendanceResponseDTO = memberStudyCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO); - if (attendanceResponseDTO.getIsCorrect()) { - return ApiResponse.onSuccess(SuccessStatus._STUDY_ATTENDANCE_CREATED_CORRECT_ANSWER, attendanceResponseDTO); - } else { - return ApiResponse.onSuccess(SuccessStatus._STUDY_ATTENDANCE_CREATED_WRONG_ANSWER, attendanceResponseDTO); - } - } - - @Tag(name = "스터디 출석체크") - @Operation(summary = "[스터디 출석체크] 출석 퀴즈 삭제하기", description = """ - ## [스터디 출석체크] 기한이 지난 출석 퀴즈를 삭제합니다. (화면 X) - * PathVariable을 통해 전달받은 정보를 바탕으로 출석 퀴즈를 삭제합니다. - * 출석 퀴즈 정보와 함께 퀴즈에 대한 MemberAttendance(회원 출석) 목록도 함께 삭제됩니다. - * date에는 출석 퀴즈를 삭제할 날짜를 입력합니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "scheduleId", description = "일정의 id를 입력합니다.", required = true) - @DeleteMapping("/studies/{studyId}/schedules/{scheduleId}/quiz") - public ApiResponse deleteAttendanceQuiz( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistSchedule Long scheduleId, - @RequestParam LocalDate date) { - StudyQuizResponseDTO.QuizDTO quizDTO = memberStudyCommandService.deleteAttendanceQuiz(studyId, scheduleId, date); - return ApiResponse.onSuccess(SuccessStatus._STUDY_QUIZ_DELETED, quizDTO); - } - - @Tag(name = "스터디 출석체크") - @Operation(summary = "[스터디 출석체크] 회원 출석부 불러오기", description = """ - ## [스터디 출석체크] 지정된 날짜의 모든 스터디 회원의 출석 여부를 불러옵니다. - * 출석체크 화면에 표시되는 스터디 회원 정보(프로필 사진, 이름, 출석 여부, 스터디장 여부) 목록를 반환합니다. - * date에는 출석 정보를 확인할 날짜를 입력합니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "scheduleId", description = "출석을 확인할 일정의 id를 입력합니다.", required = true) - @GetMapping("/studies/{studyId}/schedules/{scheduleId}/attendance") - public ApiResponse getAllAttendances( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistSchedule Long scheduleId, - @RequestParam LocalDate date) { - StudyQuizResponseDTO.AttendanceListDTO attendanceListDTO = memberStudyQueryService.getAllAttendances(studyId, scheduleId, date); - return ApiResponse.onSuccess(SuccessStatus._STUDY_MEMBER_ATTENDANCES_FOUND, attendanceListDTO); - } - - - -/* ----------------------------- 스터디 회원 신고 관련 API ------------------------------------- */ - - @Tag(name = "스터디 신고") - @Operation(summary = "[스터디 신고] 스터디원 신고하기", description = """ - ## [스터디 신고] 로그인한 회원이 참여하는 스터디의 다른 회원을 신고합니다. - 신고당한 회원의 id와 이름이 반환됩니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "memberId", description = "신고할 스터디원의 id를 입력합니다.", required = true) - @PostMapping("/studies/{studyId}/members/{memberId}/reports") - public ApiResponse reportStudyMember( - @PathVariable @ExistStudy Long studyId, @PathVariable @ExistMember Long memberId, - @RequestBody @Valid StudyMemberReportDTO studyMemberReportDTO) { - MemberResponseDTO.ReportedMemberDTO reportedMemberDTO = memberStudyCommandService.reportStudyMember(studyId, memberId, studyMemberReportDTO); - return ApiResponse.onSuccess(SuccessStatus._STUDY_MEMBER_REPORTED, reportedMemberDTO); - } - - @Tag(name = "스터디 신고") - @Operation(summary = "[스터디 신고] 스터디 게시글 신고하기", description = """ - ## [스터디 신고] 로그인한 회원이 참여하는 스터디의 게시글을 신고합니다. - 신고당한 스터디 게시글의 id와 제목이 반환됩니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "postId", description = "신고할 스터디 게시글의 id를 입력합니다.", required = true) - @PostMapping("/studies/{studyId}/posts/{postId}/reports") - public ApiResponse reportStudyPost( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistStory Long postId) { - StudyPostResDTO.PostPreviewDTO postPreviewDTO = memberStudyCommandService.reportStudyPost(studyId, postId); - return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_REPORTED, postPreviewDTO); - } - - /* ----------------------------- To-do list 관련 API ------------------------------------- */ - - @Tag(name = "To-Do List") - @Operation(summary = "[To-Do List] To-Do List 생성", description = """ - ## [To-Do List] 로그인한 회원이 참여하는 스터디에 To-Do List를 생성합니다. - To-Do List의 id와 제목, 생성 시간이 반환됩니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @PostMapping("/studies/{studyId}/to-do") - public ApiResponse createToDoList( - @PathVariable @ExistStudy Long studyId, - @RequestBody @Valid ToDoListRequestDTO.ToDoListCreateDTO request) { - ToDoListCreateResponseDTO toDoList = memberStudyCommandService.createToDoList(studyId, - request); - return ApiResponse.onSuccess(SuccessStatus._TO_DO_LIST_CREATED, toDoList); - } - - @Tag(name = "To-Do List") - @Operation(summary = "[To-Do List] To-Do List 내용 수정", description = """ - ## [To-Do List] To-Do List에 작성한 할 일의 내용을 수정 합니다. - 변경 하지 않을 값은 아예 입력하지 않아야 합니다. - ex) date만 변경할 경우, content는 입력하지 않습니다. -> "date": "2022-12-31" 만 입력 - - To-Do List의 id와 수정된 할 일의 내용, 수정 시간이 반환됩니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "toDoId", description = "상태를 변경할 To-Do List의 id를 입력합니다.", required = true) - @PostMapping("/studies/{studyId}/to-do/{toDoId}/update") - public ApiResponse updateToDoList( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistToDo Long toDoId, - @RequestBody @Valid ToDoListRequestDTO.ToDoListCreateDTO request) { - ToDoListUpdateResponseDTO toDoListUpdateResponseDTO = memberStudyCommandService.updateToDoList( - studyId, toDoId, request); - return ApiResponse.onSuccess(SuccessStatus._TO_DO_LIST_UPDATED, toDoListUpdateResponseDTO); - } - - - - @Tag(name = "To-Do List") - @Operation(summary = "[To-Do List] To-Do List 체크 처리", description = """ - ## [To-Do List] To-Do List에 작성한 할 일의 체크 상태를 변경 합니다. - - 체크 표시 되어 있는 경우, 해당 API를 재호출 하면 체크가 해제됩니다. - - To-Do List의 id와 체크한 할 일의 id, 체크 여부가 반환됩니다. - - 본인이 작성한 To-Do List만 체크할 수 있습니다. - 체크 여부가 true 인 경우, 할 일이 완료 되었음을 의미합니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "toDoId", description = "상태를 변경할 To-Do List의 id를 입력합니다.", required = true) - @PostMapping("/studies/{studyId}/to-do/{toDoId}/check") - public ApiResponse checkToDoList( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistToDo Long toDoId) { - ToDoListUpdateResponseDTO toDoListUpdateResponseDTO = memberStudyCommandService.checkToDoList( - studyId, toDoId); - return ApiResponse.onSuccess(SuccessStatus._TO_DO_LIST_UPDATED, toDoListUpdateResponseDTO); - } - - @Tag(name = "To-Do List") - @Operation(summary = "[To-Do List] To-Do List 삭제", description = """ - ## [To-do list] 로그인한 회원이 참여하는 스터디에 To-Do List를 삭제합니다. - - To-Do List 완료 처리와는 다른 개념으로, To-Do List를 삭제합니다. - To-Do List의 id와 상태 업데이트 시간이 반환됩니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "toDoId", description = "삭제할 To-Do List의 id를 입력합니다.", required = true) - @DeleteMapping("/studies/{studyId}/to-do/{toDoId}") - public ApiResponse deleteToDoList( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistToDo Long toDoId) { - ToDoListUpdateResponseDTO toDoListUpdateResponseDTO = memberStudyCommandService.deleteToDoList( - studyId, toDoId); - return ApiResponse.onSuccess(SuccessStatus._TO_DO_LIST_DELETED, toDoListUpdateResponseDTO); - } - - @Tag(name = "To-Do List") - @Operation(summary = "[To-Do List] 내 To-Do List 조회", description = """ - ## [To-Do List] 특정 스터디에 저장된 내 To-Do List를 날짜 별로 페이징 조회합니다. - 조회하고 싶은 날짜를 입력 받아, 해당 날짜의 할 일 목록, 체크 여부가 반환됩니다. - - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "page", description = "조회할 페이지 번호를 입력 받습니다. 페이지 번호는 0부터 시작합니다.", required = true) - @Parameter(name = "size", description = "조회할 페이지 크기를 입력 받습니다. 페이지 크기는 1 이상의 정수 입니다. ", required = true) - @Parameter(name = "date", description = "조회할 날짜를 입력 받습니다. 날짜는 yyyy-MM-dd 형식으로 입력 받습니다.", required = true) - @GetMapping("/studies/{studyId}/to-do/my") - public ApiResponse getMyToDoList( - @PathVariable @ExistStudy Long studyId, - @RequestParam @Min(0) Integer page, - @RequestParam @Min(1) Integer size, - @RequestParam LocalDate date) { - ToDoListSearchResponseDTO toDoList = memberStudyQueryService.getToDoList(studyId, date, - PageRequest.of(page, size)); - return ApiResponse.onSuccess(SuccessStatus._TO_DO_LIST_FOUND, toDoList); - } - - @Tag(name = "To-Do List") - @Operation(summary = "[To-Do List] 다른 스터디 원 To-Do List 조회", description = """ - ## [To-Do List] 특정 스터디에 저장된 다른 스터디원의 To-Do List를 날짜 별로 페이징 조회합니다. - 조회하고 싶은 날짜를 입력 받아, 해당 날짜의 할 일 목록, 체크 여부가 반환됩니다. - """) - @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) - @Parameter(name = "memberId", description = "To-do list를 조회할 회원의 id를 입력합니다.", required = true) - @Parameter(name = "page", description = "조회할 페이지 번호를 입력 받습니다. 페이지 번호는 0부터 시작합니다.", required = true) - @Parameter(name = "size", description = "조회할 페이지 크기를 입력 받습니다. 페이지 크기는 1 이상의 정수 입니다. ", required = true) - @Parameter(name = "date", description = "조회할 날짜를 입력 받습니다. 날짜는 yyyy-MM-dd 형식으로 입력 받습니다.", required = true) - @GetMapping("/studies/{studyId}/to-do/members/{memberId}") - public ApiResponse getOtherToDoList( - @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistMember Long memberId, - @RequestParam @Min(0) Integer page, - @RequestParam @Min(1) Integer size, - @RequestParam LocalDate date) { - ToDoListSearchResponseDTO toDoList = memberStudyQueryService.getMemberToDoList(studyId, - memberId, date, PageRequest.of(page, size)); - return ApiResponse.onSuccess(SuccessStatus._TO_DO_LIST_FOUND, toDoList); - } - -} diff --git a/src/main/java/com/example/spot/study/presentation/dto/response/SearchResponseDTO.java b/src/main/java/com/example/spot/study/presentation/dto/response/SearchResponseDTO.java index 4c638445..cb0e6689 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/response/SearchResponseDTO.java +++ b/src/main/java/com/example/spot/study/presentation/dto/response/SearchResponseDTO.java @@ -1,14 +1,14 @@ package com.example.spot.study.presentation.dto.response; -import com.example.spot.study.domain.aggregate.Region; -import com.example.spot.study.domain.aggregate.StudyRegion; -import com.example.spot.study.domain.aggregate.Theme; +import com.example.spot.study.domain.association.Region; +import com.example.spot.study.domain.association.StudyRegion; +import com.example.spot.study.domain.association.Theme; import com.example.spot.study.domain.enums.StudyApplicationStatus; import com.example.spot.study.domain.enums.StudyLikeStatus; import com.example.spot.study.domain.enums.StudyState; import com.example.spot.study.domain.enums.ThemeType; import com.example.spot.member.domain.association.PreferredStudy; -import com.example.spot.study.domain.aggregate.StudyTheme; +import com.example.spot.study.domain.association.StudyTheme; import com.example.spot.study.domain.Study; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/spot/study/presentation/dto/response/StudyImageResponseDTO.java b/src/main/java/com/example/spot/study/presentation/dto/response/StudyImageResponseDTO.java index 15a0dcae..06453485 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/response/StudyImageResponseDTO.java +++ b/src/main/java/com/example/spot/study/presentation/dto/response/StudyImageResponseDTO.java @@ -1,6 +1,6 @@ package com.example.spot.study.presentation.dto.response; -import com.example.spot.story.domain.aggregate.StoryImage; +import com.example.spot.story.domain.association.StoryImage; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/com/example/spot/todo/application/ToDoCommandService.java b/src/main/java/com/example/spot/todo/application/ToDoCommandService.java new file mode 100644 index 00000000..d8237d0f --- /dev/null +++ b/src/main/java/com/example/spot/todo/application/ToDoCommandService.java @@ -0,0 +1,20 @@ +package com.example.spot.todo.application; + +import com.example.spot.todo.presentation.dto.request.ToDoListRequestDTO; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO.ToDoListCreateResponseDTO; + +public interface ToDoCommandService { + + // 투두 리스트 생성 + ToDoListCreateResponseDTO createToDoList(Long studyId, ToDoListRequestDTO.ToDoListCreateDTO toDoListCreateDTO); + + // 투두 리스트 체크 + ToDoListResponseDTO.ToDoListUpdateResponseDTO checkToDoList(Long studyId, Long toDoListId); + + // 투두 리스트 수정 + ToDoListResponseDTO.ToDoListUpdateResponseDTO updateToDoList(Long studyId, Long toDoListId, ToDoListRequestDTO.ToDoListCreateDTO toDoListCreateDTO); + + // 투두 리스트 삭제 + ToDoListResponseDTO.ToDoListUpdateResponseDTO deleteToDoList(Long studyId, Long toDoListId); +} diff --git a/src/main/java/com/example/spot/todo/application/ToDoCommandServiceImpl.java b/src/main/java/com/example/spot/todo/application/ToDoCommandServiceImpl.java new file mode 100644 index 00000000..59e0859c --- /dev/null +++ b/src/main/java/com/example/spot/todo/application/ToDoCommandServiceImpl.java @@ -0,0 +1,255 @@ +package com.example.spot.todo.application; + +import com.example.spot.common.api.code.status.ErrorStatus; +import com.example.spot.common.api.exception.handler.MemberHandler; +import com.example.spot.common.api.exception.handler.StudyHandler; +import com.example.spot.common.security.utils.SecurityUtils; +import com.example.spot.member.domain.Member; +import com.example.spot.member.domain.MemberRepository; +import com.example.spot.notification.domain.Notification; +import com.example.spot.notification.domain.NotificationRepository; +import com.example.spot.notification.domain.enums.NotifyType; +import com.example.spot.study.domain.Study; +import com.example.spot.study.domain.StudyRepository; +import com.example.spot.study.domain.association.StudyMember; +import com.example.spot.study.domain.enums.StudyApplicationStatus; +import com.example.spot.study.domain.repository.StudyMemberRepository; +import com.example.spot.todo.domain.ToDo; +import com.example.spot.todo.domain.ToDoRepository; +import com.example.spot.todo.presentation.dto.request.ToDoListRequestDTO.ToDoListCreateDTO; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO.ToDoListCreateResponseDTO; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO.ToDoListUpdateResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; + + +@Service +@RequiredArgsConstructor +@Transactional +public class ToDoCommandServiceImpl implements ToDoCommandService { + + private final MemberRepository memberRepository; + private final StudyRepository studyRepository; + private final StudyMemberRepository studyMemberRepository; + private final ToDoRepository toDoRepository; + private final NotificationRepository notificationRepository; + + + /** + * To-Do List를 생성합니다. + * @param studyId 생성할 To-Do List가 속한 스터디 ID + * @param toDoListCreateDTO 생성할 To-Do List 정보 + * @return 생성된 To-Do List 정보 + * @throws StudyHandler 스터디를 찾을 수 없을 때 + * @throws StudyHandler To-Do List를 생성하는 회원이 스터디 회원이 아닐 때 + * @throws StudyHandler 해당 회원을 찾을 수 없을 때 + */ + @Override + public ToDoListCreateResponseDTO createToDoList(Long studyId, + ToDoListCreateDTO toDoListCreateDTO) { + + // 스터디 조회 + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + + // To-Do List를 생성하는 회원 ID 조회 + Long currentUserId = SecurityUtils.getCurrentUserId(); + + // To-Do List를 생성하는 회원이 스터디 회원인지 확인 + if (!isMember(currentUserId, studyId)) + throw new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND); + + // 회원 조회 + Member member = memberRepository.findById(currentUserId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + + // To-Do List 생성 + ToDo toDo = ToDo.builder() + .study(study) + .member(member) + .date(toDoListCreateDTO.getDate()) + .isDone(false) + .content(toDoListCreateDTO.getContent()) + .build(); + + // To-Do List 저장 + toDo.setToDoList(); + toDoRepository.save(toDo); + + // To-Do List 생성 DTO 반환 + return ToDoListCreateResponseDTO.builder() + .id(toDo.getId()) + .content(toDo.getContent()) + .createdAt(toDo.getCreatedAt()) + .build(); + } + + // studyId가 필요할까? + + /** + * To-Do List에 작성한 할 일의 체크 상태를 변경 합니다. 체크 상태를 변경 하면 해당 스터디에 참여하고 있는 모든 회원에게 알림이 전송됩니다. + * @param studyId 스터디 ID + * @param toDoListId 변경할 To-Do List ID + * @return To-Do List 변경 여부와 변경 시간 + * @throws StudyHandler To-Do List를 찾을 수 없을 때 + * @throws StudyHandler To-Do List가 스터디에 속하지 않을 때 + * @throws StudyHandler To-Do List를 변경하는 회원이 스터디 회원이 아닐 때 + * @throws StudyHandler 알림 생성 할 스터디 회원을 찾을 수 없을 때 + */ + @Override + public ToDoListUpdateResponseDTO checkToDoList(Long studyId, Long toDoListId) { + + // To-Do List 조회 + ToDo toDo = toDoRepository.findById(toDoListId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_TODO_NOT_FOUND)); + + // To-Do List가 속한 스터디가 아니면 예외 처리 + if (!Objects.equals(toDo.getStudy().getId(), studyId)) + throw new StudyHandler(ErrorStatus._STUDY_TODO_IS_NOT_BELONG_TO_STUDY); + + // To-Do List를 변경하는 회원이 스터디 회원이 아니면 예외 처리 + Long currentUserId = SecurityUtils.getCurrentUserId(); + if (!toDo.getMember().getId().equals(currentUserId)) + throw new StudyHandler(ErrorStatus._STUDY_TODO_NOT_AUTHORIZED); + + // To-Do List 체크 상태 변경 + toDo.check(); + + // 스터디 회원의 To-Do List 중 하나가 완료 되면, 해당 스터디의 모든 회원에게 알림 전송 + if (toDo.isDone()){ + List members = studyMemberRepository.findAllByStudyIdAndStatus(studyId, StudyApplicationStatus.APPROVED).stream() + .map(StudyMember::getMember) + .toList(); + + // 알림을 생성할 회원이 없으면 알림 생성하지 않음 + if (members.isEmpty()){ + return ToDoListUpdateResponseDTO.builder() + .id(toDo.getId()) + .isDone(toDo.isDone()) + .updatedAt(toDo.getUpdatedAt()) + .build(); + } + + // 알림 생성 + members.forEach(studyMember -> { + Notification notification = Notification.builder() + .member(studyMember) + .notifierName(toDo.getMember().getName()) // To-Do 완료한 회원 이름 + .study(toDo.getStudy()) + .type(NotifyType.TO_DO_UPDATE) + .isChecked(Boolean.FALSE) + .build(); + notificationRepository.save(notification); + }); + } + + // To-Do List 저장 + toDoRepository.save(toDo); + + // To-Do List 변경 DTO 반환 + return ToDoListUpdateResponseDTO.builder() + .id(toDo.getId()) + .isDone(toDo.isDone()) + .updatedAt(toDo.getUpdatedAt()) + .build(); + } + + + /** + * To-Do List 내용을 수정합니다. + * @param studyId 수정할 To-Do List가 속한 스터디 ID + * @param toDoListId 수정할 To-Do List ID + * @param toDoListCreateDTO 수정할 To-Do List 정보 + * @return 수정된 To-Do List 정보 + * @throws StudyHandler To-Do List를 찾을 수 없을 때 + * @throws StudyHandler To-Do List가 스터디에 속하지 않을 때 + * @throws StudyHandler To-Do List를 수정하는 회원이 스터디 회원이 아닐 때 + */ + @Override + public ToDoListUpdateResponseDTO updateToDoList(Long studyId, Long toDoListId, + ToDoListCreateDTO toDoListCreateDTO) { + + // To-Do List 조회 + ToDo toDo = toDoRepository.findById(toDoListId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_TODO_NOT_FOUND)); + + // To-Do List가 속한 스터디가 아니면 예외 처리 + if (!Objects.equals(toDo.getStudy().getId(), studyId)) + throw new StudyHandler(ErrorStatus._STUDY_TODO_IS_NOT_BELONG_TO_STUDY); + + // To-Do List를 수정하는 회원이 스터디 회원이 아니면 예외 처리 + Long currentUserId = SecurityUtils.getCurrentUserId(); + if (!toDo.getMember().getId().equals(currentUserId)) + throw new StudyHandler(ErrorStatus._STUDY_TODO_NOT_AUTHORIZED); + + // To-Do List 수정 + toDo.update(toDoListCreateDTO.getContent(), toDoListCreateDTO.getDate()); + + // To-Do List 저장 + toDoRepository.save(toDo); + + // To-Do List 변경 DTO 반환 + return ToDoListUpdateResponseDTO.builder() + .id(toDo.getId()) + .isDone(toDo.isDone()) + .updatedAt(toDo.getUpdatedAt()) + .build(); + } + + /** + * To-Do List를 삭제합니다. + * @param studyId 삭제할 To-Do List가 속한 스터디 ID + * @param toDoListId 삭제할 To-Do List ID + * @return 삭제된 To-Do List 정보 + * @throws StudyHandler To-Do List를 찾을 수 없을 때 + * @throws StudyHandler To-Do List가 스터디에 속하지 않을 때 + * @throws StudyHandler To-Do List를 삭제하는 회원이 스터디 회원이 아닐 때 + */ + @Override + public ToDoListUpdateResponseDTO deleteToDoList(Long studyId, Long toDoListId) { + + // 로그인 중인 회원 ID 조회 + Long currentUserId = SecurityUtils.getCurrentUserId(); + + // To-Do List를 삭제하는 회원이 스터디 회원인지 확인 + if (!isMember(currentUserId, studyId)) + throw new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND); + + // To-Do List 조회 + ToDo toDo = toDoRepository.findById(toDoListId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_TODO_NOT_FOUND)); + + // To-Do List가 속한 스터디가 아니면 예외 처리 + if (!Objects.equals(toDo.getStudy().getId(), studyId)) + throw new StudyHandler(ErrorStatus._STUDY_TODO_IS_NOT_BELONG_TO_STUDY); + + // To-Do List를 삭제하는 회원의 ID와 To-Do List를 생성한 회원의 ID가 다르면 예외 처리 + if (!toDo.getMember().getId().equals(currentUserId)) + throw new StudyHandler(ErrorStatus._STUDY_TODO_NOT_AUTHORIZED); + + // To-Do List 삭제 + toDoRepository.deleteById(toDoListId); + + // To-Do List 삭제 DTO 반환 + return ToDoListUpdateResponseDTO.builder() + .id(toDo.getId()) + .isDone(toDo.isDone()) + .updatedAt(toDo.getUpdatedAt()) + .build(); + } + + /** + * 회원이 스터디 구성원인지 확인합니다. + * @param memberId 확인 하려는 회원 ID + * @param studyId 확인 하려는 스터디 ID + * @return 스터디 참여 여부를 반환합니다. + */ + private boolean isMember(Long memberId, Long studyId) { + return studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED).isPresent(); + } + +} diff --git a/src/main/java/com/example/spot/todo/application/ToDoQueryService.java b/src/main/java/com/example/spot/todo/application/ToDoQueryService.java new file mode 100644 index 00000000..4415cb79 --- /dev/null +++ b/src/main/java/com/example/spot/todo/application/ToDoQueryService.java @@ -0,0 +1,16 @@ +package com.example.spot.todo.application; + +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO; +import org.springframework.data.domain.PageRequest; + +import java.time.LocalDate; + +public interface ToDoQueryService { + + // 내 투두 리스트 조회 + ToDoListResponseDTO.ToDoListSearchResponseDTO getToDoList(Long studyId, LocalDate date, PageRequest pageRequest); + + // 스터디원 투두 리스트 조회 + ToDoListResponseDTO.ToDoListSearchResponseDTO getMemberToDoList(Long studyId, Long memberId, LocalDate date, PageRequest pageRequest); + +} diff --git a/src/main/java/com/example/spot/todo/application/ToDoQueryServiceImpl.java b/src/main/java/com/example/spot/todo/application/ToDoQueryServiceImpl.java new file mode 100644 index 00000000..b39451b9 --- /dev/null +++ b/src/main/java/com/example/spot/todo/application/ToDoQueryServiceImpl.java @@ -0,0 +1,134 @@ +package com.example.spot.todo.application; + +import com.example.spot.common.api.code.status.ErrorStatus; +import com.example.spot.common.api.exception.GeneralException; +import com.example.spot.common.security.utils.SecurityUtils; +import com.example.spot.study.domain.enums.StudyApplicationStatus; +import com.example.spot.study.domain.repository.StudyMemberRepository; +import com.example.spot.todo.domain.ToDo; +import com.example.spot.todo.domain.ToDoRepository; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO.ToDoListSearchResponseDTO; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO.ToDoListSearchResponseDTO.ToDoListDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ToDoQueryServiceImpl implements ToDoQueryService { + + private final StudyMemberRepository studyMemberRepository; + private final ToDoRepository toDoRepository; + + + /** + * 특정 스터디에 저장된 내 To-Do List를 날짜 별로 페이징 조회합니다. + * @param studyId 스터디 ID + * @param date 조회하려는 날짜 + * @param pageRequest 페이징 정보 + * @return To-Do List 목록을 반환합니다. + * @throws GeneralException 스터디 멤버가 아닌 경우 + * @throws GeneralException 스터디 할 일이 존재하지 않는 경우 + */ + @Override + public ToDoListSearchResponseDTO getToDoList(Long studyId, LocalDate date, PageRequest pageRequest) { + // 로그인 중인 회원 ID 조회 + Long memberId = SecurityUtils.getCurrentUserId(); + + // 로그인한 회원이 스터디 회원인지 확인 + if (!isMember(memberId, studyId)) + throw new GeneralException(ErrorStatus._ONLY_STUDY_MEMBER_CAN_ACCESS_TODO_LIST); + + // 페이징 처리 + List toDos = toDoRepository.findByStudyIdAndMemberIdAndDateOrderByCreatedAtDesc( + studyId, memberId, date, pageRequest); + + // 스터디 투 두 리스트가 존재하지 않는 경우 + if (toDos.isEmpty()) + throw new GeneralException(ErrorStatus._STUDY_TODO_NOT_FOUND); + + // 투 두 리스트 갯수 조회 + long totalElements = toDoRepository.countByStudyIdAndMemberIdAndDate(studyId, memberId, date); + + // DTO로 변환 + List toDoListDTOS = getToDoListDTOS(toDos); + + return new ToDoListSearchResponseDTO( + new PageImpl<>(toDoListDTOS, pageRequest, totalElements), toDoListDTOS, totalElements); + } + + /** + * 특정 스터디에 저장된 다른 스터디원의 To-Do List를 날짜 별로 페이징 조회합니다. + * @param studyId 스터디 ID + * @param memberId 조회하려는 회원 ID + * @param date 조회하려는 날짜 + * @param pageRequest 페이징 정보 + * @return To-Do List 목록을 반환합니다. + * @throws GeneralException 스터디 멤버가 아닌 경우 + * @throws GeneralException 조회하려는 회원이 스터디 멤버가 아닌 경우 + * @throws GeneralException 스터디 할 일이 존재하지 않는 경우 + */ + @Override + public ToDoListSearchResponseDTO getMemberToDoList(Long studyId, Long memberId, LocalDate date, + PageRequest pageRequest) { + + // 로그인 중인 회원이 스터디 회원인지 확인 + if (!isMember(SecurityUtils.getCurrentUserId(), studyId)) + throw new GeneralException(ErrorStatus._ONLY_STUDY_MEMBER_CAN_ACCESS_TODO_LIST); + + // 조회하려는 회원이 스터디 회원인지 확인 + if (!isMember(memberId, studyId)) + throw new GeneralException(ErrorStatus._TODO_LIST_MEMBER_NOT_FOUND); + + // 조회하려는 회원의 투 두 리스트 조회 + List toDos = toDoRepository.findByStudyIdAndMemberIdAndDateOrderByCreatedAtDesc( + studyId, memberId, date, pageRequest); + + // 투 두 리스트가 존재하지 않는 경우 + if (toDos.isEmpty()) + throw new GeneralException(ErrorStatus._STUDY_TODO_NOT_FOUND); + + // 투 두 리스트 갯수 조회 + long totalElements = toDoRepository.countByStudyIdAndMemberIdAndDate(studyId, memberId, date); + + // DTO로 변환 + List toDoListDTOS = getToDoListDTOS(toDos); + + return new ToDoListSearchResponseDTO( + new PageImpl<>(toDoListDTOS, pageRequest, totalElements), toDoListDTOS, totalElements); + } + + /** + * 투 두 리스트를 DTO로 변환합니다. + * @param toDos 투 두 리스트 + * @return 투 두 리스트 DTO 목록을 반환합니다. + */ + private static List getToDoListDTOS(List toDos) { + List toDoListDTOS = toDos.stream() + .map(toDoList -> ToDoListDTO.builder() + .id(toDoList.getId()) + .content(toDoList.getContent()) + .date(toDoList.getDate()) + .isDone(toDoList.isDone()) + .build()) + .toList(); + return toDoListDTOS; + } + + /** + * 회원이 스터디 구성원인지 확인합니다. + * @param memberId 확인 하려는 회원 ID + * @param studyId 확인 하려는 스터디 ID + * @return 스터디 참여 여부를 반환합니다. + */ + private boolean isMember(Long memberId, Long studyId) { + return studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED).isPresent(); + } +} diff --git a/src/main/java/com/example/spot/todo/presentation/controller/ToDoController.java b/src/main/java/com/example/spot/todo/presentation/controller/ToDoController.java new file mode 100644 index 00000000..67a46b43 --- /dev/null +++ b/src/main/java/com/example/spot/todo/presentation/controller/ToDoController.java @@ -0,0 +1,156 @@ +package com.example.spot.todo.presentation.controller; + +import com.example.spot.common.api.ApiResponse; +import com.example.spot.common.api.code.status.SuccessStatus; +import com.example.spot.member.domain.validation.annotation.ExistMember; +import com.example.spot.study.domain.validation.annotation.ExistStudy; +import com.example.spot.todo.application.ToDoCommandService; +import com.example.spot.todo.application.ToDoQueryService; +import com.example.spot.todo.presentation.dto.request.ToDoListRequestDTO; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO.ToDoListCreateResponseDTO; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO.ToDoListSearchResponseDTO; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO.ToDoListUpdateResponseDTO; +import com.example.spot.todo.domain.validation.annotation.ExistToDo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/spot") +@Validated +public class ToDoController { + + private final ToDoQueryService toDoQueryService; + private final ToDoCommandService toDoCommandService; + + + @Tag(name = "To-Do List") + @Operation(summary = "[To-Do List] To-Do List 생성", description = """ + ## [To-Do List] 로그인한 회원이 참여하는 스터디에 To-Do List를 생성합니다. + To-Do List의 id와 제목, 생성 시간이 반환됩니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @PostMapping("/studies/{studyId}/to-do") + public ApiResponse createToDoList( + @PathVariable @ExistStudy Long studyId, + @RequestBody @Valid ToDoListRequestDTO.ToDoListCreateDTO request) { + ToDoListCreateResponseDTO toDoList = toDoCommandService.createToDoList(studyId, + request); + return ApiResponse.onSuccess(SuccessStatus._TO_DO_LIST_CREATED, toDoList); + } + + @Tag(name = "To-Do List") + @Operation(summary = "[To-Do List] To-Do List 내용 수정", description = """ + ## [To-Do List] To-Do List에 작성한 할 일의 내용을 수정 합니다. + 변경 하지 않을 값은 아예 입력하지 않아야 합니다. + ex) date만 변경할 경우, content는 입력하지 않습니다. -> "date": "2022-12-31" 만 입력 + + To-Do List의 id와 수정된 할 일의 내용, 수정 시간이 반환됩니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "toDoId", description = "상태를 변경할 To-Do List의 id를 입력합니다.", required = true) + @PostMapping("/studies/{studyId}/to-do/{toDoId}/update") + public ApiResponse updateToDoList( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistToDo Long toDoId, + @RequestBody @Valid ToDoListRequestDTO.ToDoListCreateDTO request) { + ToDoListUpdateResponseDTO toDoListUpdateResponseDTO = toDoCommandService.updateToDoList( + studyId, toDoId, request); + return ApiResponse.onSuccess(SuccessStatus._TO_DO_LIST_UPDATED, toDoListUpdateResponseDTO); + } + + + + @Tag(name = "To-Do List") + @Operation(summary = "[To-Do List] To-Do List 체크 처리", description = """ + ## [To-Do List] To-Do List에 작성한 할 일의 체크 상태를 변경 합니다. + + 체크 표시 되어 있는 경우, 해당 API를 재호출 하면 체크가 해제됩니다. + + To-Do List의 id와 체크한 할 일의 id, 체크 여부가 반환됩니다. + + 본인이 작성한 To-Do List만 체크할 수 있습니다. + 체크 여부가 true 인 경우, 할 일이 완료 되었음을 의미합니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "toDoId", description = "상태를 변경할 To-Do List의 id를 입력합니다.", required = true) + @PostMapping("/studies/{studyId}/to-do/{toDoId}/check") + public ApiResponse checkToDoList( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistToDo Long toDoId) { + ToDoListUpdateResponseDTO toDoListUpdateResponseDTO = toDoCommandService.checkToDoList( + studyId, toDoId); + return ApiResponse.onSuccess(SuccessStatus._TO_DO_LIST_UPDATED, toDoListUpdateResponseDTO); + } + + @Tag(name = "To-Do List") + @Operation(summary = "[To-Do List] To-Do List 삭제", description = """ + ## [To-do list] 로그인한 회원이 참여하는 스터디에 To-Do List를 삭제합니다. + + To-Do List 완료 처리와는 다른 개념으로, To-Do List를 삭제합니다. + To-Do List의 id와 상태 업데이트 시간이 반환됩니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "toDoId", description = "삭제할 To-Do List의 id를 입력합니다.", required = true) + @DeleteMapping("/studies/{studyId}/to-do/{toDoId}") + public ApiResponse deleteToDoList( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistToDo Long toDoId) { + ToDoListUpdateResponseDTO toDoListUpdateResponseDTO = toDoCommandService.deleteToDoList( + studyId, toDoId); + return ApiResponse.onSuccess(SuccessStatus._TO_DO_LIST_DELETED, toDoListUpdateResponseDTO); + } + + @Tag(name = "To-Do List") + @Operation(summary = "[To-Do List] 내 To-Do List 조회", description = """ + ## [To-Do List] 특정 스터디에 저장된 내 To-Do List를 날짜 별로 페이징 조회합니다. + 조회하고 싶은 날짜를 입력 받아, 해당 날짜의 할 일 목록, 체크 여부가 반환됩니다. + + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "page", description = "조회할 페이지 번호를 입력 받습니다. 페이지 번호는 0부터 시작합니다.", required = true) + @Parameter(name = "size", description = "조회할 페이지 크기를 입력 받습니다. 페이지 크기는 1 이상의 정수 입니다. ", required = true) + @Parameter(name = "date", description = "조회할 날짜를 입력 받습니다. 날짜는 yyyy-MM-dd 형식으로 입력 받습니다.", required = true) + @GetMapping("/studies/{studyId}/to-do/my") + public ApiResponse getMyToDoList( + @PathVariable @ExistStudy Long studyId, + @RequestParam @Min(0) Integer page, + @RequestParam @Min(1) Integer size, + @RequestParam LocalDate date) { + ToDoListSearchResponseDTO toDoList = toDoQueryService.getToDoList(studyId, date, + PageRequest.of(page, size)); + return ApiResponse.onSuccess(SuccessStatus._TO_DO_LIST_FOUND, toDoList); + } + + @Tag(name = "To-Do List") + @Operation(summary = "[To-Do List] 다른 스터디 원 To-Do List 조회", description = """ + ## [To-Do List] 특정 스터디에 저장된 다른 스터디원의 To-Do List를 날짜 별로 페이징 조회합니다. + 조회하고 싶은 날짜를 입력 받아, 해당 날짜의 할 일 목록, 체크 여부가 반환됩니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "memberId", description = "To-do list를 조회할 회원의 id를 입력합니다.", required = true) + @Parameter(name = "page", description = "조회할 페이지 번호를 입력 받습니다. 페이지 번호는 0부터 시작합니다.", required = true) + @Parameter(name = "size", description = "조회할 페이지 크기를 입력 받습니다. 페이지 크기는 1 이상의 정수 입니다. ", required = true) + @Parameter(name = "date", description = "조회할 날짜를 입력 받습니다. 날짜는 yyyy-MM-dd 형식으로 입력 받습니다.", required = true) + @GetMapping("/studies/{studyId}/to-do/members/{memberId}") + public ApiResponse getOtherToDoList( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistMember Long memberId, + @RequestParam @Min(0) Integer page, + @RequestParam @Min(1) Integer size, + @RequestParam LocalDate date) { + ToDoListSearchResponseDTO toDoList = toDoQueryService.getMemberToDoList(studyId, + memberId, date, PageRequest.of(page, size)); + return ApiResponse.onSuccess(SuccessStatus._TO_DO_LIST_FOUND, toDoList); + } + +} diff --git a/src/main/java/com/example/spot/study/presentation/dto/request/ToDoListRequestDTO.java b/src/main/java/com/example/spot/todo/presentation/dto/request/ToDoListRequestDTO.java similarity index 87% rename from src/main/java/com/example/spot/study/presentation/dto/request/ToDoListRequestDTO.java rename to src/main/java/com/example/spot/todo/presentation/dto/request/ToDoListRequestDTO.java index a04d558a..f6f00aaf 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/request/ToDoListRequestDTO.java +++ b/src/main/java/com/example/spot/todo/presentation/dto/request/ToDoListRequestDTO.java @@ -1,4 +1,4 @@ -package com.example.spot.study.presentation.dto.request; +package com.example.spot.todo.presentation.dto.request; import java.time.LocalDate; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/example/spot/study/presentation/dto/response/ToDoListResponseDTO.java b/src/main/java/com/example/spot/todo/presentation/dto/response/ToDoListResponseDTO.java similarity index 96% rename from src/main/java/com/example/spot/study/presentation/dto/response/ToDoListResponseDTO.java rename to src/main/java/com/example/spot/todo/presentation/dto/response/ToDoListResponseDTO.java index 2c96365c..4bcbf7bc 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/response/ToDoListResponseDTO.java +++ b/src/main/java/com/example/spot/todo/presentation/dto/response/ToDoListResponseDTO.java @@ -1,4 +1,4 @@ -package com.example.spot.study.presentation.dto.response; +package com.example.spot.todo.presentation.dto.response; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/spot/vote/application/VoteCommandService.java b/src/main/java/com/example/spot/vote/application/VoteCommandService.java new file mode 100644 index 00000000..5435b3dc --- /dev/null +++ b/src/main/java/com/example/spot/vote/application/VoteCommandService.java @@ -0,0 +1,31 @@ +package com.example.spot.vote.application; + +import com.example.spot.member.presentation.dto.MemberResponseDTO; +import com.example.spot.story.web.dto.response.StoryResDTO; +import com.example.spot.study.presentation.dto.request.StudyHostWithdrawRequestDTO; +import com.example.spot.study.presentation.dto.request.StudyMemberReportDTO; +import com.example.spot.study.presentation.dto.response.StudyApplyResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyTerminationResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyWithdrawalResponseDTO; +import com.example.spot.todo.presentation.dto.request.ToDoListRequestDTO; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO.ToDoListCreateResponseDTO; +import com.example.spot.vote.presentation.dto.request.StudyVoteRequestDTO; +import com.example.spot.vote.presentation.dto.response.StudyVoteResponseDTO; +import jakarta.validation.Valid; + +public interface VoteCommandService { + + // 스터디 투표 생성 + StudyVoteResponseDTO.VotePreviewDTO createVote(Long studyId, StudyVoteRequestDTO.VoteDTO voteDTO); + + // 스터디 투표 참여 + StudyVoteResponseDTO.VotedOptionDTO vote(Long studyId, Long voteId, StudyVoteRequestDTO.VotedOptionDTO votedOptionDTO); + + // 스터디 투표 수정 + StudyVoteResponseDTO.VotePreviewDTO updateVote(Long studyId, Long voteId, StudyVoteRequestDTO.VoteUpdateDTO voteDTO); + + // 스터디 투표 삭제 + StudyVoteResponseDTO.VotePreviewDTO deleteVote(Long studyId, Long voteId); + +} diff --git a/src/main/java/com/example/spot/vote/application/VoteCommandServiceImpl.java b/src/main/java/com/example/spot/vote/application/VoteCommandServiceImpl.java new file mode 100644 index 00000000..6066f836 --- /dev/null +++ b/src/main/java/com/example/spot/vote/application/VoteCommandServiceImpl.java @@ -0,0 +1,276 @@ +package com.example.spot.vote.application; + +import com.example.spot.common.api.code.status.ErrorStatus; +import com.example.spot.common.api.exception.handler.MemberHandler; +import com.example.spot.common.api.exception.handler.StudyHandler; +import com.example.spot.common.security.utils.SecurityUtils; +import com.example.spot.member.domain.Member; +import com.example.spot.member.domain.MemberRepository; +import com.example.spot.study.domain.Study; +import com.example.spot.study.domain.StudyRepository; +import com.example.spot.study.domain.enums.StudyApplicationStatus; +import com.example.spot.study.domain.repository.StudyMemberRepository; +import com.example.spot.vote.domain.Vote; +import com.example.spot.vote.domain.VoteRepository; +import com.example.spot.vote.domain.association.VoteOption; +import com.example.spot.vote.domain.association.VoteParticipant; +import com.example.spot.vote.domain.repository.VoteOptionRepository; +import com.example.spot.vote.domain.repository.VoteParticipantRepository; +import com.example.spot.vote.presentation.dto.request.StudyVoteRequestDTO; +import com.example.spot.vote.presentation.dto.response.StudyVoteResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + + +@Service +@RequiredArgsConstructor +@Transactional +public class VoteCommandServiceImpl implements VoteCommandService { + + private final MemberRepository memberRepository; + private final StudyRepository studyRepository; + private final VoteRepository voteRepository; + private final VoteOptionRepository voteOptionRepository; + private final StudyMemberRepository studyMemberRepository; + private final VoteParticipantRepository voteParticipantRepository; + + + /** + * 스터디 투표를 생성하는 메서드입니다. + * @param studyId 타겟 스터디의 아이디를 입력 받습니다. + * @param voteDTO 생성할 투표의 제목, 항목 목록, 중복 선택 가능 여부, 종료 일시를 입력 받습니다. + * @return 생성된 투표의 아이디와 제목을 반환합니다. + */ + @Override + public StudyVoteResponseDTO.VotePreviewDTO createVote(Long studyId, StudyVoteRequestDTO.VoteDTO voteDTO) { + + //=== Exception ===// + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member loginMember = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + //=== Feature ===// + Vote vote = Vote.builder() + .study(study) + .member(loginMember) + .title(voteDTO.getTitle()) + .isMultipleChoice(voteDTO.getIsMultipleChoice()) + .finishedAt(voteDTO.getFinishedAt()) + .build(); + + // Vote 저장 + vote = voteRepository.save(vote); + // Option 저장 + vote = createOption(vote, voteDTO); + // 연관관계 매핑 + loginMember.addVote(vote); + study.addVote(vote); + + return StudyVoteResponseDTO.VotePreviewDTO.toDTO(vote); + } + + /** + * 스터디 투표의 항목을 생성하는 메서드입니다. + * createVote 메서드 내부에서 사용되는 메서드입니다. + * @param vote 항목을 생성할 타겟 투표를 입력 받습니다. + * @param voteDTO 생성할 투표의 제목, 항목 목록, 중복 선택 가능 여부, 종료 일시를 입력 받습니다. + * @return 투표 객체를 반환합니다. + */ + private Vote createOption(Vote vote, StudyVoteRequestDTO.VoteDTO voteDTO) { + voteDTO.getOptions() + .forEach(stringOption -> { + VoteOption voteOption = VoteOption.builder() + .vote(vote) + .content(stringOption) + .build(); + voteOption = voteOptionRepository.save(voteOption); + vote.addOption(voteOption); + }); + return voteRepository.save(vote); + } + + /** + * 특정 항목에 투표하기 위한 메서드입니다. + * @param studyId 타겟 스터디의 아이디를 입력 받습니다. + * @param voteId 타겟 투표의 아이디를 입력 받습니다. + * @param votedOptionDTO 회원이 투표한 항목의 아이디 목록을 입력 받습니다. + * @return 투표 아이디, 회원 아이디, 투표한 항목 목록을 반환합니다. + */ + @Override + public StudyVoteResponseDTO.VotedOptionDTO vote(Long studyId, Long voteId, StudyVoteRequestDTO.VotedOptionDTO votedOptionDTO) { + + //=== Exception ===// + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member loginMember = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + Vote vote = voteRepository.findById(voteId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); + voteRepository.findByIdAndStudyId(voteId, studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 중복 선택이 허용되지 않는 투표는 여러 개의 option을 선택할 수 없음 + if (!vote.getIsMultipleChoice() && votedOptionDTO.getOptionIdList().size() > 1) { + throw new StudyHandler(ErrorStatus._STUDY_VOTE_MULTIPLE_CHOICE_NOT_VALID); + } + + // 한 번 참여한 투표는 다시 참여할 수 없음 + voteOptionRepository.findAllByVoteId(voteId) + .forEach(option -> { + if (voteParticipantRepository.existsByMemberIdAndVoteOptionId(loginMember.getId(), option.getId())) { + throw new StudyHandler(ErrorStatus._STUDY_VOTE_RE_PARTICIPATION_INVALID); + } + }); + + //=== Feature ===// + List voteParticipants = votedOptionDTO.getOptionIdList().stream() + .map(optionId -> { + VoteOption votedVoteOption = voteOptionRepository.findById(optionId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_OPTION_NOT_FOUND)); + voteOptionRepository.findByIdAndVoteId(optionId, voteId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_OPTION_NOT_FOUND)); + + VoteParticipant voteParticipant = VoteParticipant.builder() + .member(loginMember) + .voteOption(votedVoteOption) + .build(); + + voteParticipant = voteParticipantRepository.save(voteParticipant); + loginMember.addMemberVote(voteParticipant); + votedVoteOption.addMemberVote(voteParticipant); + + return voteParticipant; + }) + .toList(); + + return StudyVoteResponseDTO.VotedOptionDTO.toDTO(vote, loginMember, voteParticipants); + } + + /** + * 투표를 편집하는 메서드입니다. + * @param studyId 타겟 스터디의 아이디를 입력 받습니다. + * @param voteId 편집할 투표의 아이디를 입력 받습니다. + * @param voteDTO 편집된 투표의 제목, 항목 목록, 복수 선택 가능 여부, 종료 일시를 입력 받습니다. + * @return 편집된 투표의 아이디와 제목을 반환합니다. + */ + @Override + public StudyVoteResponseDTO.VotePreviewDTO updateVote(Long studyId, Long voteId, StudyVoteRequestDTO.VoteUpdateDTO voteDTO) { + + //=== Exception ===// + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member loginMember = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + Vote vote = voteRepository.findById(voteId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 로그인한 회원이 투표 생성자인지 확인 + if (!loginMember.equals(vote.getMember())) { + throw new StudyHandler(ErrorStatus._STUDY_VOTE_CREATOR_NOT_AUTHORIZED); + } + + // 한 명이라도 투표에 참여했으면 투표 편집 불가 + voteOptionRepository.findAllByVoteId(voteId) + .forEach(option -> { + if (voteParticipantRepository.existsByVoteOptionId(option.getId())) { + throw new StudyHandler(ErrorStatus._STUDY_VOTE_IS_IN_PROGRESS); + } + }); + + //=== Feature ===// + for (StudyVoteRequestDTO.OptionDTO optionDTO : voteDTO.getOptions()) { + VoteOption voteOption = voteOptionRepository.findById(optionDTO.getOptionId()) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_OPTION_NOT_FOUND)); + voteOption.setContent(optionDTO.getContent()); + voteOption = voteOptionRepository.save(voteOption); + vote.updateOption(voteOption); + } + + vote.updateVote(voteDTO.getTitle(), voteDTO.getIsMultipleChoice(), voteDTO.getFinishedAt()); + vote = voteRepository.save(vote); + loginMember.updateVote(vote); + study.updateVote(vote); + + return StudyVoteResponseDTO.VotePreviewDTO.toDTO(vote); + } + + /** + * 투표를 삭제하는 메서드입니다. + * @param studyId 타겟 스터디의 아이디를 입력 받습니다. + * @param voteId 삭제할 투표의 아이디를 입력 받습니다. + * @return 삭제된 투표의 아이디와 제목을 반환합니다. + */ + @Override + public StudyVoteResponseDTO.VotePreviewDTO deleteVote(Long studyId, Long voteId) { + + //=== Exception ===// + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member loginMember = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + Vote vote = voteRepository.findById(voteId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); + voteRepository.findByIdAndStudyId(voteId, studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 로그인한 회원이 투표 생성자인지 확인 + if (!loginMember.equals(vote.getMember())) { + throw new StudyHandler(ErrorStatus._STUDY_VOTE_CREATOR_NOT_AUTHORIZED); + } + + //=== Feature ===// + deleteOptions(voteId); + loginMember.deleteVote(vote); + study.deleteVote(vote); + voteRepository.delete(vote); + + return StudyVoteResponseDTO.VotePreviewDTO.toDTO(vote); + } + + /** + * 모든 투표 항목을 삭제하는 메서드입니다. + * deleteVote 메서드 내부에서 호출되는 메서드입니다. + * @param voteId 항목을 삭제할 타겟 투표의 아이디를 입력 받습니다. + */ + private void deleteOptions(Long voteId) { + List voteOptions = voteOptionRepository.findAllByVoteId(voteId); + voteOptions.forEach(option -> { + option.deleteAllMemberVotes(); + voteParticipantRepository.deleteAll(voteParticipantRepository.findAllByVoteOptionId(option.getId())); + voteOptionRepository.delete(option); + }); + } + +} diff --git a/src/main/java/com/example/spot/vote/application/VoteQueryService.java b/src/main/java/com/example/spot/vote/application/VoteQueryService.java new file mode 100644 index 00000000..9c828dee --- /dev/null +++ b/src/main/java/com/example/spot/vote/application/VoteQueryService.java @@ -0,0 +1,34 @@ +package com.example.spot.vote.application; + +import com.example.spot.schedule.presentation.dto.response.ScheduleResponseDTO; +import com.example.spot.schedule.presentation.dto.response.StudyQuizResponseDTO; +import com.example.spot.schedule.presentation.dto.response.StudyScheduleResponseDTO; +import com.example.spot.story.web.dto.response.StoryResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyImageResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyMemberResDTO; +import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO.StudyApplicantDTO; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO; +import com.example.spot.vote.presentation.dto.response.StudyVoteResponseDTO; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDate; + +public interface VoteQueryService { + + // 스터디 투표 목록 조회 + StudyVoteResponseDTO.VoteListDTO getAllVotes(Long studyId); + + // 스터디 투표 마감 여부 조회 + Boolean getIsCompleted(Long voteId); + + // 스터디 투표(진행중) 조회 + StudyVoteResponseDTO.VoteDTO getVoteInProgress(Long studyId, Long voteId); + + // 스터디 투표(마감) 조회 + StudyVoteResponseDTO.CompletedVoteDTO getVoteInCompletion(Long studyId, Long voteId); + + // 스터디 투표 현황 조회 + StudyVoteResponseDTO.CompletedVoteDetailDTO getCompletedVoteDetail(Long studyId, Long voteId); +} diff --git a/src/main/java/com/example/spot/vote/application/VoteQueryServiceImpl.java b/src/main/java/com/example/spot/vote/application/VoteQueryServiceImpl.java new file mode 100644 index 00000000..e771f715 --- /dev/null +++ b/src/main/java/com/example/spot/vote/application/VoteQueryServiceImpl.java @@ -0,0 +1,209 @@ +package com.example.spot.vote.application; + +import com.example.spot.common.api.code.status.ErrorStatus; +import com.example.spot.common.api.exception.handler.StudyHandler; +import com.example.spot.common.security.utils.SecurityUtils; +import com.example.spot.member.domain.Member; +import com.example.spot.member.domain.MemberRepository; +import com.example.spot.study.domain.Study; +import com.example.spot.study.domain.StudyRepository; +import com.example.spot.study.domain.enums.StudyApplicationStatus; +import com.example.spot.study.domain.repository.StudyMemberRepository; +import com.example.spot.vote.domain.Vote; +import com.example.spot.vote.domain.VoteRepository; +import com.example.spot.vote.domain.association.VoteOption; +import com.example.spot.vote.domain.repository.VoteParticipantRepository; +import com.example.spot.vote.presentation.dto.response.StudyVoteResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class VoteQueryServiceImpl implements VoteQueryService { + + private final MemberRepository memberRepository; + private final StudyRepository studyRepository; + private final StudyMemberRepository studyMemberRepository; + private final VoteRepository voteRepository; + private final VoteParticipantRepository voteParticipantRepository; + + + /** + * 스터디에 생성된 모든 투표 목록을 불러옵니다. + * @param studyId 투표 목록을 불러올 타겟 스터디의 아이디를 입력 받습니다. + * @return 스터디 아이디와 해당 스터디에서 진행중인 투표 목록, 마감된 투표 목록을 반환합니다. + */ + @Override + public StudyVoteResponseDTO.VoteListDTO getAllVotes(Long studyId) { + + //=== Exception ===// + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + //=== Feature ===// + + // 진행중인 투표 목록 + List votesInProgress = voteRepository.findAllByStudyIdAndFinishedAtAfter(studyId, LocalDateTime.now()).stream() + .map(vote -> { + boolean isParticipated = isParticipated(vote, member); + return StudyVoteResponseDTO.VoteInfoDTO.toDTO(vote, isParticipated); + }) + .toList(); + + // 마감된 투표 목록 + List votesInCompletion = voteRepository.findAllByStudyIdAndFinishedAtBefore(studyId, LocalDateTime.now()).stream() + .map(vote -> { + boolean isParticipated = isParticipated(vote, member); + return StudyVoteResponseDTO.VoteInfoDTO.toDTO(vote, isParticipated); + }) + .toList(); + + return StudyVoteResponseDTO.VoteListDTO.toDTO(studyId, votesInProgress, votesInCompletion); + } + + /** + * 스터디 회원의 투표 참여 여부를 확인하는 메서드입니다. + * getAllVotes에서 사용되는 내부 메서드입니다. + * @param vote 스터디에서 생성한 투표의 아이디를 입력 받습니다. + * @param loginMember 로그인한 회원의 정보를 입력 받습니다. + * @return 투표 참여 여부를 true or false로 반환합니다. + */ + private boolean isParticipated(Vote vote, Member loginMember) { + // 투표 참여 여부 확인 + boolean isParticipated = false; + for (VoteOption voteOption : vote.getVoteOptions()) { + if (voteParticipantRepository.existsByMemberIdAndVoteOptionId(loginMember.getId(), voteOption.getId())) { + isParticipated = true; + } + } + return isParticipated; + } + + /** + * 입력 받은 스터디 투표가 종료되었는지 확인하는 메서드입니다. + * (클라이언트에서 투표 불러오기 API를 호출할 때 스터디 종료 여부에 따라 Response DTO가 바뀌어야 하기 때문에 필요한 메서드입니다) + * @param voteId 스터디에서 생성한 투표의 아이디를 입력 받습니다. + * @return 투표 종료 여부를 true or false로 반환합니다. + */ + @Override + public Boolean getIsCompleted(Long voteId) { + return voteRepository.existsByIdAndFinishedAtBefore(voteId, LocalDateTime.now()); + } + + /** + * 종료된 투표의 정보를 불러오는 메서드입니다. + * @param studyId 타겟 스터디의 아이디를 입력 받습니다. + * @param voteId 스터디에서 생성한 투표의 아이디를 입력 받습니다. + * @return 종료된 투표의 아이디, 생성자, 제목, 항목별 투표 인원수, 전체 참여자 수, 종료 일시를 반환합니다. + */ + @Override + public StudyVoteResponseDTO.CompletedVoteDTO getVoteInCompletion(Long studyId, Long voteId) { + + //=== Exception ===// + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + memberRepository.findById(memberId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); + studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + Vote vote = voteRepository.findById(voteId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 해당 스터디의 투표인지 확인 + voteRepository.findByIdAndStudyId(voteId, studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); + + //=== Feature ===// + return StudyVoteResponseDTO.CompletedVoteDTO.toDTO(vote); + + } + + /** + * 진행중인 투표의 정보를 불러오는 메서드입니다. + * @param studyId 타겟 스터디의 아이디를 입력 받습니다. + * @param voteId 스터디에서 생성한 투표의 아이디를 입력 받습니다. + * @return 진행중인 투표의 아이디, 생성자, 제목, 항목 리스트, 복수 선택 가능 여부, 종료 일시, 로그인한 회원의 참여 여부를 반환합니다. + */ + @Override + public StudyVoteResponseDTO.VoteDTO getVoteInProgress(Long studyId, Long voteId) { + + //=== Exception ===// + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); + studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + Vote vote = voteRepository.findById(voteId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); + + // 해당 스터디의 투표인지 확인 + voteRepository.findByIdAndStudyId(voteId, studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + //=== Feature ===// + return StudyVoteResponseDTO.VoteDTO.toDTO(vote, member); + } + + /** + * 마감된 투표에 대해 항목별 투표 현황을 불러오는 메서드입니다. + * @param studyId 타겟 스터디의 아이디를 입력 받습니다. + * @param voteId 마감된 스터디 투표의 아이디를 입력 받습니다. + * @return 마감된 투표의 아이디와 제목, 항목별 투표 회원 목록을 반환합니다. + */ + @Override + public StudyVoteResponseDTO.CompletedVoteDetailDTO getCompletedVoteDetail(Long studyId, Long voteId) { + + //=== Exception ===// + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + memberRepository.findById(memberId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + Vote vote = voteRepository.findById(voteId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); + + // 해당 스터디의 투표인지 확인 + voteRepository.findByIdAndStudyId(voteId, studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + studyMemberRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, StudyApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 마감된 투표인지 확인 + if (!voteRepository.existsByIdAndFinishedAtBefore(voteId, LocalDateTime.now())) { + throw new StudyHandler(ErrorStatus._STUDY_VOTE_NOT_COMPLETED); + } + + //=== Feature ===// + return StudyVoteResponseDTO.CompletedVoteDetailDTO.toDTO(vote); + } +} diff --git a/src/main/java/com/example/spot/vote/domain/Vote.java b/src/main/java/com/example/spot/vote/domain/Vote.java index 2a8fa0da..5f0598b1 100644 --- a/src/main/java/com/example/spot/vote/domain/Vote.java +++ b/src/main/java/com/example/spot/vote/domain/Vote.java @@ -3,7 +3,7 @@ import com.example.spot.member.domain.Member; import com.example.spot.common.entity.BaseEntity; import com.example.spot.study.domain.Study; -import com.example.spot.vote.domain.aggregate.VoteOption; +import com.example.spot.vote.domain.association.VoteOption; import jakarta.persistence.*; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/spot/vote/domain/aggregate/VoteOption.java b/src/main/java/com/example/spot/vote/domain/association/VoteOption.java similarity index 97% rename from src/main/java/com/example/spot/vote/domain/aggregate/VoteOption.java rename to src/main/java/com/example/spot/vote/domain/association/VoteOption.java index 16eac517..8f8b91af 100644 --- a/src/main/java/com/example/spot/vote/domain/aggregate/VoteOption.java +++ b/src/main/java/com/example/spot/vote/domain/association/VoteOption.java @@ -1,4 +1,4 @@ -package com.example.spot.vote.domain.aggregate; +package com.example.spot.vote.domain.association; import com.example.spot.common.entity.BaseEntity; import com.example.spot.vote.domain.Vote; import jakarta.persistence.CascadeType; diff --git a/src/main/java/com/example/spot/vote/domain/aggregate/VoteParticipant.java b/src/main/java/com/example/spot/vote/domain/association/VoteParticipant.java similarity index 95% rename from src/main/java/com/example/spot/vote/domain/aggregate/VoteParticipant.java rename to src/main/java/com/example/spot/vote/domain/association/VoteParticipant.java index 415f3f83..278004ce 100644 --- a/src/main/java/com/example/spot/vote/domain/aggregate/VoteParticipant.java +++ b/src/main/java/com/example/spot/vote/domain/association/VoteParticipant.java @@ -1,4 +1,4 @@ -package com.example.spot.vote.domain.aggregate; +package com.example.spot.vote.domain.association; import com.example.spot.member.domain.Member; import com.example.spot.common.entity.BaseEntity; diff --git a/src/main/java/com/example/spot/vote/domain/repository/VoteOptionRepository.java b/src/main/java/com/example/spot/vote/domain/repository/VoteOptionRepository.java index 7d01696d..b9eec6b0 100644 --- a/src/main/java/com/example/spot/vote/domain/repository/VoteOptionRepository.java +++ b/src/main/java/com/example/spot/vote/domain/repository/VoteOptionRepository.java @@ -1,6 +1,6 @@ package com.example.spot.vote.domain.repository; -import com.example.spot.vote.domain.aggregate.VoteOption; +import com.example.spot.vote.domain.association.VoteOption; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/spot/vote/domain/repository/VoteParticipantRepository.java b/src/main/java/com/example/spot/vote/domain/repository/VoteParticipantRepository.java index fe204a16..d450cd27 100644 --- a/src/main/java/com/example/spot/vote/domain/repository/VoteParticipantRepository.java +++ b/src/main/java/com/example/spot/vote/domain/repository/VoteParticipantRepository.java @@ -1,6 +1,6 @@ package com.example.spot.vote.domain.repository; -import com.example.spot.vote.domain.aggregate.VoteParticipant; +import com.example.spot.vote.domain.association.VoteParticipant; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/spot/vote/presentation/controller/VoteController.java b/src/main/java/com/example/spot/vote/presentation/controller/VoteController.java new file mode 100644 index 00000000..548c1857 --- /dev/null +++ b/src/main/java/com/example/spot/vote/presentation/controller/VoteController.java @@ -0,0 +1,141 @@ +package com.example.spot.vote.presentation.controller; + +import com.example.spot.common.api.ApiResponse; +import com.example.spot.common.api.code.status.SuccessStatus; +import com.example.spot.study.domain.validation.annotation.ExistStudy; +import com.example.spot.vote.application.VoteCommandService; +import com.example.spot.vote.application.VoteQueryService; +import com.example.spot.vote.domain.validation.annotation.ExistVote; +import com.example.spot.vote.presentation.dto.request.StudyVoteRequestDTO; +import com.example.spot.vote.presentation.dto.response.StudyVoteResponseDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/spot") +@Validated +public class VoteController { + + private final VoteQueryService voteQueryService; + private final VoteCommandService voteCommandService; + + + @Tag(name = "스터디 투표") + @Operation(summary = "[스터디 투표] 투표 생성하기", description = """ + ## [스터디 투표] 내 스터디 > 스터디 > 투표 > 작성 버튼 클릭, 로그인한 회원이 참여하는 특정 스터디에서 새로운 투표를 등록합니다. + 스터디에 참여하는 회원이 생성한 투표를 vote에 저장합니다. + """) + @Parameter(name = "studyId", description = "투표를 생성할 스터디의 id를 입력합니다.", required = true) + @PostMapping("/studies/{studyId}/votes") + public ApiResponse createVote( + @PathVariable @ExistStudy Long studyId, + @RequestBody @Valid StudyVoteRequestDTO.VoteDTO voteDTO) { + StudyVoteResponseDTO.VotePreviewDTO votePreviewDTO = voteCommandService.createVote(studyId, voteDTO); + return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_CREATED, votePreviewDTO); + } + + @Tag(name = "스터디 투표") + @Operation(summary = "[스터디 투표] 투표하기", description = """ + ## [스터디 투표] 내 스터디 > 스터디 > 투표 > 특정 투표 클릭, 로그인한 회원이 참여하는 스터디에서 특정 항목에 투표합니다. + member_vote에 투표 정보를 저장합니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "voteId", description = "참여할 스터디 투표의 id를 입력합니다.") + @PostMapping("/studies/{studyId}/votes/{voteId}/options") + public ApiResponse vote( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistVote Long voteId, + @RequestBody @Valid StudyVoteRequestDTO.VotedOptionDTO votedOptionDTO) { + StudyVoteResponseDTO.VotedOptionDTO votedOptionResDTO = voteCommandService.vote(studyId, voteId, votedOptionDTO); + return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_PARTICIPATED, votedOptionResDTO); + } + + @Tag(name = "스터디 투표") + @Operation(summary = "[스터디 투표] 투표 편집하기", description = """ + ## [스터디 투표] 내 스터디 > 스터디 > 투표 > 편집하기 버튼 클릭, 로그인한 회원이 참여하는 특정 스터디에서 투표 정보를 수정합니다. + 스터디에 참여하는 회원이 생성한 투표를 vote에 저장합니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "voteId", description = "편집할 스터디 투표의 id를 입력합니다.") + @PatchMapping("/studies/{studyId}/votes/{voteId}") + public ApiResponse updateVote( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistVote Long voteId, + @RequestBody @Valid StudyVoteRequestDTO.VoteUpdateDTO voteDTO) { + StudyVoteResponseDTO.VotePreviewDTO votePreviewDTO = voteCommandService.updateVote(studyId, voteId, voteDTO); + return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_UPDATED, votePreviewDTO); + } + + @Tag(name = "스터디 투표") + @Operation(summary = "[스터디 투표] 투표 삭제하기", description = """ + ## [스터디 투표] 내 스터디 > 스터디 > 투표 > 삭제하기 버튼 클릭, 로그인한 회원이 참여하는 특정 스터디에서 투표를 삭제합니다. + 스터디에 참여하는 회원이 생성한 투표를 vote에 저장합니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "voteId", description = "삭제할 스터디 투표의 id를 입력합니다.") + @DeleteMapping("/studies/{studyId}/votes/{voteId}") + public ApiResponse deleteVote( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistVote Long voteId) { + StudyVoteResponseDTO.VotePreviewDTO votePreviewDTO = voteCommandService.deleteVote(studyId, voteId); + return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_DELETED, votePreviewDTO); + } + + @Tag(name = "스터디 투표") + @Operation(summary = "[스터디 투표] 투표 목록 불러오기", description = """ + ## [스터디 투표] 내 스터디 > 스터디 > 투표 클릭, 로그인한 회원이 참여하는 특정 스터디의 투표 목록을 불러옵니다. + 진행 중(finished_at 이전)인 투표 목록과 마감(finished_at 이후)된 투표 목록을 구분하여 반환합니다. + """) + @Parameter(name = "studyId", description = "투표 목록을 불러올 스터디의 id를 입력합니다.", required = true) + @GetMapping("/studies/{studyId}/votes") + public ApiResponse getAllVotes( + @PathVariable @ExistStudy Long studyId) { + StudyVoteResponseDTO.VoteListDTO voteListDTO = voteQueryService.getAllVotes(studyId); + return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_FOUND, voteListDTO); + } + + @Tag(name = "스터디 투표") + @Operation(summary = "[스터디 투표] 투표 불러오기", description = """ + ## [스터디 투표] 내 스터디 > 스터디 > 투표 > 특정 투표 클릭, 로그인한 회원이 참여하는 특정 스터디의 투표를 불러옵니다. + 진행중인 투표 : 진행중인 투표에 대한 항목 및 기본 정보가 반환됩니다. + 마감된 투표 : 마감된 투표에 대한 항목과 투표 인원수가 반환됩니다. + (진행중인 투표인지 마감된 투표인지에 따라 Response DTO가 서로 다릅니다.) + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "voteId", description = "불러올 스터디 투표의 id를 입력합니다.") + @GetMapping("/studies/{studyId}/votes/{voteId}") + public ApiResponse getVote( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistVote Long voteId) { + Boolean isCompleted = voteQueryService.getIsCompleted(voteId); + if (isCompleted) { + StudyVoteResponseDTO.CompletedVoteDTO completedVoteDTO = voteQueryService.getVoteInCompletion(studyId, voteId); + return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_FOUND, completedVoteDTO); + } else { + StudyVoteResponseDTO.VoteDTO voteDTO = voteQueryService.getVoteInProgress(studyId, voteId); + return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_FOUND, voteDTO); + } + } + + @Tag(name = "스터디 투표") + @Operation(summary = "[스터디 투표] 마감된 투표 현황 불러오기", description = """ + ## [스터디 투표] 내 스터디 > 스터디 > 투표 > 마감된 투표 > n명 참여 클릭, 로그인한 회원이 참여하는 특정 스터디의 투표를 불러옵니다. + 마감된 투표에 대하여 항목별 투표 회원 목록을 반환합니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "voteId", description = "마감된 스터디 투표의 id를 입력합니다.") + @GetMapping("/studies/{studyId}/votes/{voteId}/details") + public ApiResponse getCompletedVoteDetail( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistVote Long voteId) { + StudyVoteResponseDTO.CompletedVoteDetailDTO completedVoteDetailDTO = voteQueryService.getCompletedVoteDetail(studyId, voteId); + return ApiResponse.onSuccess(SuccessStatus._STUDY_VOTE_DETAIL_STATUS_FOUND, completedVoteDetailDTO); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/spot/study/presentation/dto/request/StudyVoteRequestDTO.java b/src/main/java/com/example/spot/vote/presentation/dto/request/StudyVoteRequestDTO.java similarity index 95% rename from src/main/java/com/example/spot/study/presentation/dto/request/StudyVoteRequestDTO.java rename to src/main/java/com/example/spot/vote/presentation/dto/request/StudyVoteRequestDTO.java index c079b667..ff00660f 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/request/StudyVoteRequestDTO.java +++ b/src/main/java/com/example/spot/vote/presentation/dto/request/StudyVoteRequestDTO.java @@ -1,4 +1,4 @@ -package com.example.spot.study.presentation.dto.request; +package com.example.spot.vote.presentation.dto.request; import com.example.spot.common.presentation.validator.TextLength; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/example/spot/study/presentation/dto/response/StudyVoteResponseDTO.java b/src/main/java/com/example/spot/vote/presentation/dto/response/StudyVoteResponseDTO.java similarity index 97% rename from src/main/java/com/example/spot/study/presentation/dto/response/StudyVoteResponseDTO.java rename to src/main/java/com/example/spot/vote/presentation/dto/response/StudyVoteResponseDTO.java index 688c585d..7ef16f7d 100644 --- a/src/main/java/com/example/spot/study/presentation/dto/response/StudyVoteResponseDTO.java +++ b/src/main/java/com/example/spot/vote/presentation/dto/response/StudyVoteResponseDTO.java @@ -1,9 +1,9 @@ -package com.example.spot.study.presentation.dto.response; +package com.example.spot.vote.presentation.dto.response; import com.example.spot.member.domain.Member; import com.example.spot.vote.domain.Vote; -import com.example.spot.vote.domain.aggregate.VoteParticipant; -import com.example.spot.vote.domain.aggregate.VoteOption; +import com.example.spot.vote.domain.association.VoteParticipant; +import com.example.spot.vote.domain.association.VoteOption; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; diff --git a/src/test/java/com/example/spot/service/notification/NotificationCommandServiceTest.java b/src/test/java/com/example/spot/service/notification/NotificationCommandServiceTest.java index cff57317..e75cf032 100644 --- a/src/test/java/com/example/spot/service/notification/NotificationCommandServiceTest.java +++ b/src/test/java/com/example/spot/service/notification/NotificationCommandServiceTest.java @@ -11,7 +11,7 @@ import com.example.spot.notification.domain.Notification; import com.example.spot.study.domain.enums.StudyApplicationStatus; import com.example.spot.notification.domain.enums.NotifyType; -import com.example.spot.study.domain.aggregate.StudyMember; +import com.example.spot.study.domain.association.StudyMember; import com.example.spot.study.domain.Study; import com.example.spot.study.domain.repository.StudyMemberRepository; import com.example.spot.notification.domain.NotificationRepository; diff --git a/src/test/java/com/example/spot/service/post/PostCommandServiceTest.java b/src/test/java/com/example/spot/service/post/PostCommandServiceTest.java index 9087bd0e..09d81785 100644 --- a/src/test/java/com/example/spot/service/post/PostCommandServiceTest.java +++ b/src/test/java/com/example/spot/service/post/PostCommandServiceTest.java @@ -6,6 +6,7 @@ import com.example.spot.member.domain.Member; import com.example.spot.post.domain.Post; import com.example.spot.comment.domain.PostComment; +import com.example.spot.report.application.ReportCommandServiceImpl; import com.example.spot.report.domain.PostReport; import com.example.spot.post.domain.enums.Board; import com.example.spot.post.domain.association.MemberScrap; @@ -92,6 +93,9 @@ class PostCommandServiceTest { @InjectMocks private PostCommandServiceImpl postCommandService; + @InjectMocks + private ReportCommandServiceImpl reportCommandService; + private static Member member1; private static Member member2; @@ -1016,7 +1020,7 @@ void reportPost_Success() { when(postReportRepository.save(any(PostReport.class))).thenReturn(postReport); // when - PostReportResponse result = postCommandService.reportPost(postId, memberId); + PostReportResponse result = reportCommandService.reportPost(postId, memberId); // then assertNotNull(result); @@ -1036,7 +1040,7 @@ void reportPost_SelfReport_Fail() { when(postReportRepository.existsByPostIdAndMemberId(postId, memberId)).thenReturn(false); // when & then - assertThrows(PostHandler.class, () -> postCommandService.reportPost(postId, memberId)); + assertThrows(PostHandler.class, () -> reportCommandService.reportPost(postId, memberId)); } @Test @@ -1051,7 +1055,7 @@ void reportPost_DuplicateReport_Fail() { when(postReportRepository.existsByPostIdAndMemberId(postId, memberId)).thenReturn(true); // when & then - assertThrows(PostHandler.class, () -> postCommandService.reportPost(postId, memberId)); + assertThrows(PostHandler.class, () -> reportCommandService.reportPost(postId, memberId)); } diff --git a/src/test/java/com/example/spot/service/study/studyschedule/StudyAttendanceCommandServiceTest.java b/src/test/java/com/example/spot/service/schedule/AttendanceCommandServiceTest.java similarity index 91% rename from src/test/java/com/example/spot/service/study/studyschedule/StudyAttendanceCommandServiceTest.java rename to src/test/java/com/example/spot/service/schedule/AttendanceCommandServiceTest.java index 47b0ee99..0d231df4 100644 --- a/src/test/java/com/example/spot/service/study/studyschedule/StudyAttendanceCommandServiceTest.java +++ b/src/test/java/com/example/spot/service/schedule/AttendanceCommandServiceTest.java @@ -1,23 +1,23 @@ -package com.example.spot.service.study.studyschedule; +package com.example.spot.service.schedule; import com.example.spot.common.api.exception.handler.StudyHandler; import com.example.spot.member.domain.Member; +import com.example.spot.schedule.application.ScheduleCommandServiceImpl; import com.example.spot.schedule.domain.Schedule; -import com.example.spot.schedule.domain.aggregate.Quiz; -import com.example.spot.schedule.domain.aggregate.QuizSubmission; +import com.example.spot.schedule.domain.association.Quiz; +import com.example.spot.schedule.domain.association.QuizSubmission; import com.example.spot.schedule.domain.repository.QuizRepository; import com.example.spot.schedule.domain.repository.QuizSubmissionRepository; import com.example.spot.schedule.domain.ScheduleRepository; -import com.example.spot.study.domain.aggregate.StudyMember; +import com.example.spot.study.domain.association.StudyMember; import com.example.spot.study.domain.enums.StudyApplicationStatus; import com.example.spot.member.domain.enums.Gender; import com.example.spot.study.domain.Study; import com.example.spot.member.domain.MemberRepository; import com.example.spot.study.domain.repository.StudyMemberRepository; import com.example.spot.study.domain.StudyRepository; -import com.example.spot.study.application.MemberStudyCommandServiceImpl; -import com.example.spot.study.presentation.dto.request.StudyQuizRequestDTO; -import com.example.spot.study.presentation.dto.response.StudyQuizResponseDTO; +import com.example.spot.schedule.presentation.dto.request.StudyQuizRequestDTO; +import com.example.spot.schedule.presentation.dto.response.StudyQuizResponseDTO; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -45,7 +45,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -class StudyAttendanceCommandServiceTest { +class AttendanceCommandServiceTest { @Mock private MemberRepository memberRepository; @@ -63,7 +63,7 @@ class StudyAttendanceCommandServiceTest { private QuizSubmissionRepository quizSubmissionRepository; @InjectMocks - private MemberStudyCommandServiceImpl memberStudyCommandService; + private ScheduleCommandServiceImpl scheduleCommandService; private static Study study; private static Member member1; @@ -130,7 +130,7 @@ void createAttendanceQuiz_Success() { when(quizRepository.save(any(Quiz.class))).thenReturn(quiz2); // when - StudyQuizResponseDTO.QuizDTO result = memberStudyCommandService.createAttendanceQuiz(studyId, schedule.getId(), quizRequestDTO); + StudyQuizResponseDTO.QuizDTO result = scheduleCommandService.createAttendanceQuiz(studyId, schedule.getId(), quizRequestDTO); // then assertThat(result).isNotNull(); @@ -154,7 +154,7 @@ void createAttendanceQuiz_StudyNotFound_Fail() { when(quizRepository.save(any(Quiz.class))).thenReturn(quiz2); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.createAttendanceQuiz(studyId, schedule.getId(), quizRequestDTO)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.createAttendanceQuiz(studyId, schedule.getId(), quizRequestDTO)); } @Test @@ -173,7 +173,7 @@ void createAttendanceQuiz_NotStudyMember_Fail() { when(quizRepository.save(any(Quiz.class))).thenReturn(quiz2); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.createAttendanceQuiz(studyId, schedule.getId(), quizRequestDTO)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.createAttendanceQuiz(studyId, schedule.getId(), quizRequestDTO)); } @Test @@ -192,7 +192,7 @@ void createAttendanceQuiz_NotOwner_Fail() { when(quizRepository.save(any(Quiz.class))).thenReturn(quiz2); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.createAttendanceQuiz(studyId, schedule.getId(), quizRequestDTO)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.createAttendanceQuiz(studyId, schedule.getId(), quizRequestDTO)); } @Test @@ -214,7 +214,7 @@ void createAttendanceQuiz_ScheduleNotFound_Fail() { when(quizRepository.save(any(Quiz.class))).thenReturn(quiz2); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.createAttendanceQuiz(studyId, schedule.getId(), quizRequestDTO)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.createAttendanceQuiz(studyId, schedule.getId(), quizRequestDTO)); } @Test @@ -242,7 +242,7 @@ void attendantStudy_Success() { .thenReturn(List.of(member1Attendance)); // when - StudyQuizResponseDTO.AttendanceDTO result = memberStudyCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO); + StudyQuizResponseDTO.AttendanceDTO result = scheduleCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO); // then assertThat(result).isNotNull(); @@ -276,7 +276,7 @@ void attendantStudy_NotStudyMember_Fail() { .thenReturn(List.of()); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO)); } @Test @@ -307,7 +307,7 @@ void attendantStudy_QuizNotFound_Fail() { .thenReturn(List.of(member1Attendance)); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO)); } @Test @@ -341,7 +341,7 @@ void attendantStudy_QuizTimeOver_Fail() { .thenReturn(List.of()); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO)); } @Test @@ -370,7 +370,7 @@ void attendantStudy_QuizTryOver_Fail() { .thenReturn(List.of(member1Attendance, member1Attendance2, member1Attendance3)); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO)); } @Test @@ -398,7 +398,7 @@ void attendantStudy_QuizAlreadyCorrect_Fail() { .thenReturn(List.of(ownerAttendance)); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO)); } @Test @@ -426,7 +426,7 @@ void deleteAttendanceQuiz_Success() { .thenReturn(List.of(member1Attendance, member1Attendance2, member1Attendance3, ownerAttendance)); // when - StudyQuizResponseDTO.QuizDTO result = memberStudyCommandService.deleteAttendanceQuiz(studyId, scheduleId, date); + StudyQuizResponseDTO.QuizDTO result = scheduleCommandService.deleteAttendanceQuiz(studyId, scheduleId, date); // then assertThat(result).isNotNull(); @@ -460,7 +460,7 @@ void deleteAttendanceQuiz_QuizNotFound_Fail() { .thenReturn(List.of(member1Attendance, member1Attendance2, member1Attendance3, ownerAttendance)); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.deleteAttendanceQuiz(studyId, scheduleId, date)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.deleteAttendanceQuiz(studyId, scheduleId, date)); } @Test @@ -486,7 +486,7 @@ void deleteAttendanceQuiz_NotStudyMember_Fail() { .thenReturn(List.of(member1Attendance, member1Attendance2, member1Attendance3, ownerAttendance)); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.deleteAttendanceQuiz(studyId, scheduleId, date)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.deleteAttendanceQuiz(studyId, scheduleId, date)); } @Test @@ -514,7 +514,7 @@ void deleteAttendanceQuiz_NotStudyOwner_Fail() { .thenReturn(List.of(member1Attendance, member1Attendance2, member1Attendance3, ownerAttendance)); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.deleteAttendanceQuiz(studyId, scheduleId, date)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.deleteAttendanceQuiz(studyId, scheduleId, date)); } /*-------------------------------------------------------- Utils ------------------------------------------------------------------------*/ diff --git a/src/test/java/com/example/spot/service/study/studyschedule/StudyAttendanceQueryServiceTest.java b/src/test/java/com/example/spot/service/schedule/AttendanceQueryServiceTest.java similarity index 87% rename from src/test/java/com/example/spot/service/study/studyschedule/StudyAttendanceQueryServiceTest.java rename to src/test/java/com/example/spot/service/schedule/AttendanceQueryServiceTest.java index 7c90e95c..fbbeba70 100644 --- a/src/test/java/com/example/spot/service/study/studyschedule/StudyAttendanceQueryServiceTest.java +++ b/src/test/java/com/example/spot/service/schedule/AttendanceQueryServiceTest.java @@ -1,22 +1,22 @@ -package com.example.spot.service.study.studyschedule; +package com.example.spot.service.schedule; import com.example.spot.common.api.exception.handler.StudyHandler; import com.example.spot.member.domain.Member; +import com.example.spot.schedule.application.ScheduleQueryServiceImpl; import com.example.spot.schedule.domain.Schedule; -import com.example.spot.schedule.domain.aggregate.Quiz; -import com.example.spot.schedule.domain.aggregate.QuizSubmission; +import com.example.spot.schedule.domain.association.Quiz; +import com.example.spot.schedule.domain.association.QuizSubmission; import com.example.spot.schedule.domain.repository.QuizRepository; import com.example.spot.schedule.domain.repository.QuizSubmissionRepository; import com.example.spot.schedule.domain.ScheduleRepository; -import com.example.spot.study.domain.aggregate.StudyMember; +import com.example.spot.study.domain.association.StudyMember; import com.example.spot.study.domain.enums.StudyApplicationStatus; import com.example.spot.member.domain.enums.Gender; import com.example.spot.study.domain.Study; import com.example.spot.member.domain.MemberRepository; import com.example.spot.study.domain.repository.StudyMemberRepository; import com.example.spot.study.domain.StudyRepository; -import com.example.spot.study.application.MemberStudyQueryServiceImpl; -import com.example.spot.study.presentation.dto.response.StudyQuizResponseDTO; +import com.example.spot.schedule.presentation.dto.response.StudyQuizResponseDTO; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -43,7 +43,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -class StudyAttendanceQueryServiceTest { +class AttendanceQueryServiceTest { @Mock private MemberRepository memberRepository; @@ -61,7 +61,7 @@ class StudyAttendanceQueryServiceTest { private QuizSubmissionRepository quizSubmissionRepository; @InjectMocks - private MemberStudyQueryServiceImpl memberStudyQueryService; + private ScheduleQueryServiceImpl scheduleQueryService; private static Study study; private static Member member1; @@ -118,7 +118,7 @@ void getAllAttendances_Success() { getAuthentication(member1.getId()); // when - StudyQuizResponseDTO.AttendanceListDTO result = memberStudyQueryService.getAllAttendances(studyId, schedule.getId(), date); + StudyQuizResponseDTO.AttendanceListDTO result = scheduleQueryService.getAllAttendances(studyId, schedule.getId(), date); // then assertThat(result).isNotNull(); @@ -141,7 +141,7 @@ void getAllAttendances_StudyNotFound_Fail() { getAuthentication(member2.getId()); // when & then - assertThrows(StudyHandler.class, () -> memberStudyQueryService.getAllAttendances(studyId, schedule.getId(), date)); + assertThrows(StudyHandler.class, () -> scheduleQueryService.getAllAttendances(studyId, schedule.getId(), date)); } @Test @@ -155,7 +155,7 @@ void getAllAttendances_NotStudyMember_Fail() { getAuthentication(member2.getId()); // when & then - assertThrows(StudyHandler.class, () -> memberStudyQueryService.getAllAttendances(studyId, schedule.getId(), date)); + assertThrows(StudyHandler.class, () -> scheduleQueryService.getAllAttendances(studyId, schedule.getId(), date)); } @Test @@ -169,7 +169,7 @@ void getAllAttendances_ScheduleNotFound_Fail() { getAuthentication(member1.getId()); // when & then - assertThrows(StudyHandler.class, () -> memberStudyQueryService.getAllAttendances(studyId, 2L, date)); + assertThrows(StudyHandler.class, () -> scheduleQueryService.getAllAttendances(studyId, 2L, date)); } @Test @@ -183,7 +183,7 @@ void getAttendanceQuiz_Success() { getAuthentication(member1.getId()); // when - StudyQuizResponseDTO.QuizDTO result = memberStudyQueryService.getAttendanceQuiz(studyId, schedule.getId(), date); + StudyQuizResponseDTO.QuizDTO result = scheduleQueryService.getAttendanceQuiz(studyId, schedule.getId(), date); // then assertThat(result).isNotNull(); @@ -202,7 +202,7 @@ void getAttendanceQuiz_StudyNotFound_Fail() { getAuthentication(member1.getId()); // when - assertThrows(StudyHandler.class, () -> memberStudyQueryService.getAttendanceQuiz(studyId, schedule.getId(), date)); + assertThrows(StudyHandler.class, () -> scheduleQueryService.getAttendanceQuiz(studyId, schedule.getId(), date)); } @Test @@ -216,7 +216,7 @@ void getAttendanceQuiz_NotStudyMember_Fail() { getAuthentication(member2.getId()); // when - assertThrows(StudyHandler.class, () -> memberStudyQueryService.getAttendanceQuiz(studyId, schedule.getId(), date)); + assertThrows(StudyHandler.class, () -> scheduleQueryService.getAttendanceQuiz(studyId, schedule.getId(), date)); } @Test @@ -230,7 +230,7 @@ void getAttendanceQuiz_ScheduleNotFound_Fail() { getAuthentication(member1.getId()); // when - assertThrows(StudyHandler.class, () -> memberStudyQueryService.getAttendanceQuiz(studyId, 2L, date)); + assertThrows(StudyHandler.class, () -> scheduleQueryService.getAttendanceQuiz(studyId, 2L, date)); } diff --git a/src/test/java/com/example/spot/service/study/studyschedule/StudyScheduleCommandServiceTest.java b/src/test/java/com/example/spot/service/schedule/ScheduleCommandServiceTest.java similarity index 92% rename from src/test/java/com/example/spot/service/study/studyschedule/StudyScheduleCommandServiceTest.java rename to src/test/java/com/example/spot/service/schedule/ScheduleCommandServiceTest.java index 9d855074..747ac933 100644 --- a/src/test/java/com/example/spot/service/study/studyschedule/StudyScheduleCommandServiceTest.java +++ b/src/test/java/com/example/spot/service/schedule/ScheduleCommandServiceTest.java @@ -1,11 +1,12 @@ -package com.example.spot.service.study.studyschedule; +package com.example.spot.service.schedule; import com.example.spot.common.api.exception.handler.StudyHandler; import com.example.spot.member.domain.Member; import com.example.spot.notification.domain.Notification; +import com.example.spot.schedule.application.ScheduleCommandServiceImpl; import com.example.spot.schedule.domain.Schedule; import com.example.spot.schedule.domain.enums.SchedulePeriod; -import com.example.spot.study.domain.aggregate.StudyMember; +import com.example.spot.study.domain.association.StudyMember; import com.example.spot.study.domain.enums.StudyApplicationStatus; import com.example.spot.member.domain.enums.Gender; import com.example.spot.study.domain.Study; @@ -14,9 +15,8 @@ import com.example.spot.notification.domain.NotificationRepository; import com.example.spot.schedule.domain.ScheduleRepository; import com.example.spot.study.domain.StudyRepository; -import com.example.spot.study.application.MemberStudyCommandServiceImpl; -import com.example.spot.study.presentation.dto.request.ScheduleRequestDTO; -import com.example.spot.study.presentation.dto.response.ScheduleResponseDTO; +import com.example.spot.schedule.presentation.dto.request.ScheduleRequestDTO; +import com.example.spot.schedule.presentation.dto.response.ScheduleResponseDTO; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -45,7 +45,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -class StudyScheduleCommandServiceTest { +class ScheduleCommandServiceTest { @Mock private MemberRepository memberRepository; @@ -65,7 +65,7 @@ class StudyScheduleCommandServiceTest { private NotificationRepository notificationRepository; @InjectMocks - private MemberStudyCommandServiceImpl memberStudyCommandService; + private ScheduleCommandServiceImpl scheduleCommandService; private static Study study1; private static Member member1; @@ -160,7 +160,7 @@ private void addScheduleSuccess(SchedulePeriod schedulePeriod) { when(scheduleRepository.save(schedule)).thenReturn(schedule); // when - ScheduleResponseDTO.ScheduleDTO result = memberStudyCommandService.addSchedule(studyId, scheduleRequestDTO); + ScheduleResponseDTO.ScheduleDTO result = scheduleCommandService.addSchedule(studyId, scheduleRequestDTO); // then assertThat(result).isNotNull(); @@ -250,7 +250,7 @@ private void addScheduleFail(LocalDateTime startedAt, LocalDateTime finishedAt, when(scheduleRepository.save(schedule)).thenReturn(schedule); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.addSchedule(studyId, scheduleRequestDTO)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.addSchedule(studyId, scheduleRequestDTO)); } @Test @@ -273,7 +273,7 @@ void modSchedule_Success() { when(scheduleRepository.save(schedule)).thenReturn(schedule); // when - ScheduleResponseDTO.ScheduleDTO result = memberStudyCommandService.modSchedule(studyId, scheduleId, scheduleModDTO); + ScheduleResponseDTO.ScheduleDTO result = scheduleCommandService.modSchedule(studyId, scheduleId, scheduleModDTO); // then assertThat(result).isNotNull(); @@ -295,7 +295,7 @@ void modSchedule_NotStudyMember_Fail() { ScheduleRequestDTO.ScheduleDTO scheduleModDTO = getScheduleModDTO(memberId, scheduleId, studyId, member2, study1); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.modSchedule(studyId, scheduleId, scheduleModDTO)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.modSchedule(studyId, scheduleId, scheduleModDTO)); } @Test @@ -310,7 +310,7 @@ void modSchedule_NotCreator_Fail() { ScheduleRequestDTO.ScheduleDTO scheduleModDTO = getScheduleModDTO(memberId, scheduleId, studyId, member1, study1); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.modSchedule(studyId, scheduleId, scheduleModDTO)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.modSchedule(studyId, scheduleId, scheduleModDTO)); } @Test @@ -325,7 +325,7 @@ void modSchedule_NotStudySchedule_Fail() { ScheduleRequestDTO.ScheduleDTO scheduleModDTO = getScheduleModDTO(memberId, scheduleId, studyId, member1, study1); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.modSchedule(studyId, scheduleId, scheduleModDTO)); + assertThrows(StudyHandler.class, () -> scheduleCommandService.modSchedule(studyId, scheduleId, scheduleModDTO)); } private ScheduleRequestDTO.ScheduleDTO getScheduleModDTO( diff --git a/src/test/java/com/example/spot/service/study/studyschedule/StudyScheduleQueryServiceTest.java b/src/test/java/com/example/spot/service/schedule/ScheduleQueryServiceTest.java similarity index 79% rename from src/test/java/com/example/spot/service/study/studyschedule/StudyScheduleQueryServiceTest.java rename to src/test/java/com/example/spot/service/schedule/ScheduleQueryServiceTest.java index 56478d32..da73c3fe 100644 --- a/src/test/java/com/example/spot/service/study/studyschedule/StudyScheduleQueryServiceTest.java +++ b/src/test/java/com/example/spot/service/schedule/ScheduleQueryServiceTest.java @@ -1,19 +1,22 @@ -package com.example.spot.service.study.studyschedule; +package com.example.spot.service.schedule; +import com.example.spot.common.api.exception.GeneralException; import com.example.spot.common.api.exception.handler.StudyHandler; +import com.example.spot.common.security.utils.SecurityUtils; import com.example.spot.member.domain.Member; +import com.example.spot.schedule.application.ScheduleQueryServiceImpl; import com.example.spot.schedule.domain.Schedule; import com.example.spot.schedule.domain.enums.SchedulePeriod; +import com.example.spot.schedule.presentation.dto.response.StudyScheduleResponseDTO; import com.example.spot.study.domain.enums.StudyApplicationStatus; import com.example.spot.member.domain.enums.Gender; -import com.example.spot.study.domain.aggregate.StudyMember; +import com.example.spot.study.domain.association.StudyMember; import com.example.spot.study.domain.Study; import com.example.spot.member.domain.MemberRepository; import com.example.spot.study.domain.repository.StudyMemberRepository; import com.example.spot.schedule.domain.ScheduleRepository; import com.example.spot.study.domain.StudyRepository; -import com.example.spot.study.application.MemberStudyQueryServiceImpl; -import com.example.spot.study.presentation.dto.response.ScheduleResponseDTO; +import com.example.spot.schedule.presentation.dto.response.ScheduleResponseDTO; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -23,6 +26,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.springframework.data.domain.Pageable; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; @@ -33,15 +37,17 @@ import java.time.LocalTime; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -class StudyScheduleQueryServiceTest { +class ScheduleQueryServiceTest { @Mock private MemberRepository memberRepository; @@ -50,12 +56,11 @@ class StudyScheduleQueryServiceTest { private StudyRepository studyRepository; @Mock private StudyMemberRepository studyMemberRepository; - @Mock private ScheduleRepository scheduleRepository; @InjectMocks - private MemberStudyQueryServiceImpl memberStudyQueryService; + private ScheduleQueryServiceImpl scheduleQueryService; private static Study study1; private static Study study2; @@ -86,6 +91,9 @@ void setUp() { when(studyMemberRepository.existsByMemberIdAndStudyIdAndStatus(member1.getId(), study1.getId(), StudyApplicationStatus.APPROVED)).thenReturn(true); when(studyMemberRepository.existsByMemberIdAndStudyIdAndStatus(member2.getId(), study1.getId(), StudyApplicationStatus.APPROVED)).thenReturn(false); when(studyMemberRepository.existsByMemberIdAndStudyIdAndStatus(owner.getId(), study1.getId(), StudyApplicationStatus.APPROVED)).thenReturn(true); + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus( + member1.getId(), study1.getId(),StudyApplicationStatus.APPROVED) + ).thenReturn(Optional.of(member1Study1)); when(scheduleRepository.findById(schedule1.getId())).thenReturn(Optional.of(schedule1)); when(scheduleRepository.findById(schedule2.getId())).thenReturn(Optional.of(schedule2)); @@ -107,7 +115,7 @@ void getMonthlySchedules_Success() { getAuthentication(memberId); // when - ScheduleResponseDTO.MonthlyScheduleListDTO result = memberStudyQueryService.getMonthlySchedules(studyId, 2024, 1); + ScheduleResponseDTO.MonthlyScheduleListDTO result = scheduleQueryService.getMonthlySchedules(studyId, 2024, 1); // then assertThat(result).isNotNull(); @@ -129,7 +137,7 @@ void getMonthlySchedules_NotStudyMember_Fail() { getAuthentication(memberId); // when - ScheduleResponseDTO.MonthlyScheduleListDTO result = memberStudyQueryService.getMonthlySchedules(studyId, 2024, 1); + ScheduleResponseDTO.MonthlyScheduleListDTO result = scheduleQueryService.getMonthlySchedules(studyId, 2024, 1); // then assertThat(result).isNotNull(); @@ -152,7 +160,7 @@ void getSchedule_Success() { getAuthentication(memberId); // when - ScheduleResponseDTO.MonthlyScheduleDTO result = memberStudyQueryService.getSchedule(scheduleId, studyId); + ScheduleResponseDTO.MonthlyScheduleDTO result = scheduleQueryService.getSchedule(scheduleId, studyId); // then assertThat(result).isNotNull(); @@ -172,10 +180,44 @@ void getSchedule_NotStudySchedule_Fail() { getAuthentication(memberId); // when & then - assertThrows(StudyHandler.class, () -> memberStudyQueryService.getSchedule(scheduleId, studyId)); + assertThrows(StudyHandler.class, () -> scheduleQueryService.getSchedule(scheduleId, studyId)); + } + +/* ------------------------------------------------ 스터디 모임 목록 조회 --------------------------------------------------- */ + + @Test + @DisplayName("스터디 모임 목록 조회 - 로그인 한 회원이 해당 스터디 회원이 아닌 경우") + void 스터디_모임_목록_조회_실패_1(){ + + // given + Long memberId = 1L; + Long studyId = 2L; + + // 사용자 인증 정보 생성 + getAuthentication(memberId); + + // when & then + assertThrows(GeneralException.class, () -> scheduleQueryService.findStudySchedule(studyId, Pageable.unpaged())); } -/*-------------------------------------------------------- Utils ------------------------------------------------------------------------*/ + @Test + @DisplayName("스터디 모임 목록 조회 - 스터디 모임 일정이 존재하지 않는 경우") + void 스터디_모임_목록_조회_실패_2(){ + + // given + Long memberId = 1L; + Long studyId = 2L; + + // 사용자 인증 정보 생성 + getAuthentication(memberId); + + // when & then + assertThrows(GeneralException.class, () -> scheduleQueryService.findStudySchedule(studyId, Pageable.unpaged())); + } + + + + /*-------------------------------------------------------- Utils ------------------------------------------------------------------------*/ private static void initMember() { member1 = Member.builder() diff --git a/src/test/java/com/example/spot/service/study/studypost/StoryCommandServiceTest.java b/src/test/java/com/example/spot/service/story/StoryCommandServiceTest.java similarity index 93% rename from src/test/java/com/example/spot/service/study/studypost/StoryCommandServiceTest.java rename to src/test/java/com/example/spot/service/story/StoryCommandServiceTest.java index 5c1d019c..bbb68a6e 100644 --- a/src/test/java/com/example/spot/service/study/studypost/StoryCommandServiceTest.java +++ b/src/test/java/com/example/spot/service/story/StoryCommandServiceTest.java @@ -1,19 +1,19 @@ -package com.example.spot.service.study.studypost; +package com.example.spot.service.story; import com.example.spot.common.api.exception.handler.StudyHandler; import com.example.spot.member.domain.Member; import com.example.spot.notification.domain.Notification; import com.example.spot.story.domain.Story; import com.example.spot.story.domain.StoryRepository; -import com.example.spot.story.domain.aggregate.LikedStory; -import com.example.spot.story.domain.aggregate.LikedStoryComment; -import com.example.spot.story.domain.aggregate.StoryComment; +import com.example.spot.story.domain.association.LikedStory; +import com.example.spot.story.domain.association.LikedStoryComment; +import com.example.spot.story.domain.association.StoryComment; import com.example.spot.story.domain.repository.LikedStoryCommentRepository; import com.example.spot.story.domain.repository.LikedStoryRepository; import com.example.spot.story.domain.repository.StoryCommentRepository; import com.example.spot.story.domain.repository.StoryImageRepository; -import com.example.spot.story.domain.repository.StoryReportRepository; -import com.example.spot.study.domain.aggregate.StudyMember; +import com.example.spot.report.domain.StoryReportRepository; +import com.example.spot.study.domain.association.StudyMember; import com.example.spot.study.domain.enums.StudyApplicationStatus; import com.example.spot.member.domain.enums.Gender; import com.example.spot.story.domain.enums.StoryCategory; @@ -22,12 +22,12 @@ import com.example.spot.study.domain.repository.StudyMemberRepository; import com.example.spot.notification.domain.NotificationRepository; import com.example.spot.study.domain.StudyRepository; -import com.example.spot.study.application.StudyPostCommandServiceImpl; +import com.example.spot.story.application.StoryCommandServiceImpl; import com.example.spot.common.application.s3.S3ImageService; -import com.example.spot.study.presentation.dto.request.StudyPostCommentRequestDTO; -import com.example.spot.study.presentation.dto.request.StudyPostRequestDTO; -import com.example.spot.study.presentation.dto.response.StudyPostCommentResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyPostResDTO; +import com.example.spot.story.web.dto.request.StoryCommentRequestDTO; +import com.example.spot.story.web.dto.request.StoryRequestDTO; +import com.example.spot.story.web.dto.response.StoryCommentResponseDTO; +import com.example.spot.story.web.dto.response.StoryResDTO; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -86,7 +86,7 @@ class StoryCommandServiceTest { private S3ImageService s3ImageService; @InjectMocks - private StudyPostCommandServiceImpl studyPostCommandService; + private StoryCommandServiceImpl studyPostCommandService; private static Study study; private static Member member1; @@ -161,7 +161,7 @@ void createPost_Announcement_Success() { Long memberId = 3L; Long studyId = 1L; - StudyPostRequestDTO.PostDTO postPreviewDTO = StudyPostRequestDTO.PostDTO.builder() + StoryRequestDTO.PostDTO postPreviewDTO = StoryRequestDTO.PostDTO.builder() .isAnnouncement(true) .storyCategory(StoryCategory.INFO_SHARING) .title("공지") @@ -178,7 +178,7 @@ void createPost_Announcement_Success() { .thenReturn(List.of(member1Study, ownerStudy)); // when - StudyPostResDTO.PostPreviewDTO result = studyPostCommandService.createPost(studyId, postPreviewDTO); + StoryResDTO.PostPreviewDTO result = studyPostCommandService.createPost(studyId, postPreviewDTO); // then assertNotNull(result); @@ -194,7 +194,7 @@ void createPost_Common_Success() { Long memberId = 1L; Long studyId = 1L; - StudyPostRequestDTO.PostDTO postPreviewDTO = StudyPostRequestDTO.PostDTO.builder() + StoryRequestDTO.PostDTO postPreviewDTO = StoryRequestDTO.PostDTO.builder() .isAnnouncement(false) .storyCategory(StoryCategory.FREE_TALK) .title("잡담") @@ -211,7 +211,7 @@ void createPost_Common_Success() { .thenReturn(List.of(member1Study, ownerStudy)); // when - StudyPostResDTO.PostPreviewDTO result = studyPostCommandService.createPost(studyId, postPreviewDTO); + StoryResDTO.PostPreviewDTO result = studyPostCommandService.createPost(studyId, postPreviewDTO); // then assertNotNull(result); @@ -227,7 +227,7 @@ void createPost_NotStudyMember_Fail() { Long memberId = 2L; Long studyId = 1L; - StudyPostRequestDTO.PostDTO postPreviewDTO = StudyPostRequestDTO.PostDTO.builder() + StoryRequestDTO.PostDTO postPreviewDTO = StoryRequestDTO.PostDTO.builder() .isAnnouncement(true) .storyCategory(StoryCategory.INFO_SHARING) .title("공지") @@ -255,7 +255,7 @@ void createPost_MemberAnnounced_Fail() { Long memberId = 2L; Long studyId = 1L; - StudyPostRequestDTO.PostDTO postPreviewDTO = StudyPostRequestDTO.PostDTO.builder() + StoryRequestDTO.PostDTO postPreviewDTO = StoryRequestDTO.PostDTO.builder() .isAnnouncement(true) .storyCategory(StoryCategory.INFO_SHARING) .title("공지") @@ -283,7 +283,7 @@ void createPost_TitleOverflow_Fail() { Long memberId = 1L; Long studyId = 1L; - StudyPostRequestDTO.PostDTO postPreviewDTO = StudyPostRequestDTO.PostDTO.builder() + StoryRequestDTO.PostDTO postPreviewDTO = StoryRequestDTO.PostDTO.builder() .isAnnouncement(true) .storyCategory(StoryCategory.INFO_SHARING) .title("50자가 넘어가는 제목 " @@ -327,7 +327,7 @@ void deletePost_Success() { .thenReturn(Optional.of(story1)); // when - StudyPostResDTO.PostPreviewDTO result = studyPostCommandService.deletePost(studyId, postId); + StoryResDTO.PostPreviewDTO result = studyPostCommandService.deletePost(studyId, postId); // then assertNotNull(result); @@ -398,7 +398,7 @@ void likePost_Success() { when(storyRepository.save(any(Story.class))).thenReturn(story1); // when - StudyPostResDTO.PostLikeNumDTO result = studyPostCommandService.likePost(studyId, postId); + StoryResDTO.PostLikeNumDTO result = studyPostCommandService.likePost(studyId, postId); // then assertNotNull(result); @@ -472,7 +472,7 @@ void cancelPostLike_Success() { when(storyRepository.save(any(Story.class))).thenReturn(story1); // when - StudyPostResDTO.PostLikeNumDTO result = studyPostCommandService.cancelPostLike(studyId, postId); + StoryResDTO.PostLikeNumDTO result = studyPostCommandService.cancelPostLike(studyId, postId); // then assertNotNull(result); @@ -537,7 +537,7 @@ void createComment_Anonymous_Success() { getAuthentication(memberId); - StudyPostCommentRequestDTO.CommentDTO commentDTO = StudyPostCommentRequestDTO.CommentDTO.builder() + StoryCommentRequestDTO.CommentDTO commentDTO = StoryCommentRequestDTO.CommentDTO.builder() .content("댓글") .isAnonymous(true) .build(); @@ -550,7 +550,7 @@ void createComment_Anonymous_Success() { when(storyCommentRepository.findAllByMemberIdAndStoryId(memberId, postId)).thenReturn(List.of()); // when - StudyPostCommentResponseDTO.CommentDTO result = studyPostCommandService.createComment(studyId, postId, commentDTO); + StoryCommentResponseDTO.CommentDTO result = studyPostCommandService.createComment(studyId, postId, commentDTO); // then assertNotNull(result); @@ -570,7 +570,7 @@ void createComment_Name_Success() { getAuthentication(memberId); - StudyPostCommentRequestDTO.CommentDTO commentDTO = StudyPostCommentRequestDTO.CommentDTO.builder() + StoryCommentRequestDTO.CommentDTO commentDTO = StoryCommentRequestDTO.CommentDTO.builder() .content("댓글") .isAnonymous(false) .build(); @@ -583,7 +583,7 @@ void createComment_Name_Success() { when(storyCommentRepository.findAllByMemberIdAndStoryId(memberId, postId)).thenReturn(List.of()); // when - StudyPostCommentResponseDTO.CommentDTO result = studyPostCommandService.createComment(studyId, postId, commentDTO); + StoryCommentResponseDTO.CommentDTO result = studyPostCommandService.createComment(studyId, postId, commentDTO); // then assertNotNull(result); @@ -603,7 +603,7 @@ void createComment_NotStudyMember_Fail() { getAuthentication(memberId); - StudyPostCommentRequestDTO.CommentDTO commentDTO = StudyPostCommentRequestDTO.CommentDTO.builder() + StoryCommentRequestDTO.CommentDTO commentDTO = StoryCommentRequestDTO.CommentDTO.builder() .content("댓글") .isAnonymous(false) .build(); @@ -635,7 +635,7 @@ void createReply_Anonymous_Success() { getAuthentication(memberId); - StudyPostCommentRequestDTO.CommentDTO commentDTO = StudyPostCommentRequestDTO.CommentDTO.builder() + StoryCommentRequestDTO.CommentDTO commentDTO = StoryCommentRequestDTO.CommentDTO.builder() .content("답글") .isAnonymous(true) .build(); @@ -650,7 +650,7 @@ void createReply_Anonymous_Success() { .thenReturn(List.of()); // when - StudyPostCommentResponseDTO.CommentDTO result = studyPostCommandService + StoryCommentResponseDTO.CommentDTO result = studyPostCommandService .createReply(studyId, postId, commentId, commentDTO); //then @@ -672,7 +672,7 @@ void createReply_Name_Success() { getAuthentication(memberId); - StudyPostCommentRequestDTO.CommentDTO commentDTO = StudyPostCommentRequestDTO.CommentDTO.builder() + StoryCommentRequestDTO.CommentDTO commentDTO = StoryCommentRequestDTO.CommentDTO.builder() .content("답글") .isAnonymous(false) .build(); @@ -687,7 +687,7 @@ void createReply_Name_Success() { .thenReturn(List.of()); // when - StudyPostCommentResponseDTO.CommentDTO result = studyPostCommandService + StoryCommentResponseDTO.CommentDTO result = studyPostCommandService .createReply(studyId, postId, commentId, commentDTO); //then @@ -709,7 +709,7 @@ void createReply_NotStudyMember_Fail() { getAuthentication(memberId); - StudyPostCommentRequestDTO.CommentDTO commentDTO = StudyPostCommentRequestDTO.CommentDTO.builder() + StoryCommentRequestDTO.CommentDTO commentDTO = StoryCommentRequestDTO.CommentDTO.builder() .content("답글") .isAnonymous(false) .build(); @@ -738,7 +738,7 @@ void createReply_ParentCommentNotExist_Fail() { getAuthentication(memberId); - StudyPostCommentRequestDTO.CommentDTO commentDTO = StudyPostCommentRequestDTO.CommentDTO.builder() + StoryCommentRequestDTO.CommentDTO commentDTO = StoryCommentRequestDTO.CommentDTO.builder() .content("답글") .isAnonymous(false) .build(); @@ -785,7 +785,7 @@ void likeComment_Success() { when(likedStoryCommentRepository.save(any(LikedStoryComment.class))).thenReturn(likedStoryComment); // when - StudyPostCommentResponseDTO.CommentPreviewDTO result = studyPostCommandService.likeComment(studyId, postId, commentId); + StoryCommentResponseDTO.CommentPreviewDTO result = studyPostCommandService.likeComment(studyId, postId, commentId); // then assertNotNull(result); @@ -860,7 +860,7 @@ void dislikeComment_Success() { when(likedStoryCommentRepository.save(any(LikedStoryComment.class))).thenReturn(likedStoryComment); // when - StudyPostCommentResponseDTO.CommentPreviewDTO result = studyPostCommandService.dislikeComment(studyId, postId, commentId); + StoryCommentResponseDTO.CommentPreviewDTO result = studyPostCommandService.dislikeComment(studyId, postId, commentId); // then assertNotNull(result); @@ -933,7 +933,7 @@ void cancelCommentLike_Success() { when(storyCommentRepository.save(any(StoryComment.class))).thenReturn(studyPost1Comment2); // when - StudyPostCommentResponseDTO.CommentPreviewDTO result = studyPostCommandService + StoryCommentResponseDTO.CommentPreviewDTO result = studyPostCommandService .cancelCommentLike(studyId, postId, commentId); // then @@ -1002,7 +1002,7 @@ void cancelCommentDislike() { when(storyCommentRepository.save(any(StoryComment.class))).thenReturn(studyPost1Comment2); // when - StudyPostCommentResponseDTO.CommentPreviewDTO result = studyPostCommandService + StoryCommentResponseDTO.CommentPreviewDTO result = studyPostCommandService .cancelCommentDislike(studyId, postId, commentId); // then diff --git a/src/test/java/com/example/spot/service/study/studypost/StoryQueryServiceTest.java b/src/test/java/com/example/spot/service/story/StoryQueryServiceTest.java similarity index 84% rename from src/test/java/com/example/spot/service/study/studypost/StoryQueryServiceTest.java rename to src/test/java/com/example/spot/service/story/StoryQueryServiceTest.java index 1101e204..35bca252 100644 --- a/src/test/java/com/example/spot/service/study/studypost/StoryQueryServiceTest.java +++ b/src/test/java/com/example/spot/service/story/StoryQueryServiceTest.java @@ -1,16 +1,18 @@ -package com.example.spot.service.study.studypost; +package com.example.spot.service.story; +import com.example.spot.common.api.exception.GeneralException; import com.example.spot.common.api.exception.handler.StudyHandler; import com.example.spot.member.domain.Member; import com.example.spot.story.domain.Story; -import com.example.spot.story.domain.aggregate.LikedStory; -import com.example.spot.story.domain.aggregate.LikedStoryComment; -import com.example.spot.story.domain.aggregate.StoryComment; +import com.example.spot.story.domain.association.LikedStory; +import com.example.spot.story.domain.association.LikedStoryComment; +import com.example.spot.story.domain.association.StoryComment; import com.example.spot.story.domain.enums.StoryCategory; import com.example.spot.story.domain.repository.LikedStoryRepository; import com.example.spot.story.domain.repository.StoryCommentRepository; import com.example.spot.story.domain.StoryRepository; -import com.example.spot.study.domain.aggregate.StudyMember; +import com.example.spot.story.web.dto.response.StoryResponseDTO; +import com.example.spot.study.domain.association.StudyMember; import com.example.spot.study.domain.enums.StudyApplicationStatus; import com.example.spot.member.domain.enums.Gender; import com.example.spot.story.domain.enums.StoryCategoryQuery; @@ -18,9 +20,9 @@ import com.example.spot.member.domain.MemberRepository; import com.example.spot.study.domain.repository.StudyMemberRepository; import com.example.spot.study.domain.StudyRepository; -import com.example.spot.study.application.StudyPostQueryServiceImpl; -import com.example.spot.study.presentation.dto.response.StudyPostCommentResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyPostResDTO; +import com.example.spot.story.application.StoryQueryServiceImpl; +import com.example.spot.story.web.dto.response.StoryCommentResponseDTO; +import com.example.spot.story.web.dto.response.StoryResDTO; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -66,7 +68,7 @@ class StoryQueryServiceTest { private StoryCommentRepository storyCommentRepository; @InjectMocks - private StudyPostQueryServiceImpl studyPostQueryService; + private StoryQueryServiceImpl studyPostQueryService; private static PageRequest pageRequest; @@ -149,7 +151,7 @@ void getAllPosts_All_Success() { .thenReturn(List.of(story1, story3)); // when - StudyPostResDTO.PostListDTO result = studyPostQueryService.getAllPosts(pageRequest, studyId, null); + StoryResDTO.PostListDTO result = studyPostQueryService.getAllPosts(pageRequest, studyId, null); // then assertNotNull(result); @@ -178,7 +180,7 @@ void getAllPosts_Theme_Success() { .thenReturn(List.of(story1, story3)); // when - StudyPostResDTO.PostListDTO result = studyPostQueryService.getAllPosts(pageRequest, studyId, StoryCategoryQuery.FREE_TALK); + StoryResDTO.PostListDTO result = studyPostQueryService.getAllPosts(pageRequest, studyId, StoryCategoryQuery.FREE_TALK); // then assertNotNull(result); @@ -207,7 +209,7 @@ void getAllPosts_Announcements_Success() { .thenReturn(List.of(story1, story3)); // when - StudyPostResDTO.PostListDTO result = studyPostQueryService.getAllPosts(pageRequest, studyId, StoryCategoryQuery.ANNOUNCEMENT); + StoryResDTO.PostListDTO result = studyPostQueryService.getAllPosts(pageRequest, studyId, StoryCategoryQuery.ANNOUNCEMENT); // then assertNotNull(result); @@ -286,7 +288,7 @@ void getPost_Common_Success() { .thenReturn(false); // when - StudyPostResDTO.PostDetailDTO result = studyPostQueryService.getPost(studyId, postId, false); + StoryResDTO.PostDetailDTO result = studyPostQueryService.getPost(studyId, postId, false); // then assertNotNull(result); @@ -319,7 +321,7 @@ void getPost_LikeOrScrap_Success() { .thenReturn(false); // when - StudyPostResDTO.PostDetailDTO result = studyPostQueryService.getPost(studyId, postId, true); + StoryResDTO.PostDetailDTO result = studyPostQueryService.getPost(studyId, postId, true); // then assertNotNull(result); @@ -375,7 +377,71 @@ void getPost_NotStudyPost_Fail() { assertThrows(StudyHandler.class, () ->studyPostQueryService.getPost(studyId, postId, false)); } -/*-------------------------------------------------------- 댓글 목록 조회 ------------------------------------------------------------------------*/ +/* ------------------------------------------------ 스터디 공지사항 조회 --------------------------------------------------- */ + + @Test + @DisplayName("스터디 공지사항 조회 - 성공") + void 스터디_공지사항_조회_성공(){ + + // given + long studyId = 1L; + String title = "공지"; + String content = "공지입니다."; + Story story = Story.builder() + .title(title) + .content(content) + .storyCategory(StoryCategory.WELCOME) + .isAnnouncement(true) + .build(); + + StudyMember studyMember = StudyMember.builder() + .introduction(title).study(study).member(owner).isOwned(true).status(StudyApplicationStatus.APPROVED).build(); + + when(storyRepository.findByStudyIdAndIsAnnouncement(studyId, true)).thenReturn(Optional.of(story)); + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, studyId, StudyApplicationStatus.APPROVED)).thenReturn( + Optional.ofNullable(studyMember)); + + // when + StoryResponseDTO responseDTO = studyPostQueryService.findStudyAnnouncementPost(studyId); + + // then + assertEquals(title,responseDTO.getTitle()); + assertEquals(content, responseDTO.getContent()); + } + + @Test + @DisplayName("스터디 공지사항 조회 - 로그인 한 회원이 해당 스터디 회원이 아닌 경우") + void 스터디_공지사항_조회_실패_1(){ + + // given + long studyId = 1L; + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, studyId, StudyApplicationStatus.APPROVED)).thenReturn( + Optional.empty()); + + // when & then + assertThrows(GeneralException.class, () -> studyPostQueryService.findStudyAnnouncementPost(studyId)); + } + + @Test + @DisplayName("스터디 공지사항 조회 - 스터디 공지 글이 없는 경우") + void 스터디_공지사항_조회_실패_2(){ + + // given + long studyId = 1L; + StudyMember studyMember = StudyMember.builder() + .introduction("title").study(study).member(owner).isOwned(true).status(StudyApplicationStatus.APPROVED).build(); + + + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, studyId, StudyApplicationStatus.APPROVED)).thenReturn( + Optional.ofNullable(studyMember)); + when(storyRepository.findByStudyIdAndIsAnnouncement(studyId, true)).thenReturn(Optional.empty()); + + // when & then + assertThrows(GeneralException.class, () -> studyPostQueryService.findStudyAnnouncementPost(studyId)); + } + + + /*-------------------------------------------------------- 댓글 목록 조회 ------------------------------------------------------------------------*/ @Test @DisplayName("스터디 게시글 댓글 목록 조회 - (성공)") @@ -394,7 +460,7 @@ void getAllComments_Success() { .thenReturn(List.of(studyPost1Comment1, studyPost1Comment2)); // when - StudyPostCommentResponseDTO.CommentReplyListDTO result = studyPostQueryService.getAllComments(studyId, postId); + StoryCommentResponseDTO.CommentReplyListDTO result = studyPostQueryService.getAllComments(studyId, postId); // then assertNotNull(result); diff --git a/src/test/java/com/example/spot/service/study/StudyCommandServiceTest.java b/src/test/java/com/example/spot/service/study/StudyCommandServiceTest.java index df0280fc..90227c7a 100644 --- a/src/test/java/com/example/spot/service/study/StudyCommandServiceTest.java +++ b/src/test/java/com/example/spot/service/study/StudyCommandServiceTest.java @@ -4,17 +4,17 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; -import com.example.spot.study.domain.aggregate.StudyRegion; +import com.example.spot.study.domain.association.StudyRegion; import com.example.spot.common.api.exception.handler.StudyHandler; import com.example.spot.member.domain.Member; -import com.example.spot.study.domain.aggregate.Region; -import com.example.spot.study.domain.aggregate.StudyMember; -import com.example.spot.study.domain.aggregate.Theme; +import com.example.spot.study.domain.association.Region; +import com.example.spot.study.domain.association.StudyMember; +import com.example.spot.study.domain.association.Theme; import com.example.spot.study.domain.enums.StudyApplicationStatus; import com.example.spot.member.domain.enums.Gender; import com.example.spot.study.domain.enums.StudyState; import com.example.spot.study.domain.enums.ThemeType; -import com.example.spot.study.domain.aggregate.StudyTheme; +import com.example.spot.study.domain.association.StudyTheme; import com.example.spot.study.domain.Study; import com.example.spot.member.domain.MemberRepository; import com.example.spot.study.domain.repository.StudyMemberRepository; diff --git a/src/test/java/com/example/spot/service/study/StudyQueryServiceTest.java b/src/test/java/com/example/spot/service/study/StudyQueryServiceTest.java index 8c0f11d8..4eff1be3 100644 --- a/src/test/java/com/example/spot/service/study/StudyQueryServiceTest.java +++ b/src/test/java/com/example/spot/service/study/StudyQueryServiceTest.java @@ -9,13 +9,13 @@ import static org.mockito.Mockito.when; import static org.mockito.internal.verification.VerificationModeFactory.times; -import com.example.spot.study.domain.aggregate.StudyRegion; +import com.example.spot.study.domain.association.StudyRegion; import com.example.spot.common.api.exception.handler.MemberHandler; import com.example.spot.common.api.exception.handler.StudyHandler; import com.example.spot.member.domain.Member; -import com.example.spot.study.domain.aggregate.Region; -import com.example.spot.study.domain.aggregate.StudyMember; -import com.example.spot.study.domain.aggregate.Theme; +import com.example.spot.study.domain.association.Region; +import com.example.spot.study.domain.association.StudyMember; +import com.example.spot.study.domain.association.Theme; import com.example.spot.member.domain.enums.Gender; import com.example.spot.member.domain.enums.Status; import com.example.spot.study.domain.enums.StudyApplicationStatus; @@ -26,7 +26,7 @@ import com.example.spot.member.domain.association.MemberTheme; import com.example.spot.member.domain.association.PreferredRegion; import com.example.spot.member.domain.association.PreferredStudy; -import com.example.spot.study.domain.aggregate.StudyTheme; +import com.example.spot.study.domain.association.StudyTheme; import com.example.spot.study.domain.Study; import com.example.spot.member.domain.MemberRepository; import com.example.spot.study.domain.repository.StudyMemberRepository; diff --git a/src/test/java/com/example/spot/service/study/studymember/StudyMemberCommandServiceTest.java b/src/test/java/com/example/spot/service/study/studymember/StudyMemberCommandServiceTest.java index ce3c08c8..388c2bd1 100644 --- a/src/test/java/com/example/spot/service/study/studymember/StudyMemberCommandServiceTest.java +++ b/src/test/java/com/example/spot/service/study/studymember/StudyMemberCommandServiceTest.java @@ -1,40 +1,23 @@ package com.example.spot.service.study.studymember; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.BDDMockito.*; -import static org.mockito.Mockito.when; - import com.example.spot.common.api.exception.handler.StudyHandler; import com.example.spot.member.domain.Member; -import com.example.spot.study.domain.aggregate.StudyMember; -import com.example.spot.todo.domain.ToDo; -import com.example.spot.study.domain.enums.StudyApplicationStatus; +import com.example.spot.member.domain.MemberRepository; import com.example.spot.member.domain.enums.Status; +import com.example.spot.study.application.StudyMemberCommandServiceImpl; import com.example.spot.study.domain.Study; -import com.example.spot.member.domain.MemberRepository; -import com.example.spot.study.domain.repository.StudyMemberRepository; import com.example.spot.study.domain.StudyRepository; -import com.example.spot.todo.domain.ToDoRepository; -import com.example.spot.study.application.MemberStudyCommandServiceImpl; -import com.example.spot.study.presentation.dto.request.ToDoListRequestDTO.ToDoListCreateDTO; -import com.example.spot.study.presentation.dto.response.ToDoListResponseDTO.ToDoListCreateResponseDTO; -import com.example.spot.study.presentation.dto.response.ToDoListResponseDTO.ToDoListUpdateResponseDTO; -import java.time.LocalDate; -import java.util.Collections; -import java.util.Optional; - +import com.example.spot.study.domain.association.StudyMember; +import com.example.spot.study.domain.enums.StudyApplicationStatus; +import com.example.spot.study.domain.repository.StudyMemberRepository; import com.example.spot.study.presentation.dto.response.StudyTerminationResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyWithdrawalResponseDTO; +import com.example.spot.todo.presentation.dto.request.ToDoListRequestDTO; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; @@ -43,12 +26,21 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import java.time.LocalDate; +import java.util.Collections; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) public class StudyMemberCommandServiceTest { @InjectMocks - private MemberStudyCommandServiceImpl memberStudyCommandService; + private StudyMemberCommandServiceImpl studyMemberCommandService; @Mock private StudyRepository studyRepository; @@ -59,9 +51,6 @@ public class StudyMemberCommandServiceTest { @Mock private MemberRepository memberRepository; - @Mock - private ToDoRepository toDoRepository; - @Mock private Study study; @@ -71,125 +60,21 @@ public class StudyMemberCommandServiceTest { @Mock private StudyMember studyMember; - @Mock - private ToDo toDo; - - private ToDoListCreateDTO requestDTO; + private ToDoListRequestDTO.ToDoListCreateDTO requestDTO; @BeforeEach void init() { - requestDTO = ToDoListCreateDTO.builder() + requestDTO = ToDoListRequestDTO.ToDoListCreateDTO.builder() .content("test") .date(LocalDate.EPOCH) .build(); - given(toDo.getStudy()).willReturn(study); - given(study.getId()).willReturn(1L); - given(toDo.getMember()).willReturn(member); - given(member.getId()).willReturn(1L); - Authentication authentication = new UsernamePasswordAuthenticationToken("1", null, Collections.emptyList()); SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); securityContext.setAuthentication(authentication); SecurityContextHolder.setContext(securityContext); } - /* ---------------------------- 진행중인 스터디 관련 메서드 ---------------------------- */ - - @Test - @DisplayName("스터디 탈퇴 - (성공)") - void withdrawFromStudy_Success() { - - // given - member = Member.builder() - .id(1L) - .name("회원1") - .build(); - study = Study.builder() - .id(1L) - .title("스터디") - .build(); - studyMember = StudyMember.builder() - .member(member) - .study(study) - .isOwned(false) - .status(StudyApplicationStatus.APPROVED) - .build(); - - when(memberRepository.findById(1L)).thenReturn(Optional.of(member)); - when(studyRepository.findById(1L)).thenReturn(Optional.of(study)); - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 1L, StudyApplicationStatus.APPROVED)) - .thenReturn(Optional.of(studyMember)); - - // when - StudyWithdrawalResponseDTO.WithdrawalDTO result = memberStudyCommandService.withdrawFromStudy(1L); - - // then - assertNotNull(result); - assertThat(result.getStudyId()).isEqualTo(1L); - assertThat(result.getStudyName()).isEqualTo("스터디"); - assertThat(result.getMemberId()).isEqualTo(1L); - assertThat(result.getMemberName()).isEqualTo("회원1"); - } - - @Test - @DisplayName("스터디 탈퇴 - 스터디 회원이 아닌 경우 (실패)") - void withdrawFromStudy_NotStudyMember_Fail() { - - // given - member = Member.builder() - .id(1L) - .name("회원1") - .build(); - study = Study.builder() - .id(1L) - .title("스터디") - .build(); - studyMember = StudyMember.builder() - .member(member) - .study(study) - .isOwned(false) - .status(StudyApplicationStatus.APPLIED) - .build(); - - when(memberRepository.findById(1L)).thenReturn(Optional.of(member)); - when(studyRepository.findById(1L)).thenReturn(Optional.of(study)); - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 1L, StudyApplicationStatus.APPROVED)) - .thenReturn(Optional.empty()); - - // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.withdrawFromStudy(1L)); - } - - @Test - @DisplayName("스터디 탈퇴 - 스터디장인 경우 (실패)") - void withdrawFromStudy_StudyOwner_Fail() { - - // given - member = Member.builder() - .id(1L) - .name("회원1") - .build(); - study = Study.builder() - .id(1L) - .title("스터디") - .build(); - studyMember = StudyMember.builder() - .member(member) - .study(study) - .isOwned(true) - .status(StudyApplicationStatus.APPROVED) - .build(); - - when(memberRepository.findById(1L)).thenReturn(Optional.of(member)); - when(studyRepository.findById(1L)).thenReturn(Optional.of(study)); - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 1L, StudyApplicationStatus.APPROVED)) - .thenReturn(Optional.of(studyMember)); - - // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.withdrawFromStudy(1L)); - } - @Test @DisplayName("스터디 종료 - (성공)") void terminateStudy_Success() { @@ -217,7 +102,7 @@ void terminateStudy_Success() { .thenReturn(Optional.of(studyMember)); // when - StudyTerminationResponseDTO.TerminationDTO result = memberStudyCommandService.terminateStudy(1L, "스터디 성과"); + StudyTerminationResponseDTO.TerminationDTO result = studyMemberCommandService.terminateStudy(1L, "스터디 성과"); // then assertNotNull(result); @@ -253,7 +138,7 @@ void terminateStudy_NotStudyOwner_Fail() { .thenReturn(Optional.of(studyMember)); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.terminateStudy(1L, "스터디 성과")); + assertThrows(StudyHandler.class, () -> studyMemberCommandService.terminateStudy(1L, "스터디 성과")); } @Test @@ -283,158 +168,8 @@ void terminateStudy_AlreadyTerminated_Fail() { .thenReturn(Optional.of(studyMember)); // when & then - assertThrows(StudyHandler.class, () -> memberStudyCommandService.terminateStudy(1L, "스터디 성과")); - } - - - /* ---------------------------- To-Do 생성 관련 메서드 ---------------------------- */ - - @Test - @DisplayName("To-Do 생성 - 성공") - void createToDoList() { - // given - when(studyRepository.findById(anyLong())).thenReturn(Optional.ofNullable(study)); - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(anyLong(), anyLong(), any())).thenReturn( - Optional.ofNullable(studyMember)); - when(memberRepository.findById(anyLong())).thenReturn(Optional.ofNullable(member)); - - when(toDoRepository.save(any())).thenReturn(toDo); - - // when - ToDoListCreateResponseDTO responseDTO = memberStudyCommandService.createToDoList(1L, requestDTO); - - // then - assertEquals(responseDTO.getContent(), requestDTO.getContent()); - } - - @Test - @DisplayName("To-Do 생성 - 스터디 회원이 아닌 경우") - void ToDo_생성_시_스터디_회원이_아닌_경우() { - // given - when(studyRepository.findById(anyLong())).thenReturn(Optional.ofNullable(study)); - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(anyLong(), anyLong(), any())).thenReturn( - Optional.empty()); - - - // when & then - assertThrows(StudyHandler.class, () -> { - memberStudyCommandService.createToDoList(1L, requestDTO); - }); - } - - /* ---------------------------- To-Do 수정 관련 메서드 ---------------------------- */ - - @Test - @DisplayName("To-Do 수정 - 성공") - void ToDo_수정_성공() { - // given - when(toDoRepository.findById(anyLong())).thenReturn(Optional.ofNullable(toDo)); - - // when - ToDoListUpdateResponseDTO responseDTO = memberStudyCommandService.updateToDoList(1L,1L, requestDTO); - - // then - assertEquals(false, responseDTO.isDone()); - - } - - @Test - @DisplayName("To-Do 수정 - To-Do가 없는 경우") - void ToDo_수정_시_ToDo가_없는_경우() { - // given - when(toDoRepository.findById(anyLong())).thenReturn(Optional.empty()); - - // when & then - assertThrows(StudyHandler.class, () -> { - memberStudyCommandService.updateToDoList(1L,1L, requestDTO); - }); - } - - @Test - @DisplayName("To-Do 수정 - To-Do가 다른 스터디의 것인 경우") - void ToDo_수정_시_ToDo가_다른_스터디의_것인_경우() { - // given - when(toDoRepository.findById(anyLong())).thenReturn(Optional.ofNullable(toDo)); - given(toDo.getStudy()).willReturn(Mockito.mock(Study.class)); - given(toDo.getStudy().getId()).willReturn(2L); - - // when & then - assertThrows(StudyHandler.class, () -> { - memberStudyCommandService.updateToDoList(1L,1L, requestDTO); - }); + assertThrows(StudyHandler.class, () -> studyMemberCommandService.terminateStudy(1L, "스터디 성과")); } - @Test - @DisplayName("To-Do 수정 - To-Do가 다른 회원의 것인 경우") - void ToDo_수정_시_ToDo가_다른_회원의_것인_경우() { - // given - when(toDoRepository.findById(anyLong())).thenReturn(Optional.ofNullable(toDo)); - given(toDo.getMember()).willReturn(Mockito.mock(Member.class)); - given(toDo.getMember().getId()).willReturn(2L); - - // when & then - assertThrows(StudyHandler.class, () -> { - memberStudyCommandService.updateToDoList(1L,1L, requestDTO); - }); - } - - /* ---------------------------- To-Do 삭제 관련 메서드 ---------------------------- */ - - @Test - @DisplayName("To-Do 삭제 - 성공") - void ToDo_삭제_성공() { - // given - when(toDoRepository.findById(anyLong())).thenReturn(Optional.ofNullable(toDo)); - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(anyLong(), anyLong(), any())).thenReturn( - Optional.ofNullable(studyMember)); - - // when - ToDoListUpdateResponseDTO responseDTO = memberStudyCommandService.deleteToDoList(1L, 1L); - - // then - verify(toDoRepository, times(1)).deleteById(1L); - } - - @Test - @DisplayName("To-Do 삭제 - To-Do가 없는 경우") - void ToDo_삭제_시_ToDo가_없는_경우() { - // given - when(toDoRepository.findById(anyLong())).thenReturn(Optional.empty()); - - // when & then - assertThrows(StudyHandler.class, () -> { - memberStudyCommandService.deleteToDoList(1L, 1L); - }); - } - - @Test - @DisplayName("To-Do 삭제 - To-Do가 다른 스터디의 것인 경우") - void ToDo_삭제_시_ToDo가_다른_스터디의_것인_경우() { - // given - when(toDoRepository.findById(anyLong())).thenReturn(Optional.ofNullable(toDo)); - given(toDo.getStudy()).willReturn(Mockito.mock(Study.class)); - given(toDo.getStudy().getId()).willReturn(2L); - - // when & then - assertThrows(StudyHandler.class, () -> { - memberStudyCommandService.deleteToDoList(1L, 1L); - }); - } - - @Test - @DisplayName("To-Do 삭제 - To-Do가 다른 회원의 것인 경우") - void ToDo_삭제_시_ToDo가_다른_회원의_것인_경우() { - // given - when(toDoRepository.findById(anyLong())).thenReturn(Optional.ofNullable(toDo)); - given(toDo.getMember()).willReturn(Mockito.mock(Member.class)); - given(toDo.getMember().getId()).willReturn(2L); - - // when & then - assertThrows(StudyHandler.class, () -> { - memberStudyCommandService.deleteToDoList(1L, 1L); - }); - } - - } diff --git a/src/test/java/com/example/spot/service/study/studymember/StudyMemberQueryServiceTest.java b/src/test/java/com/example/spot/service/study/studymember/StudyMemberQueryServiceTest.java index fcfde847..9b291732 100644 --- a/src/test/java/com/example/spot/service/study/studymember/StudyMemberQueryServiceTest.java +++ b/src/test/java/com/example/spot/service/study/studymember/StudyMemberQueryServiceTest.java @@ -2,71 +2,53 @@ import com.example.spot.common.api.exception.GeneralException; import com.example.spot.member.domain.Member; -import com.example.spot.story.domain.Story; -import com.example.spot.study.domain.aggregate.StudyMember; -import com.example.spot.schedule.domain.Schedule; -import com.example.spot.todo.domain.ToDo; -import com.example.spot.study.domain.enums.StudyApplicationStatus; -import com.example.spot.story.domain.enums.StoryCategory; -import com.example.spot.study.domain.Study; -import com.example.spot.member.domain.MemberRepository; -import com.example.spot.study.domain.repository.StudyMemberRepository; import com.example.spot.schedule.domain.ScheduleRepository; import com.example.spot.story.domain.StoryRepository; -import com.example.spot.todo.domain.ToDoRepository; -import com.example.spot.common.security.utils.SecurityUtils; -import com.example.spot.study.application.MemberStudyQueryServiceImpl; -import com.example.spot.study.presentation.dto.response.ToDoListResponseDTO.ToDoListSearchResponseDTO; +import com.example.spot.study.application.StudyMemberQueryServiceImpl; +import com.example.spot.study.domain.Study; +import com.example.spot.study.domain.association.StudyMember; +import com.example.spot.study.domain.enums.StudyApplicationStatus; +import com.example.spot.study.domain.repository.StudyMemberRepository; import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO; import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO.StudyApplicantDTO; import com.example.spot.study.presentation.dto.response.StudyMemberResponseDTO.StudyApplyMemberDTO; -import com.example.spot.study.presentation.dto.response.StudyPostResponseDTO; -import com.example.spot.study.presentation.dto.response.StudyScheduleResponseDTO; -import java.time.LocalDate; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - +import com.example.spot.todo.domain.ToDo; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.when; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) public class StudyMemberQueryServiceTest { @InjectMocks - private MemberStudyQueryServiceImpl memberStudyQueryService; + private StudyMemberQueryServiceImpl studyMemberQueryService; @Mock private StudyMemberRepository studyMemberRepository; - @Mock - private MemberRepository memberRepository; + @Mock private StoryRepository storyRepository; @Mock private ScheduleRepository scheduleRepository; - @Mock - private ToDoRepository toDoRepository; - @Mock - private SecurityUtils securityUtils; private static Member member; private static Member member2; @@ -84,14 +66,10 @@ void setup(){ member2 = Member.builder() .id(2L) .build(); - - study = Study.builder() .build(); - studyMember = StudyMember.builder() .introduction("title").study(study).member(member).isOwned(true).status(StudyApplicationStatus.APPROVED).build(); - apply = StudyMember.builder() .introduction("title").study(study).member(member).isOwned(false).status(StudyApplicationStatus.APPLIED).build(); studyMember2 = StudyMember.builder() @@ -111,116 +89,6 @@ void setup(){ SecurityContextHolder.setContext(securityContext); } - /* ------------------------------------------------ 스터디 공지사항 조회 --------------------------------------------------- */ - - @Test - @DisplayName("스터디 공지사항 조회 - 성공") - void 스터디_공지사항_조회_성공(){ - - // given - long studyId = 1L; - String title = "공지"; - String content = "공지입니다."; - Story story = Story.builder() - .title(title) - .content(content) - .storyCategory(StoryCategory.WELCOME) - .isAnnouncement(true) - .build(); - - StudyMember studyMember = StudyMember.builder() - .introduction(title).study(study).member(member).isOwned(true).status(StudyApplicationStatus.APPROVED).build(); - - when(storyRepository.findByStudyIdAndIsAnnouncement(studyId, true)).thenReturn(Optional.of(story)); - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, studyId, StudyApplicationStatus.APPROVED)).thenReturn( - Optional.ofNullable(studyMember)); - - // when - StudyPostResponseDTO responseDTO = memberStudyQueryService.findStudyAnnouncementPost(studyId); - - // then - assertEquals(title,responseDTO.getTitle()); - assertEquals(content, responseDTO.getContent()); - } - - @Test - @DisplayName("스터디 공지사항 조회 - 로그인 한 회원이 해당 스터디 회원이 아닌 경우") - void 스터디_공지사항_조회_실패_1(){ - - // given - long studyId = 1L; - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, studyId, StudyApplicationStatus.APPROVED)).thenReturn( - Optional.empty()); - - // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.findStudyAnnouncementPost(studyId)); - } - - @Test - @DisplayName("스터디 공지사항 조회 - 스터디 공지 글이 없는 경우") - void 스터디_공지사항_조회_실패_2(){ - - // given - long studyId = 1L; - StudyMember studyMember = StudyMember.builder() - .introduction("title").study(study).member(member).isOwned(true).status(StudyApplicationStatus.APPROVED).build(); - - - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, studyId, StudyApplicationStatus.APPROVED)).thenReturn( - Optional.ofNullable(studyMember)); - when(storyRepository.findByStudyIdAndIsAnnouncement(studyId, true)).thenReturn(Optional.empty()); - - // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.findStudyAnnouncementPost(studyId)); - } - - /* ------------------------------------------------ 스터디 모임 목록 조회 --------------------------------------------------- */ - - @Test - @DisplayName("스터디 모임 목록 조회 - 성공") - void 스터디_모임_목록_조회_성공(){ - // given - Long studyId = 1L; - String title = "title"; - - Schedule schedule1 = Schedule.builder().id(1L).title(title).build(); - Schedule schedule2 = Schedule.builder().id(2L).title("title1").build(); - when(scheduleRepository.findAllByStudyId(studyId, Pageable.unpaged())).thenReturn(List.of(schedule1, schedule2)); - - // when - StudyScheduleResponseDTO responseDTO = memberStudyQueryService.findStudySchedule(studyId, Pageable.unpaged()); - - // then - assertEquals(2, responseDTO.getTotalElements()); - assertEquals(title, responseDTO.getSchedules().get(0).getTitle()); - } - - @Test - @DisplayName("스터디 모임 목록 조회 - 로그인 한 회원이 해당 스터디 회원이 아닌 경우") - void 스터디_모임_목록_조회_실패_1(){ - // given - long studyId = 1L; - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, studyId, StudyApplicationStatus.APPROVED)).thenReturn( - Optional.empty()); - - // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.findStudySchedule(studyId, Pageable.unpaged())); - } - - @Test - @DisplayName("스터디 모임 목록 조회 - 스터디 모임 일정이 존재하지 않는 경우") - void 스터디_모임_목록_조회_실패_2(){ - // given - long studyId = 1L; - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, studyId, StudyApplicationStatus.APPROVED)).thenReturn( - Optional.of(studyMember)); - when(scheduleRepository.findAllByStudyId(studyId, Pageable.unpaged())).thenReturn(Collections.emptyList()); - - // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.findStudySchedule(studyId, Pageable.unpaged())); - } - - /* ------------------------------------------------ 특정 스터디 회원 목록 전체 조회 --------------------------------------------------- */ @Test @@ -232,7 +100,7 @@ void setup(){ when(studyMemberRepository.findAllByStudyIdAndStatus(studyId, StudyApplicationStatus.APPROVED)).thenReturn(List.of(studyMember)); // when - StudyMemberResponseDTO responseDTO = memberStudyQueryService.findStudyMembers(studyId); + StudyMemberResponseDTO responseDTO = studyMemberQueryService.findStudyMembers(studyId); // then assertEquals(1, responseDTO.getTotalElements()); @@ -249,7 +117,7 @@ void setup(){ when(studyMemberRepository.findAllByStudyIdAndStatus(studyId, StudyApplicationStatus.APPROVED)).thenReturn(List.of()); // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.findStudyMembers(studyId)); + assertThrows(GeneralException.class, () -> studyMemberQueryService.findStudyMembers(studyId)); } @@ -266,7 +134,7 @@ void setup(){ .thenReturn(List.of(apply)); // when - StudyMemberResponseDTO responseDTO = memberStudyQueryService.findStudyApplicants(1L); + StudyMemberResponseDTO responseDTO = studyMemberQueryService.findStudyApplicants(1L); // then assertEquals(1, responseDTO.getTotalElements()); @@ -282,7 +150,7 @@ void setup(){ Optional.empty()); // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.findStudyApplicants(1L)); + assertThrows(GeneralException.class, () -> studyMemberQueryService.findStudyApplicants(1L)); } @Test @@ -296,7 +164,7 @@ void setup(){ .thenReturn(List.of()); // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.findStudyApplicants(1L)); + assertThrows(GeneralException.class, () -> studyMemberQueryService.findStudyApplicants(1L)); } @@ -312,7 +180,7 @@ void setup(){ .thenReturn(Optional.ofNullable(apply)); // when - StudyApplyMemberDTO responseDTO = memberStudyQueryService.findStudyApplication(100L, 1L); + StudyApplyMemberDTO responseDTO = studyMemberQueryService.findStudyApplication(100L, 1L); // then assertEquals(1L, responseDTO.getMemberId()); @@ -326,7 +194,7 @@ void setup(){ Optional.empty()); // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.findStudyApplication(100L, 1L)); + assertThrows(GeneralException.class, () -> studyMemberQueryService.findStudyApplication(100L, 1L)); } @Test @@ -340,7 +208,7 @@ void setup(){ .thenReturn(Optional.empty()); // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.findStudyApplication(100L, 1L)); + assertThrows(GeneralException.class, () -> studyMemberQueryService.findStudyApplication(100L, 1L)); } @Test @@ -354,7 +222,7 @@ void setup(){ .thenReturn(Optional.ofNullable(studyMember)); // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.findStudyApplication(100L, 1L)); + assertThrows(GeneralException.class, () -> studyMemberQueryService.findStudyApplication(100L, 1L)); } @@ -370,7 +238,7 @@ void setup(){ .thenReturn(true); // when - StudyApplicantDTO responseDTO = memberStudyQueryService.isApplied(100L); + StudyApplicantDTO responseDTO = studyMemberQueryService.isApplied(100L); // then assertEquals(100L, responseDTO.getStudyId()); @@ -384,112 +252,6 @@ void setup(){ when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 100L, StudyApplicationStatus.APPROVED)) .thenReturn(Optional.ofNullable(studyMember)); // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.isApplied(100L)); - } - - /* ------------------------------------------------ To-Do 조회 --------------------------------------------------- */ - - @Test - @DisplayName("To-Do 조회 - 성공") - void ToDo_조회_성공() { - // given - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 100L, StudyApplicationStatus.APPROVED)) - .thenReturn(Optional.ofNullable(studyMember)); - when(toDoRepository.findByStudyIdAndMemberIdAndDateOrderByCreatedAtDesc(anyLong(), anyLong(), any(), any())) - .thenReturn(List.of(toDo)); - when(toDoRepository.countByStudyIdAndMemberIdAndDate(anyLong(), anyLong(), any())) - .thenReturn(1L); - - // when - ToDoListSearchResponseDTO responseDTO = memberStudyQueryService.getToDoList(1L, LocalDate.MAX, PageRequest.of(0, 10)); - - // then - assertEquals(1, responseDTO.getTotalElements()); - assertEquals(1L, responseDTO.getContent().get(0).getId()); - } - - @Test - @DisplayName("To-Do 조회 - 로그인 한 회원이 스터디 회원이 아닌 경우") - void ToDo_조회_시_로그인_한_회원이_스터디_회원이_아닌_경우() { - // given - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 100L, StudyApplicationStatus.APPROVED)) - .thenReturn(Optional.empty()); - - // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.getToDoList(100L, LocalDate.MAX, PageRequest.of(0, 10))); - } - - @Test - @DisplayName("To-Do 조회 - 회원의 To-Do가 존재하지 않는 경우") - void ToDo가_존재하지_않는_경우() { - // given - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 100L, StudyApplicationStatus.APPROVED)) - .thenReturn(Optional.ofNullable(studyMember)); - when(toDoRepository.findByStudyIdAndMemberIdAndDateOrderByCreatedAtDesc(anyLong(), anyLong(), any(), any())) - .thenReturn(List.of()); - - // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.getToDoList(100L, LocalDate.MAX, PageRequest.of(0, 10))); + assertThrows(GeneralException.class, () -> studyMemberQueryService.isApplied(100L)); } - - /* ------------------------------------------------ 다른 스터디원의 To-Do 조회 --------------------------------------------------- */ - - @Test - @DisplayName("특정 스터디 원 To-Do 조회 - 성공") - void 스터디_원_ToDo_조회_성공() { - // given - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 1L, StudyApplicationStatus.APPROVED)) - .thenReturn(Optional.ofNullable(studyMember)); - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(2L, 1L, StudyApplicationStatus.APPROVED)) - .thenReturn(Optional.ofNullable(studyMember)); - when(toDoRepository.findByStudyIdAndMemberIdAndDateOrderByCreatedAtDesc(anyLong(), anyLong(), any(), any())) - .thenReturn(List.of(toDo)); - when(toDoRepository.countByStudyIdAndMemberIdAndDate(anyLong(), anyLong(), any())) - .thenReturn(1L); - - // when - ToDoListSearchResponseDTO responseDTO = memberStudyQueryService.getMemberToDoList(1L, 2L, LocalDate.MAX, PageRequest.of(0, 10)); - - // then - assertEquals(1, responseDTO.getTotalElements()); - assertEquals(1L, responseDTO.getContent().get(0).getId()); - } - - @Test - @DisplayName("특정 스터디 원 To-Do 조회 - 로그인 한 회원이 스터디 회원이 아닌 경우") - void 스터디_원_ToDo_조회_시_로그인_한_회원이_스터디_회원이_아닌_경우() { - // given - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 100L, StudyApplicationStatus.APPROVED)) - .thenReturn(Optional.empty()); - - // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.getMemberToDoList(100L, 2L, LocalDate.MAX, PageRequest.of(0, 10))); - } - - @Test - @DisplayName("특정 스터디 원 To-Do 조회 - 조회 하려는 회원이 스터디 회원이 아닌 경우") - void 스터디_원_ToDo_조회_시_조회_하려는_회원이_스터디_회원이_아닌_경우() { - // given - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 100L, StudyApplicationStatus.APPROVED)) - .thenReturn(Optional.ofNullable(studyMember)); - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(2L, 100L, StudyApplicationStatus.APPROVED)) - .thenReturn(Optional.empty()); - - // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.getMemberToDoList(100L, 2L, LocalDate.MAX, PageRequest.of(0, 10))); - } - - @Test - @DisplayName("특정 스터디 원 To-Do 조회 - 회원의 To-Do가 존재하지 않는 경우") - void 스터디_원_ToDo가_존재하지_않는_경우() { - // given - when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 100L, StudyApplicationStatus.APPROVED)) - .thenReturn(Optional.ofNullable(studyMember)); - when(toDoRepository.findByStudyIdAndMemberIdAndDateOrderByCreatedAtDesc(anyLong(), anyLong(), any(), any())) - .thenReturn(List.of()); - - // when & then - assertThrows(GeneralException.class, () -> memberStudyQueryService.getToDoList(100L, LocalDate.MAX, PageRequest.of(0, 10))); - } - } diff --git a/src/test/java/com/example/spot/service/todo/ToDoCommandServiceTest.java b/src/test/java/com/example/spot/service/todo/ToDoCommandServiceTest.java new file mode 100644 index 00000000..ea7d9f99 --- /dev/null +++ b/src/test/java/com/example/spot/service/todo/ToDoCommandServiceTest.java @@ -0,0 +1,248 @@ +package com.example.spot.service.todo; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.when; + +import com.example.spot.common.api.exception.handler.StudyHandler; +import com.example.spot.member.domain.Member; +import com.example.spot.study.domain.association.StudyMember; +import com.example.spot.todo.application.ToDoCommandServiceImpl; +import com.example.spot.todo.domain.ToDo; +import com.example.spot.study.domain.enums.StudyApplicationStatus; +import com.example.spot.member.domain.enums.Status; +import com.example.spot.study.domain.Study; +import com.example.spot.member.domain.MemberRepository; +import com.example.spot.study.domain.repository.StudyMemberRepository; +import com.example.spot.study.domain.StudyRepository; +import com.example.spot.todo.domain.ToDoRepository; +import com.example.spot.todo.presentation.dto.request.ToDoListRequestDTO.ToDoListCreateDTO; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO.ToDoListCreateResponseDTO; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO.ToDoListUpdateResponseDTO; +import java.time.LocalDate; +import java.util.Collections; +import java.util.Optional; + +import com.example.spot.study.presentation.dto.response.StudyTerminationResponseDTO; +import com.example.spot.study.presentation.dto.response.StudyWithdrawalResponseDTO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class ToDoCommandServiceTest { + + @InjectMocks + private ToDoCommandServiceImpl toDoCommandService; + + @Mock + private StudyRepository studyRepository; + + @Mock + private StudyMemberRepository studyMemberRepository; + + @Mock + private MemberRepository memberRepository; + + @Mock + private ToDoRepository toDoRepository; + + @Mock + private Study study; + + @Mock + private Member member; + + @Mock + private StudyMember studyMember; + + @Mock + private ToDo toDo; + + private ToDoListCreateDTO requestDTO; + + @BeforeEach + void init() { + requestDTO = ToDoListCreateDTO.builder() + .content("test") + .date(LocalDate.EPOCH) + .build(); + + given(toDo.getStudy()).willReturn(study); + given(study.getId()).willReturn(1L); + given(toDo.getMember()).willReturn(member); + given(member.getId()).willReturn(1L); + + Authentication authentication = new UsernamePasswordAuthenticationToken("1", null, Collections.emptyList()); + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(authentication); + SecurityContextHolder.setContext(securityContext); + } + + + /* ---------------------------- To-Do 생성 관련 메서드 ---------------------------- */ + + @Test + @DisplayName("To-Do 생성 - 성공") + void createToDoList() { + // given + when(studyRepository.findById(anyLong())).thenReturn(Optional.ofNullable(study)); + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(anyLong(), anyLong(), any())).thenReturn( + Optional.ofNullable(studyMember)); + when(memberRepository.findById(anyLong())).thenReturn(Optional.ofNullable(member)); + + when(toDoRepository.save(any())).thenReturn(toDo); + + // when + ToDoListCreateResponseDTO responseDTO = toDoCommandService.createToDoList(1L, requestDTO); + + // then + assertEquals(responseDTO.getContent(), requestDTO.getContent()); + } + + @Test + @DisplayName("To-Do 생성 - 스터디 회원이 아닌 경우") + void ToDo_생성_시_스터디_회원이_아닌_경우() { + // given + when(studyRepository.findById(anyLong())).thenReturn(Optional.ofNullable(study)); + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(anyLong(), anyLong(), any())).thenReturn( + Optional.empty()); + + + // when & then + assertThrows(StudyHandler.class, () -> { + toDoCommandService.createToDoList(1L, requestDTO); + }); + } + + /* ---------------------------- To-Do 수정 관련 메서드 ---------------------------- */ + + @Test + @DisplayName("To-Do 수정 - 성공") + void ToDo_수정_성공() { + // given + when(toDoRepository.findById(anyLong())).thenReturn(Optional.ofNullable(toDo)); + + // when + ToDoListUpdateResponseDTO responseDTO = toDoCommandService.updateToDoList(1L,1L, requestDTO); + + // then + assertEquals(false, responseDTO.isDone()); + + } + + @Test + @DisplayName("To-Do 수정 - To-Do가 없는 경우") + void ToDo_수정_시_ToDo가_없는_경우() { + // given + when(toDoRepository.findById(anyLong())).thenReturn(Optional.empty()); + + // when & then + assertThrows(StudyHandler.class, () -> { + toDoCommandService.updateToDoList(1L,1L, requestDTO); + }); + } + + @Test + @DisplayName("To-Do 수정 - To-Do가 다른 스터디의 것인 경우") + void ToDo_수정_시_ToDo가_다른_스터디의_것인_경우() { + // given + when(toDoRepository.findById(anyLong())).thenReturn(Optional.ofNullable(toDo)); + given(toDo.getStudy()).willReturn(Mockito.mock(Study.class)); + given(toDo.getStudy().getId()).willReturn(2L); + + // when & then + assertThrows(StudyHandler.class, () -> { + toDoCommandService.updateToDoList(1L,1L, requestDTO); + }); + } + + @Test + @DisplayName("To-Do 수정 - To-Do가 다른 회원의 것인 경우") + void ToDo_수정_시_ToDo가_다른_회원의_것인_경우() { + // given + when(toDoRepository.findById(anyLong())).thenReturn(Optional.ofNullable(toDo)); + given(toDo.getMember()).willReturn(Mockito.mock(Member.class)); + given(toDo.getMember().getId()).willReturn(2L); + + // when & then + assertThrows(StudyHandler.class, () -> { + toDoCommandService.updateToDoList(1L,1L, requestDTO); + }); + } + + /* ---------------------------- To-Do 삭제 관련 메서드 ---------------------------- */ + + @Test + @DisplayName("To-Do 삭제 - 성공") + void ToDo_삭제_성공() { + // given + when(toDoRepository.findById(anyLong())).thenReturn(Optional.ofNullable(toDo)); + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(anyLong(), anyLong(), any())).thenReturn( + Optional.ofNullable(studyMember)); + + // when + ToDoListUpdateResponseDTO responseDTO = toDoCommandService.deleteToDoList(1L, 1L); + + // then + verify(toDoRepository, times(1)).deleteById(1L); + } + + @Test + @DisplayName("To-Do 삭제 - To-Do가 없는 경우") + void ToDo_삭제_시_ToDo가_없는_경우() { + // given + when(toDoRepository.findById(anyLong())).thenReturn(Optional.empty()); + + // when & then + assertThrows(StudyHandler.class, () -> { + toDoCommandService.deleteToDoList(1L, 1L); + }); + } + + @Test + @DisplayName("To-Do 삭제 - To-Do가 다른 스터디의 것인 경우") + void ToDo_삭제_시_ToDo가_다른_스터디의_것인_경우() { + // given + when(toDoRepository.findById(anyLong())).thenReturn(Optional.ofNullable(toDo)); + given(toDo.getStudy()).willReturn(Mockito.mock(Study.class)); + given(toDo.getStudy().getId()).willReturn(2L); + + // when & then + assertThrows(StudyHandler.class, () -> { + toDoCommandService.deleteToDoList(1L, 1L); + }); + } + + @Test + @DisplayName("To-Do 삭제 - To-Do가 다른 회원의 것인 경우") + void ToDo_삭제_시_ToDo가_다른_회원의_것인_경우() { + // given + when(toDoRepository.findById(anyLong())).thenReturn(Optional.ofNullable(toDo)); + given(toDo.getMember()).willReturn(Mockito.mock(Member.class)); + given(toDo.getMember().getId()).willReturn(2L); + + // when & then + assertThrows(StudyHandler.class, () -> { + toDoCommandService.deleteToDoList(1L, 1L); + }); + } + + + +} diff --git a/src/test/java/com/example/spot/service/todo/ToDoQueryServiceTest.java b/src/test/java/com/example/spot/service/todo/ToDoQueryServiceTest.java new file mode 100644 index 00000000..4aec7331 --- /dev/null +++ b/src/test/java/com/example/spot/service/todo/ToDoQueryServiceTest.java @@ -0,0 +1,198 @@ +package com.example.spot.service.todo; + +import com.example.spot.common.api.exception.GeneralException; +import com.example.spot.member.domain.Member; +import com.example.spot.study.domain.association.StudyMember; +import com.example.spot.todo.application.ToDoQueryServiceImpl; +import com.example.spot.todo.domain.ToDo; +import com.example.spot.study.domain.enums.StudyApplicationStatus; +import com.example.spot.study.domain.Study; +import com.example.spot.study.domain.repository.StudyMemberRepository; +import com.example.spot.todo.domain.ToDoRepository; +import com.example.spot.todo.presentation.dto.response.ToDoListResponseDTO.ToDoListSearchResponseDTO; +import java.time.LocalDate; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.data.domain.PageRequest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class ToDoQueryServiceTest { + + @InjectMocks + private ToDoQueryServiceImpl toDoQueryService; + + @Mock + private StudyMemberRepository studyMemberRepository; + @Mock + private ToDoRepository toDoRepository; + + private static Member member; + private static Member member2; + private static Study study; + private static StudyMember studyMember; + private static StudyMember studyMember2; + private static StudyMember apply; + private static ToDo toDo; + + @BeforeEach + void setup(){ + member = Member.builder() + .id(1L) + .build(); + member2 = Member.builder() + .id(2L) + .build(); + + + study = Study.builder() + .build(); + + studyMember = StudyMember.builder() + .introduction("title").study(study).member(member).isOwned(true).status(StudyApplicationStatus.APPROVED).build(); + + apply = StudyMember.builder() + .introduction("title").study(study).member(member).isOwned(false).status(StudyApplicationStatus.APPLIED).build(); + studyMember2 = StudyMember.builder() + .introduction("title").study(study).member(member2).isOwned(true).status(StudyApplicationStatus.APPROVED).build(); + toDo = ToDo.builder() + .id(1L) + .build(); + + Long studyId = 1L; + + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, studyId, StudyApplicationStatus.APPROVED)).thenReturn( + Optional.ofNullable(studyMember)); + Authentication authentication = new UsernamePasswordAuthenticationToken("1", null, Collections.emptyList()); + // SecurityContext 생성 및 설정 + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(authentication); + SecurityContextHolder.setContext(securityContext); + } + +/* ------------------------------------------------ To-Do 조회 --------------------------------------------------- */ + + @Test + @DisplayName("To-Do 조회 - 성공") + void ToDo_조회_성공() { + // given + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 100L, StudyApplicationStatus.APPROVED)) + .thenReturn(Optional.ofNullable(studyMember)); + when(toDoRepository.findByStudyIdAndMemberIdAndDateOrderByCreatedAtDesc(anyLong(), anyLong(), any(), any())) + .thenReturn(List.of(toDo)); + when(toDoRepository.countByStudyIdAndMemberIdAndDate(anyLong(), anyLong(), any())) + .thenReturn(1L); + + // when + ToDoListSearchResponseDTO responseDTO = toDoQueryService.getToDoList(1L, LocalDate.MAX, PageRequest.of(0, 10)); + + // then + assertEquals(1, responseDTO.getTotalElements()); + assertEquals(1L, responseDTO.getContent().get(0).getId()); + } + + @Test + @DisplayName("To-Do 조회 - 로그인 한 회원이 스터디 회원이 아닌 경우") + void ToDo_조회_시_로그인_한_회원이_스터디_회원이_아닌_경우() { + // given + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 100L, StudyApplicationStatus.APPROVED)) + .thenReturn(Optional.empty()); + + // when & then + assertThrows(GeneralException.class, () -> toDoQueryService.getToDoList(100L, LocalDate.MAX, PageRequest.of(0, 10))); + } + + @Test + @DisplayName("To-Do 조회 - 회원의 To-Do가 존재하지 않는 경우") + void ToDo가_존재하지_않는_경우() { + // given + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 100L, StudyApplicationStatus.APPROVED)) + .thenReturn(Optional.ofNullable(studyMember)); + when(toDoRepository.findByStudyIdAndMemberIdAndDateOrderByCreatedAtDesc(anyLong(), anyLong(), any(), any())) + .thenReturn(List.of()); + + // when & then + assertThrows(GeneralException.class, () -> toDoQueryService.getToDoList(100L, LocalDate.MAX, PageRequest.of(0, 10))); + } + + /* ------------------------------------------------ 다른 스터디원의 To-Do 조회 --------------------------------------------------- */ + + @Test + @DisplayName("특정 스터디 원 To-Do 조회 - 성공") + void 스터디_원_ToDo_조회_성공() { + // given + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 1L, StudyApplicationStatus.APPROVED)) + .thenReturn(Optional.ofNullable(studyMember)); + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(2L, 1L, StudyApplicationStatus.APPROVED)) + .thenReturn(Optional.ofNullable(studyMember)); + when(toDoRepository.findByStudyIdAndMemberIdAndDateOrderByCreatedAtDesc(anyLong(), anyLong(), any(), any())) + .thenReturn(List.of(toDo)); + when(toDoRepository.countByStudyIdAndMemberIdAndDate(anyLong(), anyLong(), any())) + .thenReturn(1L); + + // when + ToDoListSearchResponseDTO responseDTO = toDoQueryService.getMemberToDoList(1L, 2L, LocalDate.MAX, PageRequest.of(0, 10)); + + // then + assertEquals(1, responseDTO.getTotalElements()); + assertEquals(1L, responseDTO.getContent().get(0).getId()); + } + + @Test + @DisplayName("특정 스터디 원 To-Do 조회 - 로그인 한 회원이 스터디 회원이 아닌 경우") + void 스터디_원_ToDo_조회_시_로그인_한_회원이_스터디_회원이_아닌_경우() { + // given + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 100L, StudyApplicationStatus.APPROVED)) + .thenReturn(Optional.empty()); + + // when & then + assertThrows(GeneralException.class, () -> toDoQueryService.getMemberToDoList(100L, 2L, LocalDate.MAX, PageRequest.of(0, 10))); + } + + @Test + @DisplayName("특정 스터디 원 To-Do 조회 - 조회 하려는 회원이 스터디 회원이 아닌 경우") + void 스터디_원_ToDo_조회_시_조회_하려는_회원이_스터디_회원이_아닌_경우() { + // given + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 100L, StudyApplicationStatus.APPROVED)) + .thenReturn(Optional.ofNullable(studyMember)); + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(2L, 100L, StudyApplicationStatus.APPROVED)) + .thenReturn(Optional.empty()); + + // when & then + assertThrows(GeneralException.class, () -> toDoQueryService.getMemberToDoList(100L, 2L, LocalDate.MAX, PageRequest.of(0, 10))); + } + + @Test + @DisplayName("특정 스터디 원 To-Do 조회 - 회원의 To-Do가 존재하지 않는 경우") + void 스터디_원_ToDo가_존재하지_않는_경우() { + // given + when(studyMemberRepository.findByMemberIdAndStudyIdAndStatus(1L, 100L, StudyApplicationStatus.APPROVED)) + .thenReturn(Optional.ofNullable(studyMember)); + when(toDoRepository.findByStudyIdAndMemberIdAndDateOrderByCreatedAtDesc(anyLong(), anyLong(), any(), any())) + .thenReturn(List.of()); + + // when & then + assertThrows(GeneralException.class, () -> toDoQueryService.getToDoList(100L, LocalDate.MAX, PageRequest.of(0, 10))); + } + +}