diff --git a/src/main/java/com/example/spot/api/code/status/ErrorStatus.java b/src/main/java/com/example/spot/api/code/status/ErrorStatus.java index 37862f98..6687eb99 100644 --- a/src/main/java/com/example/spot/api/code/status/ErrorStatus.java +++ b/src/main/java/com/example/spot/api/code/status/ErrorStatus.java @@ -75,6 +75,8 @@ public enum ErrorStatus implements BaseErrorCode { _ONLY_STUDY_MEMBER_CAN_ACCESS_SCHEDULE(HttpStatus.FORBIDDEN, "STUDY4015", "스터디 멤버만 일정에 접근할 수 있습니다."), _ONLY_STUDY_MEMBER_CAN_ACCESS_MEMBERS(HttpStatus.FORBIDDEN, "STUDY4016", "스터디 멤버만 회원 목록에 접근할 수 있습니다."), _ALREADY_STUDY_MEMBER(HttpStatus.BAD_REQUEST, "STUDY4017", "이미 스터디 멤버입니다."), + _STUDY_OWNER_ONLY_CAN_TERMINATE(HttpStatus.BAD_REQUEST, "STUDY4018", "스터디장만 스터디를 종료할 수 있습니다."), + _STUDY_ALREADY_TERMINATED(HttpStatus.BAD_REQUEST, "STUDY4019", "이미 종료된 스터디입니다."), //스터디 게시글 관련 에러 _STUDY_POST_NOT_FOUND(HttpStatus.NOT_FOUND, "POST4001", "스터디 게시글을 찾을 수 없습니다."), @@ -90,7 +92,7 @@ public enum ErrorStatus implements BaseErrorCode { _STUDY_POST_COMMENT_ALREADY_DISLIKED(HttpStatus.BAD_REQUEST, "POST4011", "이미 싫어요 한 댓글입니다."), _STUDY_LIKED_COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "POST4012", "좋아요를 누르지 않은 게시글의 좋아요를 취소할 수 없습니다."), _STUDY_DISLIKED_COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "POST4013", "싫어요를 누르지 않은 게시글의 싫어요를 취소할 수 없습니다."), - _STUDY_POST_DELETION_INVALID(HttpStatus.FORBIDDEN, "POST4014", "게시글 작성자만 삭제 가능합니다."), + _STUDY_POST_DELETION_INVALID(HttpStatus.FORBIDDEN, "POST4014", "게시글 작성자 및 스터디장만 삭제 가능합니다."), _STUDY_POST_NULL(HttpStatus.BAD_REQUEST, "POST4015", "게시글 아이디가 입력되지 않았습니다."), _STUDY_POST_COMMENT_NULL(HttpStatus.BAD_REQUEST, "POST4016", "댓글 아이디가 입력되지 않았습니다."), _STUDY_POST_COMMENT_REACTIOM_ID_NULL(HttpStatus.BAD_REQUEST, "POST4017", "댓글 반응 아이디가 입력되지 않았습니다."), diff --git a/src/main/java/com/example/spot/domain/mapping/StudyLikedComment.java b/src/main/java/com/example/spot/domain/mapping/StudyLikedComment.java index d87d7640..a9171ff0 100644 --- a/src/main/java/com/example/spot/domain/mapping/StudyLikedComment.java +++ b/src/main/java/com/example/spot/domain/mapping/StudyLikedComment.java @@ -10,8 +10,10 @@ @Entity @Getter +@Builder @DynamicUpdate @DynamicInsert +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class StudyLikedComment extends BaseEntity { @@ -31,13 +33,4 @@ public class StudyLikedComment extends BaseEntity { @JoinColumn(name = "member_id", nullable = false) private Member member; -/* ----------------------------- 생성자 ------------------------------------- */ - - @Builder - public StudyLikedComment(StudyPostComment studyPostComment, Member member, Boolean isLiked) { - this.studyPostComment = studyPostComment; - this.member = member; - this.isLiked = isLiked; - } - } diff --git a/src/main/java/com/example/spot/domain/mapping/StudyLikedPost.java b/src/main/java/com/example/spot/domain/mapping/StudyLikedPost.java index 28644d82..7ff36c46 100644 --- a/src/main/java/com/example/spot/domain/mapping/StudyLikedPost.java +++ b/src/main/java/com/example/spot/domain/mapping/StudyLikedPost.java @@ -16,8 +16,10 @@ @Entity @Getter +@Builder @DynamicUpdate @DynamicInsert +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class StudyLikedPost extends BaseEntity { @@ -35,11 +37,4 @@ public class StudyLikedPost extends BaseEntity { @JoinColumn(name = "member_id", nullable = false) private Member member; -/* ----------------------------- 생성자 ------------------------------------- */ - - @Builder - public StudyLikedPost(Member member, StudyPost studyPost) { - this.member = member; - this.studyPost = studyPost; - } } diff --git a/src/main/java/com/example/spot/domain/study/Study.java b/src/main/java/com/example/spot/domain/study/Study.java index 59399c10..cf5f61aa 100644 --- a/src/main/java/com/example/spot/domain/study/Study.java +++ b/src/main/java/com/example/spot/domain/study/Study.java @@ -28,8 +28,8 @@ @Entity @Getter -@DynamicUpdate -@DynamicInsert +@Builder +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Study extends BaseEntity { @@ -62,7 +62,7 @@ public class Study extends BaseEntity { @Column(nullable = false) private Boolean isOnline; - @Column(nullable = false, columnDefinition = "BIGINT DEFAULT 0") + @Column(nullable = false, columnDefinition = "INTEGER DEFAULT 0") private Integer heartCount; @Column(nullable = false) @@ -85,72 +85,46 @@ public class Study extends BaseEntity { @Column(nullable = false) private Long maxPeople; + @Builder.Default @OneToMany(mappedBy = "study", cascade = CascadeType.ALL) private List schedules = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "study", cascade = CascadeType.ALL) private List posts = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "study", cascade = CascadeType.ALL) private List votes = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "study", cascade = CascadeType.ALL) private List studyThemes = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "study", cascade = CascadeType.ALL) private List memberStudies = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "study", cascade = CascadeType.ALL) private List regionStudies = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "study", cascade = CascadeType.ALL) private List preferredStudies = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "study", cascade = CascadeType.ALL) private List studyPosts = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "study", cascade = CascadeType.ALL) private List toDoLists = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "study", cascade = CascadeType.ALL) private List notifications = new ArrayList<>(); - -/* ----------------------------- 생성자 ------------------------------------- */ - - @Builder - public Study(Gender gender, Integer minAge, Integer maxAge, Integer fee, - String profileImage, boolean hasFee, - Boolean isOnline, String goal, String introduction, - String title, Long maxPeople) { - this.gender = gender; - this.minAge = minAge; - this.maxAge = maxAge; - this.fee = fee; - this.profileImage = profileImage; - this.studyState = StudyState.RECRUITING; - this.isOnline = isOnline; - this.heartCount = 0; - this.hasFee = hasFee; - this.goal = goal; - this.introduction = introduction; - this.title = title; - this.status = Status.ON; - this.hitNum = 0L; - this.maxPeople = maxPeople; - this.schedules = new ArrayList<>(); - this.posts = new ArrayList<>(); - this.votes = new ArrayList<>(); - this.studyThemes = new ArrayList<>(); - this.preferredStudies = new ArrayList<>(); - this.memberStudies = new ArrayList<>(); - this.regionStudies = new ArrayList<>(); - this.studyPosts = new ArrayList<>(); - this.toDoLists = new ArrayList<>(); - this.notifications = new ArrayList<>(); - - } - /* ----------------------------- 연관관계 메소드 ------------------------------------- */ public void addMemberStudy(MemberStudy memberStudy) { diff --git a/src/main/java/com/example/spot/domain/study/StudyPost.java b/src/main/java/com/example/spot/domain/study/StudyPost.java index f7bba4f7..b58d5397 100644 --- a/src/main/java/com/example/spot/domain/study/StudyPost.java +++ b/src/main/java/com/example/spot/domain/study/StudyPost.java @@ -12,13 +12,11 @@ import java.util.List; import lombok.*; -import org.hibernate.annotations.DynamicInsert; -import org.hibernate.annotations.DynamicUpdate; @Entity @Getter -@DynamicUpdate -@DynamicInsert +@Builder +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class StudyPost extends BaseEntity { @@ -50,47 +48,35 @@ public class StudyPost extends BaseEntity { @Column(nullable = false) private String title; - @Column(nullable = false) + @Column(nullable = false, columnDefinition = "text") private String content; - @Column(nullable = false) + @Column(columnDefinition = "INTEGER DEFAULT 0") private Integer likeNum; - @Column(nullable = false) + @Column(columnDefinition = "INTEGER DEFAULT 0") private Integer hitNum; @Setter - @Column(nullable = false) + @Column(columnDefinition = "INTEGER DEFAULT 0") private Integer commentNum; + @Builder.Default @OneToMany(mappedBy = "studyPost", cascade = CascadeType.ALL) - private List images; + private List images = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "studyPost", cascade = CascadeType.ALL) - private List comments; + private List comments = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "studyPost", cascade = CascadeType.ALL) - private List likedPosts; + private List likedPosts = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "studyPost", cascade = CascadeType.ALL) - private List studyPostReports; - -/* ----------------------------- 생성자 ------------------------------------- */ - - @Builder - public StudyPost(Boolean isAnnouncement, Theme theme, String title, String content) { - this.isAnnouncement = isAnnouncement; - this.theme = theme; - this.title = title; - this.content = content; - this.likeNum = 0; - this.hitNum = 0; - this.commentNum = 0; - this.images = new ArrayList<>(); - this.comments = new ArrayList<>(); - this.likedPosts = new ArrayList<>(); - this.studyPostReports = new ArrayList<>(); - } + private List studyPostReports = new ArrayList<>(); + /* ----------------------------- 연관관계 메소드 ------------------------------------- */ public void addImage(StudyPostImage image) { diff --git a/src/main/java/com/example/spot/domain/study/StudyPostComment.java b/src/main/java/com/example/spot/domain/study/StudyPostComment.java index 6083cd80..9e397391 100644 --- a/src/main/java/com/example/spot/domain/study/StudyPostComment.java +++ b/src/main/java/com/example/spot/domain/study/StudyPostComment.java @@ -14,8 +14,8 @@ @Entity @Getter -@DynamicUpdate -@DynamicInsert +@Builder +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class StudyPostComment extends BaseEntity { @@ -36,10 +36,10 @@ public class StudyPostComment extends BaseEntity { @Column(nullable = false) private String content; - @Column(nullable = false) + @Column(columnDefinition = "INTEGER DEFAULT 0") private Integer likeCount; - @Column(nullable = false) + @Column(columnDefinition = "INTEGER DEFAULT 0") private Integer dislikeCount; @Column(nullable = false, columnDefinition = "BIT DEFAULT 0") @@ -56,29 +56,13 @@ public class StudyPostComment extends BaseEntity { @JoinColumn(name = "parent_comment_id") private StudyPostComment parentComment; + @Builder.Default @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL) - private List childrenComment; + private List childrenComment = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "studyPostComment", cascade = CascadeType.ALL) - private List likedComments; - -/* ----------------------------- 생성자 ------------------------------------- */ - - @Builder - public StudyPostComment(StudyPost studyPost, Member member, String content, - Boolean isAnonymous, Integer anonymousNum, StudyPostComment parentComment) { - this.studyPost = studyPost; - this.member = member; - this.content = content; - this.likeCount = 0; - this.dislikeCount = 0; - this.isAnonymous = isAnonymous; - this.anonymousNum = anonymousNum; - this.isDeleted = Boolean.FALSE; - this.parentComment = parentComment; - this.childrenComment = new ArrayList<>(); - this.likedComments = new ArrayList<>(); - } + private List likedComments = new ArrayList<>(); /* ----------------------------- 연관관계 메소드 ------------------------------------- */ diff --git a/src/main/java/com/example/spot/repository/querydsl/StudyRepositoryCustom.java b/src/main/java/com/example/spot/repository/querydsl/StudyRepositoryCustom.java index c2a19271..f165eb86 100644 --- a/src/main/java/com/example/spot/repository/querydsl/StudyRepositoryCustom.java +++ b/src/main/java/com/example/spot/repository/querydsl/StudyRepositoryCustom.java @@ -21,6 +21,7 @@ public interface StudyRepositoryCustom { List findByStudyTheme(List studyThemes); List findByStudyThemeAndNotInIds(List studyThemes, List studyIds); + List findByRegionStudyAndNotInIds(List regionStudies, List studyIds); // 모집중 스터디 조회 List findRecruitingStudyByConditions(Map search, StudySortBy sortBy, Pageable pageable); diff --git a/src/main/java/com/example/spot/repository/querydsl/impl/StudyRepositoryCustomImpl.java b/src/main/java/com/example/spot/repository/querydsl/impl/StudyRepositoryCustomImpl.java index c119f808..92d4bec8 100644 --- a/src/main/java/com/example/spot/repository/querydsl/impl/StudyRepositoryCustomImpl.java +++ b/src/main/java/com/example/spot/repository/querydsl/impl/StudyRepositoryCustomImpl.java @@ -76,7 +76,18 @@ public List findByStudyThemeAndNotInIds(List studyThemes, return queryFactory.selectFrom(study) .where(study.studyThemes.any().in(studyThemes)) .where(study.id.notIn(studyIds)) - .orderBy(study.createdAt.desc()) + .orderBy(study.hitNum.desc()) + .offset(0) + .limit(3) + .fetch(); + } + + @Override + public List findByRegionStudyAndNotInIds(List regionStudies, List studyIds) { + return queryFactory.selectFrom(study) + .where(study.regionStudies.any().in(regionStudies)) + .where(study.id.notIn(studyIds)) + .orderBy(study.heartCount.desc()) .offset(0) .limit(3) .fetch(); diff --git a/src/main/java/com/example/spot/service/memberstudy/MemberStudyCommandServiceImpl.java b/src/main/java/com/example/spot/service/memberstudy/MemberStudyCommandServiceImpl.java index 8cc85612..2a30badb 100644 --- a/src/main/java/com/example/spot/service/memberstudy/MemberStudyCommandServiceImpl.java +++ b/src/main/java/com/example/spot/service/memberstudy/MemberStudyCommandServiceImpl.java @@ -87,7 +87,7 @@ public StudyWithdrawalResponseDTO.WithdrawalDTO withdrawFromStudy(Long studyId) .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); // 참여가 승인되지 않은 스터디는 탈퇴할 수 없음 - if (memberStudy.getStatus().equals(ApplicationStatus.APPLIED)) { + if (!memberStudy.getStatus().equals(ApplicationStatus.APPROVED)) { throw new StudyHandler(ErrorStatus._STUDY_NOT_APPROVED); } // 스터디장은 스터디를 탈퇴할 수 없음 @@ -107,10 +107,24 @@ public StudyWithdrawalResponseDTO.WithdrawalDTO withdrawFromStudy(Long studyId) */ public StudyTerminationResponseDTO.TerminationDTO terminateStudy(Long studyId) { + // 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)); + MemberStudy memberStudy = memberStudyRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, ApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 스터디장이 아니면 스터디를 종료할 수 없음 + if (memberStudy.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.setStatus(Status.OFF); studyRepository.save(study); diff --git a/src/main/java/com/example/spot/service/memberstudy/MemberStudyQueryServiceImpl.java b/src/main/java/com/example/spot/service/memberstudy/MemberStudyQueryServiceImpl.java index 3fb768dc..2bc6cf44 100644 --- a/src/main/java/com/example/spot/service/memberstudy/MemberStudyQueryServiceImpl.java +++ b/src/main/java/com/example/spot/service/memberstudy/MemberStudyQueryServiceImpl.java @@ -101,13 +101,14 @@ public StudyScheduleResponseDTO findStudySchedule(Long studyId, Pageable pageabl // 스터디 일정이 존재하지 않는 경우 if (schedules.isEmpty()) - throw new GeneralException(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND); + throw new GeneralException(ErrorStatus._STUDY_SCHEDULE_NOT_FOUND); // DTO로 변환하여 반환 List scheduleDTOS = schedules.stream().map(schedule -> StudyScheduleDTO.builder() .title(schedule.getTitle()) .location(schedule.getLocation()) - .staredAt(schedule.getStartedAt()) + .startedAt(schedule.getStartedAt()) + .finishedAt(schedule.getFinishedAt()) .build()).toList(); // 페이징 처리 diff --git a/src/main/java/com/example/spot/service/study/StudyCommandServiceImpl.java b/src/main/java/com/example/spot/service/study/StudyCommandServiceImpl.java index 4f71aa0e..7a6d52a3 100644 --- a/src/main/java/com/example/spot/service/study/StudyCommandServiceImpl.java +++ b/src/main/java/com/example/spot/service/study/StudyCommandServiceImpl.java @@ -7,6 +7,7 @@ import com.example.spot.domain.Region; import com.example.spot.domain.Theme; import com.example.spot.domain.enums.ApplicationStatus; +import com.example.spot.domain.enums.Status; import com.example.spot.domain.enums.StudyLikeStatus; import com.example.spot.domain.enums.StudyState; import com.example.spot.domain.mapping.MemberStudy; @@ -121,13 +122,17 @@ public StudyRegisterResponseDTO.RegisterDTO registerStudy(StudyRegisterRequestDT .gender(studyRegisterRequestDTO.getGender()) .minAge(studyRegisterRequestDTO.getMinAge()) .maxAge(studyRegisterRequestDTO.getMaxAge()) + .hasFee(studyRegisterRequestDTO.isHasFee()) .fee(studyRegisterRequestDTO.getFee()) .profileImage(studyRegisterRequestDTO.getProfileImage()) + .studyState(StudyState.RECRUITING) .isOnline(studyRegisterRequestDTO.getIsOnline()) - .hasFee(studyRegisterRequestDTO.isHasFee()) + .heartCount(0) .goal(studyRegisterRequestDTO.getGoal()) .introduction(studyRegisterRequestDTO.getIntroduction()) .title(studyRegisterRequestDTO.getTitle()) + .status(Status.ON) + .hitNum(0L) .maxPeople(studyRegisterRequestDTO.getMaxPeople()) .build(); diff --git a/src/main/java/com/example/spot/service/study/StudyQueryServiceImpl.java b/src/main/java/com/example/spot/service/study/StudyQueryServiceImpl.java index 0748c48d..82df5415 100644 --- a/src/main/java/com/example/spot/service/study/StudyQueryServiceImpl.java +++ b/src/main/java/com/example/spot/service/study/StudyQueryServiceImpl.java @@ -47,12 +47,15 @@ import com.example.spot.web.dto.study.response.StudyScheduleResponseDTO.StudyScheduleDTO; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.Set; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -254,33 +257,66 @@ public StudyPreviewDTO findRecommendStudies(Long memberId) { // 회원 관심사 조회 List memberThemes = memberThemeRepository.findAllByMemberId(memberId); + List preferredRegions = preferredRegionRepository.findAllByMemberId(memberId); // 회원 관심사가 없을 경우 if (memberThemes.isEmpty()) throw new MemberHandler(ErrorStatus._STUDY_THEME_IS_INVALID); - // MemberId로 회원 관심사 전체 조회 + if (preferredRegions.isEmpty()) + throw new MemberHandler(ErrorStatus._STUDY_REGION_IS_INVALID); + + // MemberId로 회원 관심사 및 관심 지역 전체 조회 List themes = memberThemes.stream() .map(MemberTheme::getTheme) .toList(); + List regions = preferredRegions.stream() + .map(PreferredRegion::getRegion) + .toList(); + // 회원 관심사로 스터디 테마 조회 List studyThemes = themes.stream() .flatMap(theme -> studyThemeRepository.findAllByTheme(theme).stream()) .toList(); + // 회원 관심 지역으로 스터디 지역 조회 + List regionStudies = regions.stream() + .flatMap(region -> regionStudyRepository.findAllByRegion(region).stream()) + .toList(); + // 해당 관심사에 해당하는 스터디가 존재하지 않을 경우 if (studyThemes.isEmpty()) throw new StudyHandler(ErrorStatus._STUDY_THEME_NOT_EXIST); + // 해당 관심 지역에 해당하는 스터디가 존재하지 않을 경우 + if (regionStudies.isEmpty()) + throw new StudyHandler(ErrorStatus._STUDY_REGION_NOT_EXIST); + // 회원 관심사로 추천 스터디 조회 - List studies = studyRepository.findByStudyThemeAndNotInIds(studyThemes, memberOngoingStudyIds); + List preferThemeStudies = studyRepository.findByStudyThemeAndNotInIds(studyThemes, memberOngoingStudyIds); + + // 회원 관심 지역으로 추천 스터디 조회 + List preferRegionStudies = studyRepository.findByRegionStudyAndNotInIds(regionStudies, memberOngoingStudyIds); // 추천 스터디가 없을 경우 - if (studies.isEmpty()) + if (preferRegionStudies.isEmpty() || preferThemeStudies.isEmpty()) throw new StudyHandler(ErrorStatus._STUDY_IS_NOT_MATCH); - return getDTOs(studies, Pageable.unpaged(), studies.size(), memberId); + // 두 리스트를 합쳐서 중복 제거 + Set combinedStudies = new HashSet<>(preferThemeStudies); + combinedStudies.addAll(preferRegionStudies); + + // 리스트 변환 + List studyList = new ArrayList<>(combinedStudies); + + // 랜덤으로 최대 3개 선택 + Collections.shuffle(studyList); + List selectedStudies = studyList.stream() + .limit(3) + .collect(Collectors.toList()); + + return getDTOs(selectedStudies, Pageable.unpaged(), selectedStudies.size(), memberId); } diff --git a/src/main/java/com/example/spot/service/studypost/StudyPostCommandServiceImpl.java b/src/main/java/com/example/spot/service/studypost/StudyPostCommandServiceImpl.java index 3e4fbb9d..503682e8 100644 --- a/src/main/java/com/example/spot/service/studypost/StudyPostCommandServiceImpl.java +++ b/src/main/java/com/example/spot/service/studypost/StudyPostCommandServiceImpl.java @@ -93,6 +93,9 @@ public StudyPostResDTO.PostPreviewDTO createPost(Long studyId, StudyPostRequestD .theme(postRequestDTO.getTheme()) .title(postRequestDTO.getTitle()) .content(postRequestDTO.getContent()) + .likeNum(0) + .hitNum(0) + .commentNum(0) .build(); // 공지면 announcedAt 설정 @@ -173,19 +176,28 @@ public StudyPostResDTO.PostPreviewDTO deletePost(Long studyId, Long postId) { studyPostRepository.findByIdAndStudyId(postId, studyId) .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_POST_NOT_FOUND)); - // 로그인 회원이 게시글 작성자인지 확인 - studyPostRepository.findByIdAndMemberId(postId, memberId) - .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_POST_DELETION_INVALID)); + // 로그인 회원이 게시글 작성자거나 owner인지 확인 + Long ownerId = studyPost.getStudy().getMemberStudies().stream() + .filter(MemberStudy::getIsOwned) + .map(memberStudy -> memberStudy.getMember().getId()) + .findFirst() + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_OWNER_NOT_FOUND)); - //=== Feature ===// - studyPostImageRepository.deleteAllByStudyPostId(postId); - studyPostCommentRepository.deleteAllByStudyPostId(postId); - studyLikedPostRepository.deleteAllByStudyPostId(postId); - studyPostReportRepository.deleteAllByStudyPostId(postId); + if (studyPost.getMember().getId().equals(memberId) || + memberId.equals(ownerId)) { + + studyPostImageRepository.deleteAllByStudyPostId(postId); + studyPostCommentRepository.deleteAllByStudyPostId(postId); + studyLikedPostRepository.deleteAllByStudyPostId(postId); + studyPostReportRepository.deleteAllByStudyPostId(postId); + + member.deleteStudyPost(studyPost); + study.deleteStudyPost(studyPost); + studyPostRepository.delete(studyPost); - member.deleteStudyPost(studyPost); - study.deleteStudyPost(studyPost); - studyPostRepository.delete(studyPost); + } else { + throw new StudyHandler(ErrorStatus._STUDY_POST_DELETION_INVALID); + } return StudyPostResDTO.PostPreviewDTO.toDTO(studyPost); } @@ -320,8 +332,11 @@ public StudyPostCommentResponseDTO.CommentDTO createComment(Long studyId, Long p .studyPost(studyPost) .member(member) .content(commentRequestDTO.getContent()) + .likeCount(0) + .dislikeCount(0) .isAnonymous(commentRequestDTO.getIsAnonymous()) .parentComment(null) + .isDeleted(false) .anonymousNum(anonymousNum) .build(); @@ -376,9 +391,12 @@ public StudyPostCommentResponseDTO.CommentDTO createReply(Long studyId, Long pos .studyPost(studyPost) .member(member) .content(commentRequestDTO.getContent()) + .likeCount(0) + .dislikeCount(0) .isAnonymous(commentRequestDTO.getIsAnonymous()) .anonymousNum(anonymousNum) .parentComment(parentComment) + .isDeleted(false) .build(); studyPostCommentRepository.save(studyPostComment); diff --git a/src/main/java/com/example/spot/service/studypost/StudyPostQueryServiceImpl.java b/src/main/java/com/example/spot/service/studypost/StudyPostQueryServiceImpl.java index 7b30e46a..0ee04353 100644 --- a/src/main/java/com/example/spot/service/studypost/StudyPostQueryServiceImpl.java +++ b/src/main/java/com/example/spot/service/studypost/StudyPostQueryServiceImpl.java @@ -62,8 +62,10 @@ public StudyPostResDTO.PostListDTO getAllPosts(PageRequest pageRequest, Long stu Study study = studyRepository.findById(studyId) .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); - //=== Feature ===// + memberStudyRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, ApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + //=== Feature ===// List studyPosts; if (themeQuery == null) { // query가 없는 경우 diff --git a/src/main/java/com/example/spot/web/dto/memberstudy/request/StudyPostCommentRequestDTO.java b/src/main/java/com/example/spot/web/dto/memberstudy/request/StudyPostCommentRequestDTO.java index d8505e3e..95ce9c9c 100644 --- a/src/main/java/com/example/spot/web/dto/memberstudy/request/StudyPostCommentRequestDTO.java +++ b/src/main/java/com/example/spot/web/dto/memberstudy/request/StudyPostCommentRequestDTO.java @@ -2,6 +2,7 @@ import com.example.spot.validation.annotation.TextLength; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -9,6 +10,7 @@ public class StudyPostCommentRequestDTO { @Getter + @Builder @NoArgsConstructor @AllArgsConstructor public static class CommentDTO { diff --git a/src/main/java/com/example/spot/web/dto/memberstudy/request/StudyPostRequestDTO.java b/src/main/java/com/example/spot/web/dto/memberstudy/request/StudyPostRequestDTO.java index aca1527a..a3716640 100644 --- a/src/main/java/com/example/spot/web/dto/memberstudy/request/StudyPostRequestDTO.java +++ b/src/main/java/com/example/spot/web/dto/memberstudy/request/StudyPostRequestDTO.java @@ -15,6 +15,7 @@ public class StudyPostRequestDTO { @Getter @Setter + @Builder @Schema(name = "StudyPostDTO") @AllArgsConstructor public static class PostDTO { @@ -28,7 +29,7 @@ public static class PostDTO { private Theme theme; @NotNull - @TextLength(min = 1, max = 255) + @TextLength(min = 1, max = 50) @Schema(description = "제목", example = "title") private String title; diff --git a/src/main/java/com/example/spot/web/dto/memberstudy/response/StudyPostCommentResponseDTO.java b/src/main/java/com/example/spot/web/dto/memberstudy/response/StudyPostCommentResponseDTO.java index 908811f6..3255e105 100644 --- a/src/main/java/com/example/spot/web/dto/memberstudy/response/StudyPostCommentResponseDTO.java +++ b/src/main/java/com/example/spot/web/dto/memberstudy/response/StudyPostCommentResponseDTO.java @@ -39,7 +39,7 @@ public static CommentDTO toDTO(StudyPostComment comment, String name, String def @Getter @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @Builder(access = AccessLevel.PRIVATE) - private static class MemberInfoDTO { + public static class MemberInfoDTO { private final Long memberId; private final String name; diff --git a/src/main/java/com/example/spot/web/dto/study/response/StudyScheduleResponseDTO.java b/src/main/java/com/example/spot/web/dto/study/response/StudyScheduleResponseDTO.java index 3fa78d8b..c25e5484 100644 --- a/src/main/java/com/example/spot/web/dto/study/response/StudyScheduleResponseDTO.java +++ b/src/main/java/com/example/spot/web/dto/study/response/StudyScheduleResponseDTO.java @@ -36,7 +36,8 @@ public StudyScheduleResponseDTO(Page page, List schedules, @NoArgsConstructor @AllArgsConstructor public static class StudyScheduleDTO{ - private LocalDateTime staredAt; + private LocalDateTime startedAt; + private LocalDateTime finishedAt; private String title; private String location; } diff --git a/src/test/java/com/example/spot/service/memberstudy/MemberStudyCommandServiceTest.java b/src/test/java/com/example/spot/service/memberstudy/MemberStudyCommandServiceTest.java index 8e79e214..1e7bdc42 100644 --- a/src/test/java/com/example/spot/service/memberstudy/MemberStudyCommandServiceTest.java +++ b/src/test/java/com/example/spot/service/memberstudy/MemberStudyCommandServiceTest.java @@ -1,5 +1,6 @@ package com.example.spot.service.memberstudy; +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; @@ -8,6 +9,8 @@ import com.example.spot.api.exception.handler.StudyHandler; import com.example.spot.domain.Member; +import com.example.spot.domain.enums.ApplicationStatus; +import com.example.spot.domain.enums.Status; import com.example.spot.domain.mapping.MemberStudy; import com.example.spot.domain.study.Study; import com.example.spot.domain.study.ToDoList; @@ -21,6 +24,9 @@ import java.time.LocalDate; import java.util.Collections; import java.util.Optional; + +import com.example.spot.web.dto.memberstudy.response.StudyTerminationResponseDTO; +import com.example.spot.web.dto.memberstudy.response.StudyWithdrawalResponseDTO; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -89,6 +95,199 @@ void init() { SecurityContextHolder.setContext(securityContext); } + /* ---------------------------- 진행중인 스터디 관련 메서드 ---------------------------- */ + + @Test + @DisplayName("스터디 탈퇴 - (성공)") + void withdrawFromStudy_Success() { + + // given + member = Member.builder() + .id(1L) + .name("회원1") + .build(); + study = Study.builder() + .id(1L) + .title("스터디") + .build(); + memberStudy = MemberStudy.builder() + .member(member) + .study(study) + .isOwned(false) + .status(ApplicationStatus.APPROVED) + .build(); + + when(memberRepository.findById(1L)).thenReturn(Optional.of(member)); + when(studyRepository.findById(1L)).thenReturn(Optional.of(study)); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(1L, 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(memberStudy)); + + // 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(); + memberStudy = MemberStudy.builder() + .member(member) + .study(study) + .isOwned(false) + .status(ApplicationStatus.APPLIED) + .build(); + + when(memberRepository.findById(1L)).thenReturn(Optional.of(member)); + when(studyRepository.findById(1L)).thenReturn(Optional.of(study)); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(1L, 1L, ApplicationStatus.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(); + memberStudy = MemberStudy.builder() + .member(member) + .study(study) + .isOwned(true) + .status(ApplicationStatus.APPROVED) + .build(); + + when(memberRepository.findById(1L)).thenReturn(Optional.of(member)); + when(studyRepository.findById(1L)).thenReturn(Optional.of(study)); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(1L, 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(memberStudy)); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyCommandService.withdrawFromStudy(1L)); + } + + @Test + @DisplayName("스터디 종료 - (성공)") + void terminateStudy_Success() { + + // given + member = Member.builder() + .id(1L) + .name("회원1") + .build(); + study = Study.builder() + .id(1L) + .title("스터디") + .status(Status.ON) + .build(); + memberStudy = MemberStudy.builder() + .member(member) + .study(study) + .isOwned(true) + .status(ApplicationStatus.APPROVED) + .build(); + + when(memberRepository.findById(1L)).thenReturn(Optional.of(member)); + when(studyRepository.findById(1L)).thenReturn(Optional.of(study)); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(1L, 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(memberStudy)); + + // when + StudyTerminationResponseDTO.TerminationDTO result = memberStudyCommandService.terminateStudy(1L); + + // then + assertNotNull(result); + assertThat(result.getStudyId()).isEqualTo(1L); + assertThat(result.getStudyName()).isEqualTo("스터디"); + assertThat(result.getStatus()).isEqualTo(Status.OFF); + } + + @Test + @DisplayName("스터디 종료 - 스터디장이 아닌 경우 (실패)") + void terminateStudy_NotStudyOwner_Fail() { + + // given + member = Member.builder() + .id(1L) + .name("회원1") + .build(); + study = Study.builder() + .id(1L) + .title("스터디") + .status(Status.ON) + .build(); + memberStudy = MemberStudy.builder() + .member(member) + .study(study) + .isOwned(false) + .status(ApplicationStatus.APPROVED) + .build(); + + when(memberRepository.findById(1L)).thenReturn(Optional.of(member)); + when(studyRepository.findById(1L)).thenReturn(Optional.of(study)); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(1L, 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(memberStudy)); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyCommandService.terminateStudy(1L)); + } + + @Test + @DisplayName("스터디 종료 - 진행중인 스터디가 아닌 경우 (실패)") + void terminateStudy_AlreadyTerminated_Fail() { + + // given + member = Member.builder() + .id(1L) + .name("회원1") + .build(); + study = Study.builder() + .id(1L) + .title("스터디") + .status(Status.OFF) + .build(); + memberStudy = MemberStudy.builder() + .member(member) + .study(study) + .isOwned(false) + .status(ApplicationStatus.APPROVED) + .build(); + + when(memberRepository.findById(1L)).thenReturn(Optional.of(member)); + when(studyRepository.findById(1L)).thenReturn(Optional.of(study)); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(1L, 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(memberStudy)); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyCommandService.terminateStudy(1L)); + } + + /* ---------------------------- To-Do 생성 관련 메서드 ---------------------------- */ @Test diff --git a/src/test/java/com/example/spot/service/memberstudy/MemberStudyQueryServiceTest.java b/src/test/java/com/example/spot/service/memberstudy/MemberStudyQueryServiceTest.java index 01c7db33..ddb714ab 100644 --- a/src/test/java/com/example/spot/service/memberstudy/MemberStudyQueryServiceTest.java +++ b/src/test/java/com/example/spot/service/memberstudy/MemberStudyQueryServiceTest.java @@ -77,6 +77,7 @@ public class MemberStudyQueryServiceTest { private static MemberStudy studyMember; private static MemberStudy apply; private static ToDoList toDoList; + @BeforeEach void setup(){ member = Member.builder() @@ -122,7 +123,13 @@ void setup(){ long studyId = 1L; String title = "공지"; String content = "공지입니다."; - StudyPost studyPost = new StudyPost(true, Theme.WELCOME, title, content); + StudyPost studyPost = StudyPost.builder() + .title(title) + .content(content) + .theme(Theme.WELCOME) + .isAnnouncement(true) + .build(); + MemberStudy memberStudy = MemberStudy.builder() .introduction(title).study(study).member(member).isOwned(true).status(ApplicationStatus.APPROVED).build(); 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 0c018c3f..d294a45c 100644 --- a/src/test/java/com/example/spot/service/study/StudyCommandServiceTest.java +++ b/src/test/java/com/example/spot/service/study/StudyCommandServiceTest.java @@ -299,6 +299,7 @@ private static void initStudy() { .profileImage("a.jpg") .hasFee(true) .isOnline(true) + .studyState(StudyState.RECRUITING) .goal("SQLD") .introduction("SQLD 자격증 스터디") .title("SQLD Master") 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 60929b8b..fb6154b9 100644 --- a/src/test/java/com/example/spot/service/study/StudyQueryServiceTest.java +++ b/src/test/java/com/example/spot/service/study/StudyQueryServiceTest.java @@ -16,12 +16,7 @@ import com.example.spot.domain.Member; import com.example.spot.domain.Region; import com.example.spot.domain.Theme; -import com.example.spot.domain.enums.ApplicationStatus; -import com.example.spot.domain.enums.Gender; -import com.example.spot.domain.enums.StudyLikeStatus; -import com.example.spot.domain.enums.StudySortBy; -import com.example.spot.domain.enums.StudyState; -import com.example.spot.domain.enums.ThemeType; +import com.example.spot.domain.enums.*; import com.example.spot.domain.mapping.MemberStudy; import com.example.spot.domain.mapping.MemberTheme; import com.example.spot.domain.mapping.PreferredRegion; @@ -394,12 +389,17 @@ void findRecommendStudies() { // Mock the memberThemeRepository to return a list of MemberTheme when(memberThemeRepository.findAllByMemberId(member.getId())).thenReturn(List.of(memberTheme1, memberTheme2)); + when(preferredRegionRepository.findAllByMemberId(member.getId())).thenReturn(List.of(preferredRegion1, preferredRegion2)); when(studyThemeRepository.findAllByTheme(theme1)).thenReturn(List.of(studyTheme1)); when(studyThemeRepository.findAllByTheme(theme2)).thenReturn(List.of(studyTheme2)); + when(regionStudyRepository.findAllByRegion(region1)).thenReturn(List.of(regionStudy1)); + when(regionStudyRepository.findAllByRegion(region2)).thenReturn(List.of(regionStudy2)); + // Mocking the studyRepository to return studies based on the study themes when(studyRepository.findByStudyThemeAndNotInIds(anyList(), anyList())).thenReturn(List.of(study1, study2)); + when(studyRepository.findByRegionStudyAndNotInIds(anyList(), anyList())).thenReturn(List.of(study1, study2)); when(memberRepository.existsById(member.getId())).thenReturn(true); @@ -422,13 +422,17 @@ void findRecommendStudiesOnFail() { // given when(memberThemeRepository.findAllByMemberId(member.getId())).thenReturn(List.of(memberTheme1, memberTheme2)); - + when(preferredRegionRepository.findAllByMemberId(member.getId())).thenReturn(List.of(preferredRegion1, preferredRegion2)); when(studyThemeRepository.findAllByTheme(theme1)).thenReturn(List.of(studyTheme1)); when(studyThemeRepository.findAllByTheme(theme2)).thenReturn(List.of(studyTheme2)); + when(regionStudyRepository.findAllByRegion(region1)).thenReturn(List.of(regionStudy1)); + when(regionStudyRepository.findAllByRegion(region2)).thenReturn(List.of(regionStudy2)); + // Mocking the studyRepository to return studies based on the study themes when(studyRepository.findByStudyThemeAndNotInIds(anyList(), anyList())).thenReturn(List.of()); + when(studyRepository.findByRegionStudyAndNotInIds(anyList(), anyList())).thenReturn(List.of()); when(memberRepository.existsById(member.getId())).thenReturn(true); @@ -443,18 +447,46 @@ void findRecommendStudiesOnFail() { } @Test - @DisplayName("추천 스터디 조회 - 회원의 관심 테마에_해당하는_스터디가 없는 경우") + @DisplayName("추천 스터디 조회 - 회원의 관심 테마에 해당하는 스터디가 없는 경우") void 추천_스터디_조회_시_회원의_관심_테마에_해당하는_스터디가_없는_경우() { // given when(memberThemeRepository.findAllByMemberId(member.getId())).thenReturn(List.of(memberTheme1, memberTheme2)); + when(preferredRegionRepository.findAllByMemberId(member.getId())).thenReturn(List.of(preferredRegion1, preferredRegion2)); when(studyThemeRepository.findAllByTheme(theme1)).thenReturn(List.of()); when(studyThemeRepository.findAllByTheme(theme2)).thenReturn(List.of()); + when(regionStudyRepository.findAllByRegion(region1)).thenReturn(List.of(regionStudy1)); + when(regionStudyRepository.findAllByRegion(region2)).thenReturn(List.of(regionStudy2)); + // when & then assertThrows(StudyHandler.class, () -> { studyQueryService.findRecommendStudies(member.getId()); }); + + verify(memberThemeRepository).findAllByMemberId(member.getId()); + verify(studyThemeRepository, times(1)).findAllByTheme(theme1); + verify(studyThemeRepository, times(1)).findAllByTheme(theme2); + } + + @Test + @DisplayName("추천 스터디 조회 - 회원의 관심 지역에 해당하는 스터디가 없는 경우") + void 추천_스터디_조회_시_회원의_관심_지역에_해당하는_스터디가_없는_경우() { + // given + when(memberThemeRepository.findAllByMemberId(member.getId())).thenReturn(List.of(memberTheme1, memberTheme2)); + when(preferredRegionRepository.findAllByMemberId(member.getId())).thenReturn(List.of(preferredRegion1, preferredRegion2)); + + when(studyThemeRepository.findAllByTheme(theme1)).thenReturn(List.of(studyTheme1)); + when(studyThemeRepository.findAllByTheme(theme2)).thenReturn(List.of(studyTheme2)); + + when(regionStudyRepository.findAllByRegion(region1)).thenReturn(List.of()); + when(regionStudyRepository.findAllByRegion(region2)).thenReturn(List.of()); + + // when & then + assertThrows(StudyHandler.class, () -> { + studyQueryService.findRecommendStudies(member.getId()); + }); + verify(memberThemeRepository).findAllByMemberId(member.getId()); verify(studyThemeRepository, times(1)).findAllByTheme(theme1); verify(studyThemeRepository, times(1)).findAllByTheme(theme2); @@ -468,9 +500,9 @@ void findRecommendStudiesOnInvalidUser() { Member member = getMember(); // when & then - when(memberRepository.findById(member.getId())).thenReturn(Optional.empty()); + when(memberRepository.existsById(member.getId())).thenReturn(false); - assertThrows(StudyHandler.class, () -> { + assertThrows(MemberHandler.class, () -> { studyQueryService.findRecommendStudies(member.getId()); }); } @@ -492,6 +524,23 @@ void findRecommendStudiesOnNoInterest() { }); } + @Test + @DisplayName("추천 스터디 조회 - 사용자의 관심 지역이 없는 경우") + void findRecommendStudiesOnNoRegion() { + // given + Member member = getMember(); + Long memberId = member.getId(); + + // Mock the preferredRegionRepository to return an empty list + when(preferredRegionRepository.findAllByMemberId(memberId)).thenReturn(List.of()); + + // when & then + assertThrows(MemberHandler.class, () -> { + studyQueryService.findRecommendStudies(memberId); + }); + + } + /* -------------------------------------------------------- 관심 Best 스터디 조회 ------------------------------------------------------------------------*/ @Test @DisplayName("관심 Best 스터디 조회 - 성공") @@ -1807,6 +1856,7 @@ private static Member getMember() { } private static void initStudy() { study1 = Study.builder() + .id(1L) .gender(Gender.MALE) .minAge(18) .maxAge(35) @@ -1814,6 +1864,10 @@ private static void initStudy() { .profileImage("profile1.jpg") .hasFee(true) .isOnline(true) + .studyState(StudyState.RECRUITING) + .heartCount(0) + .status(Status.ON) + .hitNum(0L) .goal("Learn English") .introduction("This is an English study group") .title("English Study Group") @@ -1828,6 +1882,10 @@ private static void initStudy() { .profileImage("profile2.jpg") .hasFee(true) .isOnline(false) + .studyState(StudyState.RECRUITING) + .heartCount(0) + .status(Status.ON) + .hitNum(0L) .goal("Win a competition") .introduction("This is a competition study group") .title("Competition Study Group") @@ -1841,6 +1899,10 @@ private static void initStudy() { .profileImage("profile1.jpg") .hasFee(true) .isOnline(true) + .studyState(StudyState.RECRUITING) + .heartCount(0) + .status(Status.ON) + .hitNum(0L) .goal("Learn Korean") .introduction("This is an Korean study group") .title("Korean Study Group") diff --git a/src/test/java/com/example/spot/service/studypost/StudyPostCommandServiceTest.java b/src/test/java/com/example/spot/service/studypost/StudyPostCommandServiceTest.java new file mode 100644 index 00000000..5577640c --- /dev/null +++ b/src/test/java/com/example/spot/service/studypost/StudyPostCommandServiceTest.java @@ -0,0 +1,1229 @@ +package com.example.spot.service.studypost; + +import com.example.spot.api.exception.handler.StudyHandler; +import com.example.spot.domain.Member; +import com.example.spot.domain.Notification; +import com.example.spot.domain.enums.ApplicationStatus; +import com.example.spot.domain.enums.Gender; +import com.example.spot.domain.enums.Theme; +import com.example.spot.domain.mapping.MemberStudy; +import com.example.spot.domain.mapping.StudyLikedComment; +import com.example.spot.domain.mapping.StudyLikedPost; +import com.example.spot.domain.study.Study; +import com.example.spot.domain.study.StudyPost; +import com.example.spot.domain.study.StudyPostComment; +import com.example.spot.repository.*; +import com.example.spot.web.dto.memberstudy.request.StudyPostCommentRequestDTO; +import com.example.spot.web.dto.memberstudy.request.StudyPostRequestDTO; +import com.example.spot.web.dto.memberstudy.response.StudyPostCommentResponseDTO; +import com.example.spot.web.dto.memberstudy.response.StudyPostResDTO; +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.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; + +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.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class StudyPostCommandServiceTest { + + @Mock + private MemberRepository memberRepository; + + @Mock + private StudyRepository studyRepository; + @Mock + private MemberStudyRepository memberStudyRepository; + + @Mock + private StudyPostRepository studyPostRepository; + @Mock + private StudyLikedPostRepository studyLikedPostRepository; + @Mock + private StudyPostImageRepository studyPostImageRepository; + @Mock + private StudyPostReportRepository studyPostReportRepository; + + @Mock + private StudyPostCommentRepository studyPostCommentRepository; + @Mock + private StudyLikedCommentRepository studyLikedCommentRepository; + + @Mock + private NotificationRepository notificationRepository; + + @InjectMocks + private StudyPostCommandServiceImpl studyPostCommandService; + + private static Study study; + private static Member member1; + private static Member member2; + private static Member owner; + private static MemberStudy member1Study; + private static MemberStudy ownerStudy; + + private static StudyPost studyPost1; + private static StudyPost studyPost2; + private static StudyPost studyPost3; + private static StudyLikedPost studyLikedPost; + private static StudyPostComment studyPost1Comment1; + private static StudyPostComment studyPost1Comment2; + private static StudyLikedComment studyLikedComment; + private static StudyLikedComment studyDislikedComment; + + @BeforeEach + void setUp() { + initMember(); + initStudy(); + initMemberStudy(); + initStudyPost(); + initStudyLikedPost(); + initStudyPostComment(); + initStudyLikedComment(); + + // Member + when(memberRepository.findById(1L)).thenReturn(Optional.of(member1)); + when(memberRepository.findById(2L)).thenReturn(Optional.of(member2)); + when(memberRepository.findById(3L)).thenReturn(Optional.of(owner)); + + // Study + when(studyRepository.findById(1L)).thenReturn(Optional.of(study)); + + // MemberStudy + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(1L, 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(member1Study)); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(2L, 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.empty()); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(3L, 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(ownerStudy)); + + // StudyPost + when(studyPostRepository.findById(1L)).thenReturn(Optional.of(studyPost1)); + when(studyPostRepository.findById(2L)).thenReturn(Optional.of(studyPost2)); + when(studyPostRepository.findById(3L)).thenReturn(Optional.of(studyPost3)); + + when(studyLikedPostRepository.findByMemberIdAndStudyPostId(3L, 1L)) + .thenReturn(Optional.of(studyLikedPost)); + when(studyLikedPostRepository.existsByMemberIdAndStudyPostId(3L, 1L)) + .thenReturn(true); + + // Comment + when(studyPostCommentRepository.findAllByStudyPostId(1L)) + .thenReturn(List.of(studyPost1Comment1, studyPost1Comment2)); + when(studyPostCommentRepository.findById(1L)).thenReturn(Optional.of(studyPost1Comment1)); + when(studyPostCommentRepository.findById(2L)).thenReturn(Optional.of(studyPost1Comment2)); + + } + +/*-------------------------------------------------------- 게시글 작성 ------------------------------------------------------------------------*/ + + @Test + @DisplayName("스터디 게시글 작성 - 공지 게시글 (성공)") + void createPost_Announcement_Success() { + + // given + Long memberId = 3L; + Long studyId = 1L; + + StudyPostRequestDTO.PostDTO postPreviewDTO = StudyPostRequestDTO.PostDTO.builder() + .isAnnouncement(true) + .theme(Theme.INFO_SHARING) + .title("공지") + .content("내용") + .build(); + + getAuthentication(memberId); + + when(memberStudyRepository.findByMemberIdAndStudyIdAndIsOwned(memberId, studyId, true)) + .thenReturn(Optional.of(ownerStudy)); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost2); + when(notificationRepository.save(any(Notification.class))).thenReturn(null); + when(memberStudyRepository.findAllByStudyIdAndStatus(studyId, ApplicationStatus.APPROVED)) + .thenReturn(List.of(member1Study, ownerStudy)); + + // when + StudyPostResDTO.PostPreviewDTO result = studyPostCommandService.createPost(studyId, postPreviewDTO); + + // then + assertNotNull(result); + assertThat(result.getTitle()).isEqualTo("공지"); + verify(studyPostRepository, times(1)).save(any(StudyPost.class)); + } + + @Test + @DisplayName("스터디 게시글 작성 - 일반 게시글 (성공)") + void createPost_Common_Success() { + + // given + Long memberId = 1L; + Long studyId = 1L; + + StudyPostRequestDTO.PostDTO postPreviewDTO = StudyPostRequestDTO.PostDTO.builder() + .isAnnouncement(false) + .theme(Theme.FREE_TALK) + .title("잡담") + .content("내용") + .build(); + + getAuthentication(memberId); + + when(memberStudyRepository.findByMemberIdAndStudyIdAndIsOwned(memberId, studyId, true)) + .thenReturn(Optional.of(member1Study)); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost1); + when(notificationRepository.save(any(Notification.class))).thenReturn(null); + when(memberStudyRepository.findAllByStudyIdAndStatus(studyId, ApplicationStatus.APPROVED)) + .thenReturn(List.of(member1Study, ownerStudy)); + + // when + StudyPostResDTO.PostPreviewDTO result = studyPostCommandService.createPost(studyId, postPreviewDTO); + + // then + assertNotNull(result); + assertThat(result.getTitle()).isEqualTo("잡담"); + verify(studyPostRepository, times(1)).save(any(StudyPost.class)); + } + + @Test + @DisplayName("스터디 게시글 작성 - 스터디 회원이 아닌 경우 (실패)") + void createPost_NotStudyMember_Fail() { + + // given + Long memberId = 2L; + Long studyId = 1L; + + StudyPostRequestDTO.PostDTO postPreviewDTO = StudyPostRequestDTO.PostDTO.builder() + .isAnnouncement(true) + .theme(Theme.INFO_SHARING) + .title("공지") + .content("내용") + .build(); + + getAuthentication(memberId); + + when(memberStudyRepository.findByMemberIdAndStudyIdAndIsOwned(memberId, studyId, true)) + .thenReturn(Optional.of(ownerStudy)); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost2); + when(notificationRepository.save(any(Notification.class))).thenReturn(null); + when(memberStudyRepository.findAllByStudyIdAndStatus(studyId, ApplicationStatus.APPROVED)) + .thenReturn(List.of(member1Study, ownerStudy)); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostCommandService.createPost(studyId, postPreviewDTO)); + } + + @Test + @DisplayName("스터디 게시글 작성 - 스터디장이 아닌 회원이 공지 게시글을 작성하는 경우 (실패)") + void createPost_MemberAnnounced_Fail() { + + // given + Long memberId = 2L; + Long studyId = 1L; + + StudyPostRequestDTO.PostDTO postPreviewDTO = StudyPostRequestDTO.PostDTO.builder() + .isAnnouncement(true) + .theme(Theme.INFO_SHARING) + .title("공지") + .content("내용") + .build(); + + getAuthentication(memberId); + + when(memberStudyRepository.findByMemberIdAndStudyIdAndIsOwned(memberId, studyId, true)) + .thenReturn(Optional.of(member1Study)); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost2); + when(notificationRepository.save(any(Notification.class))).thenReturn(null); + when(memberStudyRepository.findAllByStudyIdAndStatus(studyId, ApplicationStatus.APPROVED)) + .thenReturn(List.of(member1Study, ownerStudy)); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostCommandService.createPost(studyId, postPreviewDTO)); + } + + @Test + @DisplayName("스터디 게시글 작성 - 제목이 50자를 초과하는 경우 (실패)") + void createPost_TitleOverflow_Fail() { + + // given + Long memberId = 1L; + Long studyId = 1L; + + StudyPostRequestDTO.PostDTO postPreviewDTO = StudyPostRequestDTO.PostDTO.builder() + .isAnnouncement(true) + .theme(Theme.INFO_SHARING) + .title("50자가 넘어가는 제목 " + + "50자가 넘어가는 제목 " + + "50자가 넘어가는 제목 " + + "50자가 넘어가는 제목 " + + "50자가 넘어가는 제목 ") + .content("내용") + .build(); + + getAuthentication(memberId); + + when(memberStudyRepository.findByMemberIdAndStudyIdAndIsOwned(memberId, studyId, true)) + .thenReturn(Optional.of(ownerStudy)); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost2); + when(notificationRepository.save(any(Notification.class))).thenReturn(null); + when(memberStudyRepository.findAllByStudyIdAndStatus(studyId, ApplicationStatus.APPROVED)) + .thenReturn(List.of(member1Study, ownerStudy)); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostCommandService.createPost(studyId, postPreviewDTO)); + } + + +/*-------------------------------------------------------- 게시글 삭제 ------------------------------------------------------------------------*/ + + @Test + @DisplayName("스터디 게시글 삭제 - (성공)") + void deletePost_Success() { + + // given + Long memberId = 1L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyPostRepository.findByIdAndMemberId(postId, memberId)) + .thenReturn(Optional.of(studyPost1)); + + // when + StudyPostResDTO.PostPreviewDTO result = studyPostCommandService.deletePost(studyId, postId); + + // then + assertNotNull(result); + assertThat(result.getPostId()).isEqualTo(1L); + assertThat(result.getTitle()).isEqualTo("잡담"); + verify(studyPostRepository, times(1)).delete(any(StudyPost.class)); + verify(studyLikedPostRepository, times(1)).deleteAllByStudyPostId(postId); + } + + @Test + @DisplayName("스터디 게시글 삭제 - 이미 삭제된 게시글인 경우 (실패)") + void deletePost_AlreadyDeleted_Fail() { + + // given + Long memberId = 1L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.empty()); + when(studyPostRepository.findByIdAndMemberId(postId, memberId)) + .thenReturn(Optional.empty()); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostCommandService.deletePost(studyId, postId)); + } + + @Test + @DisplayName("스터디 게시글 삭제 - 작성자 본인이나 스터디장이 아닌 경우 (실패)") + void deletePost_NotAvailableMember_Fail() { + + // given + Long memberId = 2L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyPostRepository.findByIdAndMemberId(postId, memberId)) + .thenReturn(Optional.of(studyPost1)); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostCommandService.deletePost(studyId, postId)); + } + +/*-------------------------------------------------------- 게시글 좋아요 ------------------------------------------------------------------------*/ + + @Test + @DisplayName("스터디 게시글 좋아요 - (성공)") + void likePost_Success() { + + // given + Long memberId = 3L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyLikedPostRepository.findByMemberIdAndStudyPostId(memberId, postId)) + .thenReturn(Optional.empty()); + when(studyLikedPostRepository.save(any(StudyLikedPost.class))).thenReturn(studyLikedPost); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost1); + + // when + StudyPostResDTO.PostLikeNumDTO result = studyPostCommandService.likePost(studyId, postId); + + // then + assertNotNull(result); + assertThat(result.getPostId()).isEqualTo(1L); + assertThat(result.getTitle()).isEqualTo("잡담"); + assertThat(result.getLikeNum()).isEqualTo(2); + verify(studyLikedPostRepository, times(1)).save(any(StudyLikedPost.class)); + } + + @Test + @DisplayName("스터디 게시글 좋아요 - 스터디 회원이 아닌 경우 (실패)") + void likePost_NotStudyMember_Fail() { + + // given + Long memberId = 2L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyLikedPostRepository.findByMemberIdAndStudyPostId(memberId, postId)) + .thenReturn(Optional.empty()); + when(studyLikedPostRepository.save(any(StudyLikedPost.class))).thenReturn(studyLikedPost); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost1); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostCommandService.likePost(studyId, postId)); + } + @Test + @DisplayName("스터디 게시글 좋아요 - 이미 좋아요를 누른 경우 (실패)") + void likePost_AlreadyLiked_Fail() { + + // given + Long memberId = 3L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyLikedPostRepository.findByMemberIdAndStudyPostId(memberId, postId)) + .thenReturn(Optional.of(studyLikedPost)); + when(studyLikedPostRepository.save(any(StudyLikedPost.class))).thenReturn(studyLikedPost); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost1); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostCommandService.likePost(studyId, postId)); + } + + +/*-------------------------------------------------------- 게시글 좋아요 취소 ------------------------------------------------------------------------*/ + + @Test + @DisplayName("스터디 게시글 좋아요 취소 - (성공)") + void cancelPostLike_Success() { + + // given + Long memberId = 3L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyLikedPostRepository.findByMemberIdAndStudyPostId(memberId, postId)) + .thenReturn(Optional.of(studyLikedPost)); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost1); + + // when + StudyPostResDTO.PostLikeNumDTO result = studyPostCommandService.cancelPostLike(studyId, postId); + + // then + assertNotNull(result); + assertThat(result.getPostId()).isEqualTo(1L); + assertThat(result.getTitle()).isEqualTo("잡담"); + assertThat(result.getLikeNum()).isEqualTo(0); + } + + @Test + @DisplayName("스터디 게시글 좋아요 취소 - 스터디 회원이 아닌 경우 (실패)") + void cancelPostLike_NotStudyMember_Fail() { + + // given + Long memberId = 2L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyLikedPostRepository.findByMemberIdAndStudyPostId(memberId, postId)) + .thenReturn(Optional.of(studyLikedPost)); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost1); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostCommandService.cancelPostLike(studyId, postId)); + } + + @Test + @DisplayName("스터디 게시글 좋아요 취소 - 좋아요를 누르지 않은 게시글인 경우 (실패)") + void cancelPostLike_NotLiked_Fail() { + + // given + Long memberId = 3L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyLikedPostRepository.findByMemberIdAndStudyPostId(memberId, postId)) + .thenReturn(Optional.empty()); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost1); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostCommandService.cancelPostLike(studyId, postId)); + } + + +/*-------------------------------------------------------- 댓글 작성 ------------------------------------------------------------------------*/ + + @Test + @DisplayName("스터디 게시글 댓글 작성 - 익명 댓글 (성공)") + void createComment_Anonymous_Success() { + + // given + Long memberId = 1L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + StudyPostCommentRequestDTO.CommentDTO commentDTO = StudyPostCommentRequestDTO.CommentDTO.builder() + .content("댓글") + .isAnonymous(true) + .build(); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyPostCommentRepository.save(any(StudyPostComment.class))).thenReturn(studyPost1Comment1); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost1); + when(studyPostCommentRepository.findAllByStudyPostId(postId)).thenReturn(List.of()); + when(studyPostCommentRepository.findAllByMemberIdAndStudyPostId(memberId, postId)).thenReturn(List.of()); + + // when + StudyPostCommentResponseDTO.CommentDTO result = studyPostCommandService.createComment(studyId, postId, commentDTO); + + // then + assertNotNull(result); + assertThat(result.getMember().getMemberId()).isEqualTo(1L); + assertThat(result.getMember().getName()).isEqualTo("익명1"); + assertThat(result.getContent()).isEqualTo("댓글"); + } + + @Test + @DisplayName("스터디 게시글 댓글 작성 - 실명 댓글 (성공)") + void createComment_Name_Success() { + + // given + Long memberId = 1L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + StudyPostCommentRequestDTO.CommentDTO commentDTO = StudyPostCommentRequestDTO.CommentDTO.builder() + .content("댓글") + .isAnonymous(false) + .build(); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyPostCommentRepository.save(any(StudyPostComment.class))).thenReturn(studyPost1Comment1); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost1); + when(studyPostCommentRepository.findAllByStudyPostId(postId)).thenReturn(List.of()); + when(studyPostCommentRepository.findAllByMemberIdAndStudyPostId(memberId, postId)).thenReturn(List.of()); + + // when + StudyPostCommentResponseDTO.CommentDTO result = studyPostCommandService.createComment(studyId, postId, commentDTO); + + // then + assertNotNull(result); + assertThat(result.getMember().getMemberId()).isEqualTo(1L); + assertThat(result.getMember().getName()).isEqualTo("회원1"); + assertThat(result.getContent()).isEqualTo("댓글"); + } + + @Test + @DisplayName("스터디 게시글 댓글 작성 - 스터디 회원이 아닌 경우 (실패)") + void createComment_NotStudyMember_Fail() { + + // given + Long memberId = 2L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + StudyPostCommentRequestDTO.CommentDTO commentDTO = StudyPostCommentRequestDTO.CommentDTO.builder() + .content("댓글") + .isAnonymous(false) + .build(); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyPostCommentRepository.save(any(StudyPostComment.class))).thenReturn(studyPost1Comment1); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost1); + when(studyPostCommentRepository.findAllByStudyPostId(postId)).thenReturn(List.of()); + when(studyPostCommentRepository.findAllByMemberIdAndStudyPostId(memberId, postId)).thenReturn(List.of()); + + // when + assertThrows(StudyHandler.class, () -> studyPostCommandService.createComment(studyId, postId, commentDTO)); + } + + + +/*-------------------------------------------------------- 답글 작성 ------------------------------------------------------------------------*/ + + @Test + @DisplayName("스터디 게시글 답글 작성 - 익명 댓글 (성공)") + void createReply_Anonymous_Success() { + + // given + Long memberId = 3L; + Long studyId = 1L; + Long postId = 1L; + Long commentId = 1L; + + getAuthentication(memberId); + + StudyPostCommentRequestDTO.CommentDTO commentDTO = StudyPostCommentRequestDTO.CommentDTO.builder() + .content("답글") + .isAnonymous(true) + .build(); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyPostCommentRepository.save(any(StudyPostComment.class))).thenReturn(studyPost1Comment1); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost1); + when(studyPostCommentRepository.findAllByStudyPostId(postId)) + .thenReturn(List.of(studyPost1Comment1)); + when(studyPostCommentRepository.findAllByMemberIdAndStudyPostId(memberId, postId)) + .thenReturn(List.of()); + + // when + StudyPostCommentResponseDTO.CommentDTO result = studyPostCommandService + .createReply(studyId, postId, commentId, commentDTO); + + //then + assertNotNull(result); + assertThat(result.getMember().getMemberId()).isEqualTo(3L); + assertThat(result.getMember().getName()).isEqualTo("익명2"); + assertThat(result.getContent()).isEqualTo("답글"); + } + + @Test + @DisplayName("스터디 게시글 답글 작성 - 실명 댓글 (성공)") + void createReply_Name_Success() { + + // given + Long memberId = 3L; + Long studyId = 1L; + Long postId = 1L; + Long commentId = 1L; + + getAuthentication(memberId); + + StudyPostCommentRequestDTO.CommentDTO commentDTO = StudyPostCommentRequestDTO.CommentDTO.builder() + .content("답글") + .isAnonymous(false) + .build(); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyPostCommentRepository.save(any(StudyPostComment.class))).thenReturn(studyPost1Comment1); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost1); + when(studyPostCommentRepository.findAllByStudyPostId(postId)) + .thenReturn(List.of(studyPost1Comment1)); + when(studyPostCommentRepository.findAllByMemberIdAndStudyPostId(memberId, postId)) + .thenReturn(List.of()); + + // when + StudyPostCommentResponseDTO.CommentDTO result = studyPostCommandService + .createReply(studyId, postId, commentId, commentDTO); + + //then + assertNotNull(result); + assertThat(result.getMember().getMemberId()).isEqualTo(3L); + assertThat(result.getMember().getName()).isEqualTo("회원3"); + assertThat(result.getContent()).isEqualTo("답글"); + } + + @Test + @DisplayName("스터디 게시글 답글 작성 - 스터디 회원이 아닌 경우 (실패)") + void createReply_NotStudyMember_Fail() { + + // given + Long memberId = 2L; + Long studyId = 1L; + Long postId = 1L; + Long commentId = 1L; + + getAuthentication(memberId); + + StudyPostCommentRequestDTO.CommentDTO commentDTO = StudyPostCommentRequestDTO.CommentDTO.builder() + .content("답글") + .isAnonymous(false) + .build(); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyPostCommentRepository.save(any(StudyPostComment.class))).thenReturn(studyPost1Comment1); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost1); + when(studyPostCommentRepository.findAllByStudyPostId(postId)) + .thenReturn(List.of(studyPost1Comment1)); + when(studyPostCommentRepository.findAllByMemberIdAndStudyPostId(memberId, postId)) + .thenReturn(List.of()); + + // when + assertThrows(StudyHandler.class, () -> studyPostCommandService.createReply(studyId, postId, commentId, commentDTO)); + } + + @Test + @DisplayName("스터디 게시글 답글 작성 - 상위 댓글이 존재하지 않는 경우 (실패)") + void createReply_ParentCommentNotExist_Fail() { + + // given + Long memberId = 1L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + StudyPostCommentRequestDTO.CommentDTO commentDTO = StudyPostCommentRequestDTO.CommentDTO.builder() + .content("답글") + .isAnonymous(false) + .build(); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyPostCommentRepository.save(any(StudyPostComment.class))).thenReturn(studyPost1Comment1); + when(studyPostRepository.save(any(StudyPost.class))).thenReturn(studyPost1); + when(studyPostCommentRepository.findAllByStudyPostId(postId)) + .thenReturn(List.of(studyPost1Comment1, studyPost1Comment2)); + when(studyPostCommentRepository.findAllByMemberIdAndStudyPostId(memberId, postId)) + .thenReturn(List.of(studyPost1Comment1)); + + // when + assertThrows(StudyHandler.class, () -> studyPostCommandService.createReply(studyId, postId, null, commentDTO)); + } + + +/*-------------------------------------------------------- 댓글 삭제 ------------------------------------------------------------------------*/ + + // @Test + // @DisplayName("스터디 게시글 댓글 삭제") + // void deleteComment() { + // } + +/*-------------------------------------------------------- 댓글 좋아요 ------------------------------------------------------------------------*/ + + @Test + @DisplayName("스터디 게시글 댓글 좋아요 - (성공)") + void likeComment_Success() { + + // given + Long memberId = 1L; + Long studyId = 1L; + Long postId = 1L; + Long commentId = 1L; + + getAuthentication(memberId); + + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, commentId, true)) + .thenReturn(Optional.empty()); + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, commentId, false)) + .thenReturn(Optional.empty()); + when(studyLikedCommentRepository.save(any(StudyLikedComment.class))).thenReturn(studyLikedComment); + + // when + StudyPostCommentResponseDTO.CommentPreviewDTO result = studyPostCommandService.likeComment(studyId, postId, commentId); + + // then + assertNotNull(result); + assertThat(result.getCommentId()).isEqualTo(1L); + assertThat(result.getLikeCount()).isEqualTo(1L); + assertThat(result.getDislikeCount()).isEqualTo(0L); + } + + @Test + @DisplayName("스터디 게시글 댓글 좋아요 - 스터디 회원이 아닌 경우 (실패)") + void likeComment_NotStudyMember_Fail() { + + // given + Long memberId = 2L; + Long studyId = 1L; + Long postId = 1L; + Long commentId = 1L; + + getAuthentication(memberId); + + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, commentId, true)) + .thenReturn(Optional.empty()); + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, commentId, false)) + .thenReturn(Optional.empty()); + when(studyLikedCommentRepository.save(any(StudyLikedComment.class))).thenReturn(studyLikedComment); + + // when + assertThrows(StudyHandler.class, () -> studyPostCommandService.likeComment(studyId, postId, commentId)); + } + + @Test + @DisplayName("스터디 게시글 댓글 좋아요 - 이미 좋아요를 누른 경우 (실패)") + void likeComment_AlreadyLiked_Fail() { + + // given + Long memberId = 1L; + Long studyId = 1L; + Long postId = 1L; + Long commentId = 2L; + + getAuthentication(memberId); + + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, commentId, true)) + .thenReturn(Optional.of(studyLikedComment)); + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, commentId, false)) + .thenReturn(Optional.empty()); + when(studyLikedCommentRepository.save(any(StudyLikedComment.class))).thenReturn(studyLikedComment); + + // when + assertThrows(StudyHandler.class, () -> studyPostCommandService.likeComment(studyId, postId, commentId)); + } + + +/*-------------------------------------------------------- 댓글 싫어요 ------------------------------------------------------------------------*/ + + @Test + @DisplayName("스터디 게시글 댓글 싫어요 - (성공)") + void dislikeComment_Success() { + + // given + Long memberId = 1L; + Long studyId = 1L; + Long postId = 1L; + Long commentId = 1L; + + getAuthentication(memberId); + + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, commentId, true)) + .thenReturn(Optional.empty()); + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, commentId, false)) + .thenReturn(Optional.empty()); + when(studyLikedCommentRepository.save(any(StudyLikedComment.class))).thenReturn(studyLikedComment); + + // when + StudyPostCommentResponseDTO.CommentPreviewDTO result = studyPostCommandService.dislikeComment(studyId, postId, commentId); + + // then + assertNotNull(result); + assertThat(result.getCommentId()).isEqualTo(1L); + assertThat(result.getLikeCount()).isEqualTo(1L); + assertThat(result.getDislikeCount()).isEqualTo(0L); + } + + @Test + @DisplayName("스터디 게시글 댓글 싫어요 - 스터디 회원인 아닌 경우(실패)") + void dislikeComment_NotStudyMember_Fail() { + + // given + Long memberId = 2L; + Long studyId = 1L; + Long postId = 1L; + Long commentId = 1L; + + getAuthentication(memberId); + + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, commentId, true)) + .thenReturn(Optional.empty()); + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, commentId, false)) + .thenReturn(Optional.empty()); + when(studyLikedCommentRepository.save(any(StudyLikedComment.class))).thenReturn(studyLikedComment); + + // when + assertThrows(StudyHandler.class, () -> studyPostCommandService.dislikeComment(studyId, postId, commentId)); + + } + + @Test + @DisplayName("스터디 게시글 댓글 싫어요 - 이미 싫어요를 누른 경우(실패)") + void dislikeComment_AlreadyDisliked_Fail() { + + // given + Long memberId = 3L; + Long studyId = 1L; + Long postId = 1L; + Long commentId = 2L; + + getAuthentication(memberId); + + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, commentId, true)) + .thenReturn(Optional.empty()); + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, commentId, false)) + .thenReturn(Optional.of(studyLikedComment)); + when(studyLikedCommentRepository.save(any(StudyLikedComment.class))).thenReturn(studyLikedComment); + + // when + assertThrows(StudyHandler.class, () -> studyPostCommandService.dislikeComment(studyId, postId, commentId)); + } + +/*-------------------------------------------------------- 댓글 좋아요 취소 ------------------------------------------------------------------------*/ + + @Test + @DisplayName("스터디 게시글 댓글 좋아요 취소 - (성공)") + void cancelCommentLike_Success() { + + // given + Long memberId = 1L; + Long studyId = 1L; + Long postId = 1L; + Long commentId = 2L; + + getAuthentication(memberId); + + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, studyPost1Comment2.getId(), true)) + .thenReturn(Optional.of(studyLikedComment)); + when(studyPostCommentRepository.save(any(StudyPostComment.class))).thenReturn(studyPost1Comment2); + + // when + StudyPostCommentResponseDTO.CommentPreviewDTO result = studyPostCommandService + .cancelCommentLike(studyId, postId, commentId); + + // then + assertNotNull(result); + assertThat(result.getCommentId()).isEqualTo(2L); + assertThat(result.getLikeCount()).isEqualTo(0L); + assertThat(result.getDislikeCount()).isEqualTo(1L); + } + + @Test + @DisplayName("스터디 게시글 댓글 좋아요 취소 - 스터디 회원이 아닌 경우 (실패)") + void cancelCommentLike_NotStudyMember_Fail() { + + // given + Long memberId = 2L; + Long studyId = 1L; + Long postId = 1L; + Long commentId = 2L; + + getAuthentication(memberId); + + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, studyPost1Comment2.getId(), true)) + .thenReturn(Optional.of(studyLikedComment)); + when(studyPostCommentRepository.save(any(StudyPostComment.class))).thenReturn(studyPost1Comment2); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostCommandService.cancelCommentLike(studyId, postId, commentId)); + } + + @Test + @DisplayName("스터디 게시글 댓글 좋아요 취소 - 좋아요를 누른 댓글이 아닌 경우 (실패)") + void cancelCommentLike_NotLiked_Fail() { + + // given + Long memberId = 3L; + Long studyId = 1L; + Long postId = 1L; + Long commentId = 2L; + + getAuthentication(memberId); + + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, studyPost1Comment2.getId(), true)) + .thenReturn(Optional.empty()); + when(studyPostCommentRepository.save(any(StudyPostComment.class))).thenReturn(studyPost1Comment2); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostCommandService.cancelCommentLike(studyId, postId, commentId)); + } + +/*-------------------------------------------------------- 댓글 싫어요 취소 ------------------------------------------------------------------------*/ + + @Test + @DisplayName("스터디 게시글 댓글 싫어요 취소 - (성공)") + void cancelCommentDislike() { + + // given + Long memberId = 3L; + Long studyId = 1L; + Long postId = 1L; + Long commentId = 2L; + + getAuthentication(memberId); + + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, studyPost1Comment2.getId(), false)) + .thenReturn(Optional.of(studyDislikedComment)); + when(studyPostCommentRepository.save(any(StudyPostComment.class))).thenReturn(studyPost1Comment2); + + // when + StudyPostCommentResponseDTO.CommentPreviewDTO result = studyPostCommandService + .cancelCommentDislike(studyId, postId, commentId); + + // then + assertNotNull(result); + assertThat(result.getCommentId()).isEqualTo(2L); + assertThat(result.getLikeCount()).isEqualTo(1L); + assertThat(result.getDislikeCount()).isEqualTo(0L); + } + + @Test + @DisplayName("스터디 게시글 댓글 싫어요 취소 - 스터디 회원이 아닌 경우 (실패)") + void cancelCommentDislike_NotStudyMember_Fail() { + + // given + Long memberId = 2L; + Long studyId = 1L; + Long postId = 1L; + Long commentId = 2L; + + getAuthentication(memberId); + + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, studyPost1Comment2.getId(), false)) + .thenReturn(Optional.of(studyDislikedComment)); + when(studyPostCommentRepository.save(any(StudyPostComment.class))).thenReturn(studyPost1Comment2); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostCommandService.cancelCommentDislike(studyId, postId, commentId)); + } + + @Test + @DisplayName("스터디 게시글 댓글 싫어요 취소 - 싫어요를 누른 댓글이 아닌 경우 (실패)") + void cancelCommentDislike_NotDisliked_Fail() { + + // given + Long memberId = 1L; + Long studyId = 1L; + Long postId = 1L; + Long commentId = 2L; + + getAuthentication(memberId); + + when(studyLikedCommentRepository.findByMemberIdAndStudyPostCommentIdAndIsLiked(memberId, studyPost1Comment2.getId(), false)) + .thenReturn(Optional.empty()); + when(studyPostCommentRepository.save(any(StudyPostComment.class))).thenReturn(studyPost1Comment2); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostCommandService.cancelCommentDislike(studyId, postId, commentId)); + } + + +/*-------------------------------------------------------- Utils ------------------------------------------------------------------------*/ + + private static void getAuthentication(Long memberId) { + String idString = String.valueOf(memberId); + Authentication authentication = new UsernamePasswordAuthenticationToken(idString, null, Collections.emptyList()); + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(authentication); + SecurityContextHolder.setContext(securityContext); + } + + private static void initMember() { + member1 = Member.builder() + .id(1L) + .name("회원1") + .studyPostList(new ArrayList<>()) + .studyLikedPostList(new ArrayList<>()) + .studyPostCommentList(new ArrayList<>()) + .studyLikedCommentList(new ArrayList<>()) + .build(); + member2 = Member.builder() + .id(2L) + .name("회원2") + .studyPostList(new ArrayList<>()) + .studyLikedPostList(new ArrayList<>()) + .studyPostCommentList(new ArrayList<>()) + .studyLikedCommentList(new ArrayList<>()) + .build(); + owner = Member.builder() + .id(3L) + .name("회원3") + .studyPostList(new ArrayList<>()) + .studyLikedPostList(new ArrayList<>()) + .studyPostCommentList(new ArrayList<>()) + .studyLikedCommentList(new ArrayList<>()) + .build(); + } + + private static void initStudy() { + study = Study.builder() + .id(1L) + .gender(Gender.MALE) + .minAge(20) + .maxAge(29) + .fee(10000) + .profileImage("a.jpg") + .hasFee(true) + .isOnline(true) + .goal("SQLD") + .introduction("SQLD 자격증 스터디") + .title("SQLD Master") + .maxPeople(10L) + .build(); + } + + private static void initMemberStudy() { + ownerStudy = MemberStudy.builder() + .id(1L) + .status(ApplicationStatus.APPROVED) + .isOwned(true) + .introduction("Hi") + .member(owner) + .study(study) + .build(); + owner.addMemberStudy(ownerStudy); + study.addMemberStudy(ownerStudy); + + member1Study = MemberStudy.builder() + .id(2L) + .status(ApplicationStatus.APPROVED) + .isOwned(false) + .introduction("Hi") + .member(member1) + .study(study) + .build(); + member1.addMemberStudy(member1Study); + study.addMemberStudy(member1Study); + } + + private static void initStudyPost() { + studyPost1 = StudyPost.builder() + .id(1L) + .member(member1) + .study(study) + .isAnnouncement(false) + .theme(Theme.FREE_TALK) + .title("잡담") + .content("내용") + .hitNum(0) + .likeNum(0) + .commentNum(0) + .build(); + member1.addStudyPost(studyPost1); + study.addStudyPost(studyPost1); + + studyPost2 = StudyPost.builder() + .id(2L) + .member(owner) + .study(study) + .isAnnouncement(true) + .theme(Theme.INFO_SHARING) + .title("공지") + .content("내용") + .hitNum(0) + .likeNum(0) + .commentNum(0) + .build(); + owner.addStudyPost(studyPost2); + study.addStudyPost(studyPost2); + + studyPost3 = StudyPost.builder() + .id(3L) + .member(owner) + .study(study) + .isAnnouncement(false) + .theme(Theme.FREE_TALK) + .title("테스트") + .content("내용") + .hitNum(0) + .likeNum(0) + .commentNum(0) + .build(); + owner.addStudyPost(studyPost3); + study.addStudyPost(studyPost3); + + for (int i=0; i<10; i++) { + studyPost1.plusHitNum(); + } + } + + private static void initStudyLikedPost() { + studyLikedPost = StudyLikedPost.builder() + .id(1L) + .studyPost(studyPost1) + .member(owner) + .build(); + studyPost1.addLikedPost(studyLikedPost); + studyPost1.plusLikeNum(); + owner.addStudyLikedPost(studyLikedPost); + } + + private static void initStudyPostComment() { + studyPost1Comment1 = StudyPostComment.builder() + .id(1L) + .studyPost(studyPost1) + .member(member1) + .content("댓글") + .likeCount(0) + .dislikeCount(0) + .isAnonymous(true) + .anonymousNum(1) + .isDeleted(false) + .parentComment(null) + .build(); + studyPost1.addComment(studyPost1Comment1); + + studyPost1Comment2 = StudyPostComment.builder() + .id(2L) + .studyPost(studyPost1) + .member(owner) + .content("답글") + .likeCount(0) + .dislikeCount(0) + .isAnonymous(false) + .isDeleted(false) + .parentComment(studyPost1Comment1) + .build(); + studyPost1Comment1.addChildrenComment(studyPost1Comment2); + studyPost1.addComment(studyPost1Comment2); + } + + private static void initStudyLikedComment() { + studyLikedComment = StudyLikedComment.builder() + .id(1L) + .isLiked(true) + .studyPostComment(studyPost1Comment2) + .member(member1) + .build(); + studyPost1Comment2.addLikedComment(studyLikedComment); + studyPost1Comment2.plusLikeCount(); + member1.addStudyLikedComment(studyLikedComment); + + studyDislikedComment = StudyLikedComment.builder() + .id(2L) + .isLiked(false) + .studyPostComment(studyPost1Comment2) + .member(owner) + .build(); + studyPost1Comment2.addLikedComment(studyDislikedComment); + studyPost1Comment2.plusDislikeCount(); + owner.addStudyLikedComment(studyDislikedComment); + } +} \ No newline at end of file diff --git a/src/test/java/com/example/spot/service/studypost/StudyPostQueryServiceTest.java b/src/test/java/com/example/spot/service/studypost/StudyPostQueryServiceTest.java new file mode 100644 index 00000000..09fe76e1 --- /dev/null +++ b/src/test/java/com/example/spot/service/studypost/StudyPostQueryServiceTest.java @@ -0,0 +1,600 @@ +package com.example.spot.service.studypost; + +import com.example.spot.api.exception.handler.StudyHandler; +import com.example.spot.domain.Member; +import com.example.spot.domain.enums.ApplicationStatus; +import com.example.spot.domain.enums.Gender; +import com.example.spot.domain.enums.Theme; +import com.example.spot.domain.enums.ThemeQuery; +import com.example.spot.domain.mapping.MemberStudy; +import com.example.spot.domain.mapping.StudyLikedComment; +import com.example.spot.domain.mapping.StudyLikedPost; +import com.example.spot.domain.study.Study; +import com.example.spot.domain.study.StudyPost; +import com.example.spot.domain.study.StudyPostComment; +import com.example.spot.repository.*; +import com.example.spot.web.dto.memberstudy.response.StudyPostCommentResponseDTO; +import com.example.spot.web.dto.memberstudy.response.StudyPostResDTO; +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.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; + +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.*; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class StudyPostQueryServiceTest { + + @Mock + private MemberRepository memberRepository; + + @Mock + private StudyRepository studyRepository; + @Mock + private MemberStudyRepository memberStudyRepository; + + @Mock + private StudyPostRepository studyPostRepository; + @Mock + private StudyLikedPostRepository studyLikedPostRepository; + + @Mock + private StudyPostCommentRepository studyPostCommentRepository; + + @InjectMocks + private StudyPostQueryServiceImpl studyPostQueryService; + + private static PageRequest pageRequest; + + private static Study study; + private static Member member1; + private static Member member2; + private static Member owner; + private static MemberStudy member1Study; + private static MemberStudy ownerStudy; + + private static StudyPost studyPost1; + private static StudyPost studyPost2; + private static StudyPost studyPost3; + private static StudyLikedPost studyLikedPost; + private static StudyPostComment studyPost1Comment1; + private static StudyPostComment studyPost1Comment2; + private static StudyLikedComment studyLikedComment; + private static StudyLikedComment studyDislikedComment; + + @BeforeEach + void setUp() { + initMember(); + initStudy(); + initMemberStudy(); + initStudyPost(); + initStudyLikedPost(); + initStudyPostComment(); + initStudyLikedComment(); + + // Member + when(memberRepository.findById(1L)).thenReturn(Optional.of(member1)); + when(memberRepository.findById(2L)).thenReturn(Optional.of(member2)); + when(memberRepository.findById(3L)).thenReturn(Optional.of(owner)); + + // Study + when(studyRepository.findById(1L)).thenReturn(Optional.of(study)); + + // MemberStudy + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(1L, 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(member1Study)); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(2L, 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.empty()); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(3L, 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(ownerStudy)); + + // StudyPost + when(studyPostRepository.findById(1L)).thenReturn(Optional.of(studyPost1)); + when(studyPostRepository.findById(2L)).thenReturn(Optional.of(studyPost2)); + when(studyPostRepository.findById(3L)).thenReturn(Optional.of(studyPost3)); + + when(studyLikedPostRepository.findByMemberIdAndStudyPostId(3L, 1L)) + .thenReturn(Optional.of(studyLikedPost)); + when(studyLikedPostRepository.existsByMemberIdAndStudyPostId(3L, 1L)) + .thenReturn(true); + + // Comment + when(studyPostCommentRepository.findAllByStudyPostId(1L)) + .thenReturn(List.of(studyPost1Comment1, studyPost1Comment2)); + + } + +/*-------------------------------------------------------- 게시글 목록 조회 ------------------------------------------------------------------------*/ + + @Test + @DisplayName("스터디 게시글 목록 조회 - 전체 게시글 조회 (성공)") + void getAllPosts_All_Success() { + + // given + Long studyId = 1L; + Long memberId = 1L; + + getAuthentication(memberId); + + pageRequest = PageRequest.of(0, 10); + when(studyPostRepository.findAllByStudyId(studyId, pageRequest)) + .thenReturn(List.of(studyPost1, studyPost2, studyPost3)); + when(studyPostRepository.findAllByStudyIdAndIsAnnouncement(studyId, true, pageRequest)) + .thenReturn(List.of(studyPost2)); + when(studyPostRepository.findAllByStudyIdAndTheme(studyId, Theme.FREE_TALK, pageRequest)) + .thenReturn(List.of(studyPost1, studyPost3)); + + // when + StudyPostResDTO.PostListDTO result = studyPostQueryService.getAllPosts(pageRequest, studyId, null); + + // then + assertNotNull(result); + assertThat(result.getPosts()).isNotEmpty(); + assertThat(result.getPosts()).size().isLessThanOrEqualTo(10); + assertThat(result.getPosts()).size().isEqualTo(3); + assertThat(result.getStudyId()).isEqualTo(studyId); + } + + @Test + @DisplayName("스터디 게시글 목록 조회 - 테마별 조회 (성공)") + void getAllPosts_Theme_Success() { + + // given + Long studyId = 1L; + Long memberId = 1L; + + getAuthentication(memberId); + + pageRequest = PageRequest.of(0, 10); + when(studyPostRepository.findAllByStudyId(studyId, pageRequest)) + .thenReturn(List.of(studyPost1, studyPost2, studyPost3)); + when(studyPostRepository.findAllByStudyIdAndIsAnnouncement(studyId, true, pageRequest)) + .thenReturn(List.of(studyPost2)); + when(studyPostRepository.findAllByStudyIdAndTheme(studyId, Theme.FREE_TALK, pageRequest)) + .thenReturn(List.of(studyPost1, studyPost3)); + + // when + StudyPostResDTO.PostListDTO result = studyPostQueryService.getAllPosts(pageRequest, studyId, ThemeQuery.FREE_TALK); + + // then + assertNotNull(result); + assertThat(result.getPosts()).isNotEmpty(); + assertThat(result.getPosts()).size().isLessThanOrEqualTo(10); + assertThat(result.getPosts()).size().isEqualTo(2); + assertThat(result.getStudyId()).isEqualTo(studyId); + } + + @Test + @DisplayName("스터디 게시글 목록 조회 - 공지 조회 (성공)") + void getAllPosts_Announcements_Success() { + + // given + Long studyId = 1L; + Long memberId = 1L; + + getAuthentication(memberId); + + pageRequest = PageRequest.of(0, 10); + when(studyPostRepository.findAllByStudyId(studyId, pageRequest)) + .thenReturn(List.of(studyPost1, studyPost2, studyPost3)); + when(studyPostRepository.findAllByStudyIdAndIsAnnouncement(studyId, true, pageRequest)) + .thenReturn(List.of(studyPost2)); + when(studyPostRepository.findAllByStudyIdAndTheme(studyId, Theme.FREE_TALK, pageRequest)) + .thenReturn(List.of(studyPost1, studyPost3)); + + // when + StudyPostResDTO.PostListDTO result = studyPostQueryService.getAllPosts(pageRequest, studyId, ThemeQuery.ANNOUNCEMENT); + + // then + assertNotNull(result); + assertThat(result.getPosts()).isNotEmpty(); + assertThat(result.getPosts()).size().isLessThanOrEqualTo(10); + assertThat(result.getPosts()).size().isEqualTo(1); + assertThat(result.getStudyId()).isEqualTo(studyId); + } + + @Test + @DisplayName("스터디 게시글 목록 조회 - 스터디 회원이 아닌 경우(실패)") + void getAllPosts_NotStudyMember_Fail() { + + // given + Long studyId = 1L; + Long memberId = 2L; + + getAuthentication(memberId); + + pageRequest = PageRequest.of(0, 10); + when(studyPostRepository.findAllByStudyId(studyId, pageRequest)) + .thenReturn(List.of(studyPost1, studyPost2, studyPost3)); + when(studyPostRepository.findAllByStudyIdAndIsAnnouncement(studyId, true, pageRequest)) + .thenReturn(List.of(studyPost2)); + when(studyPostRepository.findAllByStudyIdAndTheme(studyId, Theme.FREE_TALK, pageRequest)) + .thenReturn(List.of(studyPost1, studyPost3)); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostQueryService.getAllPosts(pageRequest, studyId, null)); + } + + @Test + @DisplayName("스터디 게시글 목록 조회 - 존재하는 카테고리가 아닌 경우(실패)") + void getAllPosts_NotCategorized_Fail() { + + // given + Long studyId = 1L; + Long memberId = 1L; + + getAuthentication(memberId); + + pageRequest = PageRequest.of(0, 10); + when(studyPostRepository.findAllByStudyId(studyId, pageRequest)) + .thenReturn(List.of(studyPost1, studyPost2, studyPost3)); + when(studyPostRepository.findAllByStudyIdAndIsAnnouncement(studyId, true, pageRequest)) + .thenReturn(List.of(studyPost2)); + when(studyPostRepository.findAllByStudyIdAndTheme(studyId, Theme.FREE_TALK, pageRequest)) + .thenReturn(List.of(studyPost1, studyPost3)); + + // when & then + assertThrows(IllegalArgumentException.class, () -> studyPostQueryService.getAllPosts(pageRequest, studyId, ThemeQuery.valueOf("Nothing"))); + } + + +/*-------------------------------------------------------- 게시글 조회 ------------------------------------------------------------------------*/ + + @Test + @DisplayName("스터디 게시글 단건 조회 - (성공)") + void getPost_Success() { + + // given + Long studyId = 1L; + Long memberId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyPostRepository.save(studyPost1)).thenReturn(studyPost1); + when(memberRepository.save(member1)).thenReturn(member1); + when(studyRepository.save(study)).thenReturn(study); + when(studyPostCommentRepository.findAllByStudyPostId(postId)) + .thenReturn(List.of(studyPost1Comment1, studyPost1Comment2)); + when(studyLikedPostRepository.existsByMemberIdAndStudyPostId(memberId, postId)) + .thenReturn(false); + + // when + StudyPostResDTO.PostDetailDTO result = studyPostQueryService.getPost(studyId, postId); + + // then + assertNotNull(result); + assertThat(result.getPostId()).isEqualTo(1L); + assertThat(result.getHitNum()).isEqualTo(11); + assertThat(result.getTitle()).isEqualTo("잡담"); + assertThat(result.getCommentNum()).isEqualTo(2); + assertThat(result.getIsLiked()).isEqualTo(false); + + } + + @Test + @DisplayName("스터디 게시글 단건 조회 - 스터디 회원이 아닌 경우(실패)") + void getPost_NotStudyMember_Fail() { + + // given + Long studyId = 1L; + Long memberId = 2L; + Long postId = 1L; + + getAuthentication(memberId); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyPostRepository.save(studyPost1)).thenReturn(studyPost1); + when(memberRepository.save(member1)).thenReturn(member1); + when(studyRepository.save(study)).thenReturn(study); + when(studyPostCommentRepository.findAllByStudyPostId(postId)) + .thenReturn(List.of(studyPost1Comment1, studyPost1Comment2)); + when(studyLikedPostRepository.existsByMemberIdAndStudyPostId(memberId, postId)) + .thenReturn(false); + + // when & then + assertThrows(StudyHandler.class, () ->studyPostQueryService.getPost(studyId, postId)); + } + + @Test + @DisplayName("스터디 게시글 단건 조회 - 스터디 게시글이 아닌 경우(실패)") + void getPost_NotStudyPost_Fail() { + + // given + Long studyId = 1L; + Long memberId = 2L; + Long postId = 1L; + + getAuthentication(memberId); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.empty()); + when(studyPostCommentRepository.findAllByStudyPostId(postId)) + .thenReturn(List.of()); + + // when & then + assertThrows(StudyHandler.class, () ->studyPostQueryService.getPost(studyId, postId)); + } + +/*-------------------------------------------------------- 댓글 목록 조회 ------------------------------------------------------------------------*/ + + @Test + @DisplayName("스터디 게시글 댓글 목록 조회 - (성공)") + void getAllComments_Success() { + + // given + Long memberId = 1L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyPostCommentRepository.findAllByStudyPostId(postId)) + .thenReturn(List.of(studyPost1Comment1, studyPost1Comment2)); + + // when + StudyPostCommentResponseDTO.CommentReplyListDTO result = studyPostQueryService.getAllComments(studyId, postId); + + // then + assertNotNull(result); + assertThat(result.getPostId()).isEqualTo(1L); + assertThat(result.getComments()).size().isEqualTo(1); + result.getComments() + .forEach(comment -> { + assertThat(comment.getCommentId()).isEqualTo(1L); // 댓글 1개 + assertThat(comment.getApplies()).size().isEqualTo(1L); // 답글 1개 + }); + } + + @Test + @DisplayName("스터디 게시글 댓글 목록 조회 - 스터디 회원이 아닌 경우(실패)") + void getAllComments_NotStudyMember_Fail() { + + // given + Long memberId = 2L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.of(studyPost1)); + when(studyPostCommentRepository.findAllByStudyPostId(postId)) + .thenReturn(List.of(studyPost1Comment1, studyPost1Comment2)); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostQueryService.getAllComments(studyId, postId)); + } + + @Test + @DisplayName("스터디 게시글 댓글 목록 조회 - 스터디 게시글이 아닌 경우(실패)") + void getAllComments_NotStudyPost_Fail() { + + // given + Long memberId = 1L; + Long studyId = 1L; + Long postId = 1L; + + getAuthentication(memberId); + + when(studyPostRepository.findByIdAndStudyId(postId, studyId)) + .thenReturn(Optional.empty()); + when(studyPostCommentRepository.findAllByStudyPostId(postId)) + .thenReturn(List.of()); + + // when & then + assertThrows(StudyHandler.class, () -> studyPostQueryService.getAllComments(studyId, postId)); + } + + +/*-------------------------------------------------------- Utils ------------------------------------------------------------------------*/ + + private static void getAuthentication(Long memberId) { + String idString = String.valueOf(memberId); + Authentication authentication = new UsernamePasswordAuthenticationToken(idString, null, Collections.emptyList()); + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(authentication); + SecurityContextHolder.setContext(securityContext); + } + + private static void initMember() { + member1 = Member.builder() + .id(1L) + .studyPostList(new ArrayList<>()) + .studyLikedPostList(new ArrayList<>()) + .studyPostCommentList(new ArrayList<>()) + .studyLikedCommentList(new ArrayList<>()) + .build(); + member2 = Member.builder() + .id(2L) + .studyPostList(new ArrayList<>()) + .studyLikedPostList(new ArrayList<>()) + .studyPostCommentList(new ArrayList<>()) + .studyLikedCommentList(new ArrayList<>()) + .build(); + owner = Member.builder() + .id(3L) + .studyPostList(new ArrayList<>()) + .studyLikedPostList(new ArrayList<>()) + .studyPostCommentList(new ArrayList<>()) + .studyLikedCommentList(new ArrayList<>()) + .build(); + } + + private static void initStudy() { + study = Study.builder() + .id(1L) + .gender(Gender.MALE) + .minAge(20) + .maxAge(29) + .fee(10000) + .profileImage("a.jpg") + .hasFee(true) + .isOnline(true) + .goal("SQLD") + .introduction("SQLD 자격증 스터디") + .title("SQLD Master") + .maxPeople(10L) + .build(); + } + + private static void initMemberStudy() { + ownerStudy = MemberStudy.builder() + .id(1L) + .status(ApplicationStatus.APPROVED) + .isOwned(true) + .introduction("Hi") + .member(owner) + .study(study) + .build(); + owner.addMemberStudy(ownerStudy); + study.addMemberStudy(ownerStudy); + + member1Study = MemberStudy.builder() + .id(2L) + .status(ApplicationStatus.APPROVED) + .isOwned(false) + .introduction("Hi") + .member(member1) + .study(study) + .build(); + member1.addMemberStudy(member1Study); + study.addMemberStudy(member1Study); + } + + private static void initStudyPost() { + studyPost1 = StudyPost.builder() + .id(1L) + .member(member1) + .study(study) + .isAnnouncement(false) + .theme(Theme.FREE_TALK) + .title("잡담") + .content("내용") + .hitNum(0) + .likeNum(0) + .commentNum(0) + .build(); + member1.addStudyPost(studyPost1); + study.addStudyPost(studyPost1); + + studyPost2 = StudyPost.builder() + .id(2L) + .member(owner) + .study(study) + .isAnnouncement(true) + .theme(Theme.INFO_SHARING) + .title("공지") + .content("내용") + .hitNum(0) + .likeNum(0) + .commentNum(0) + .build(); + owner.addStudyPost(studyPost2); + study.addStudyPost(studyPost2); + + studyPost3 = StudyPost.builder() + .id(3L) + .member(owner) + .study(study) + .isAnnouncement(false) + .theme(Theme.FREE_TALK) + .title("테스트") + .content("내용") + .hitNum(0) + .likeNum(0) + .commentNum(0) + .build(); + owner.addStudyPost(studyPost3); + study.addStudyPost(studyPost3); + + for (int i=0; i<10; i++) { + studyPost1.plusHitNum(); + } + } + + private static void initStudyLikedPost() { + studyLikedPost = StudyLikedPost.builder() + .id(1L) + .studyPost(studyPost1) + .member(owner) + .build(); + studyPost1.addLikedPost(studyLikedPost); + studyPost1.plusLikeNum(); + owner.addStudyLikedPost(studyLikedPost); + } + + private static void initStudyPostComment() { + studyPost1Comment1 = StudyPostComment.builder() + .id(1L) + .studyPost(studyPost1) + .member(member1) + .content("댓글") + .likeCount(0) + .dislikeCount(0) + .isAnonymous(true) + .isDeleted(false) + .parentComment(null) + .build(); + studyPost1Comment2 = StudyPostComment.builder() + .id(2L) + .studyPost(studyPost1) + .member(owner) + .content("답글") + .likeCount(0) + .dislikeCount(0) + .isAnonymous(false) + .isDeleted(false) + .parentComment(studyPost1Comment1) + .build(); + studyPost1Comment1.addChildrenComment(studyPost1Comment2); + + studyPost1.addComment(studyPost1Comment1); + studyPost1.addComment(studyPost1Comment2); + } + + private static void initStudyLikedComment() { + studyLikedComment = StudyLikedComment.builder() + .id(1L) + .isLiked(true) + .studyPostComment(studyPost1Comment2) + .member(member1) + .build(); + studyPost1Comment2.addLikedComment(studyLikedComment); + studyPost1Comment2.plusLikeCount(); + member1.addStudyLikedComment(studyLikedComment); + + studyDislikedComment = StudyLikedComment.builder() + .id(2L) + .isLiked(false) + .studyPostComment(studyPost1Comment2) + .member(owner) + .build(); + studyPost1Comment2.addLikedComment(studyDislikedComment); + studyPost1Comment2.plusDislikeCount(); + owner.addStudyLikedComment(studyDislikedComment); + } +} \ No newline at end of file