diff --git a/src/main/java/com/example/spot/domain/Post.java b/src/main/java/com/example/spot/domain/Post.java index 4dacbf33..d3a254f3 100644 --- a/src/main/java/com/example/spot/domain/Post.java +++ b/src/main/java/com/example/spot/domain/Post.java @@ -67,7 +67,7 @@ public class Post extends BaseEntity { @JoinColumn(name = "member_id") private Member member; - public void edit(PostUpdateRequest request, List images) { + public void edit(PostUpdateRequest request, List images, String existingImage) { if (StringUtils.hasText(request.getTitle())) { this.title = request.getTitle(); } @@ -78,13 +78,25 @@ public void edit(PostUpdateRequest request, List images) { this.isAnonymous = request.isAnonymous(); - this.image = (images != null && !images.isEmpty() && StringUtils.hasText(images.get(0))) - ? images.get(0) - : null; + updateImage(images, existingImage); if (request.getType() != null) { this.board = request.getType(); } + + if (request.getType() != null) { + this.board = request.getType(); + } + } + + private void updateImage(List images, String existingImage) { + if (StringUtils.hasText(existingImage)) { + this.image = existingImage; + } else if (images != null && !images.isEmpty() && StringUtils.hasText(images.get(0))) { + this.image = images.get(0); + } else { + this.image = null; + } } 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 587285c3..0e7aeec6 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 @@ -4,6 +4,8 @@ import com.example.spot.domain.enums.Status; import com.example.spot.domain.enums.StudySortBy; import com.example.spot.domain.enums.StudyState; +import com.example.spot.domain.enums.Theme; +import com.example.spot.domain.enums.ThemeType; import com.example.spot.domain.mapping.MemberStudy; import com.example.spot.domain.mapping.QMemberStudy; import com.example.spot.domain.mapping.QRegionStudy; @@ -424,6 +426,10 @@ private static void getConditions(Map search, QStudy study, if (search.get("fee") != null) { builder.and(study.fee.loe((Integer) search.get("fee"))); } + if (search.get("themeTypes") != null) { + List themeTypes = (List) search.get("themeTypes"); + builder.and(study.studyThemes.any().theme.studyTheme.in(themeTypes)); + } } } diff --git a/src/main/java/com/example/spot/service/auth/AuthServiceImpl.java b/src/main/java/com/example/spot/service/auth/AuthServiceImpl.java index 54e2c0de..aea01c17 100644 --- a/src/main/java/com/example/spot/service/auth/AuthServiceImpl.java +++ b/src/main/java/com/example/spot/service/auth/AuthServiceImpl.java @@ -7,6 +7,9 @@ import com.example.spot.domain.Member; import com.example.spot.domain.auth.RsaKey; import com.example.spot.repository.MemberStudyRepository; +import com.example.spot.repository.MemberThemeRepository; +import com.example.spot.repository.PreferredRegionRepository; +import com.example.spot.repository.StudyReasonRepository; import com.example.spot.web.dto.rsa.Rsa; import com.example.spot.domain.auth.RefreshToken; import com.example.spot.domain.auth.VerificationCode; @@ -67,6 +70,10 @@ public class AuthServiceImpl implements AuthService{ private final RefreshTokenRepository refreshTokenRepository; private final VerificationCodeRepository verificationCodeRepository; + private final MemberThemeRepository memberThemeRepository; + private final PreferredRegionRepository preferredRegionRepository; + private final StudyReasonRepository studyReasonRepository; + private final MailService mailService; private final NaverOAuthService naverOAuthService; @@ -263,6 +270,10 @@ private SocialLoginSignInDTO getSocialLoginSignInDTO(NaverMember.ResponseDTO res Member member = memberRepository.findByEmailAndLoginType(email, LoginType.NAVER) .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + if (!isMemberExistsByCheckList(member)) { + isSpotMember = Boolean.FALSE; + } + // 로그인을 위한 토큰 발급 TokenDTO token = jwtTokenProvider.createToken(member.getId()); saveRefreshToken(member, token); @@ -277,6 +288,13 @@ private SocialLoginSignInDTO getSocialLoginSignInDTO(NaverMember.ResponseDTO res return SocialLoginSignInDTO.toDTO(isSpotMember, signInDTO); } + public boolean isMemberExistsByCheckList(Member member) { + Long memberId = member.getId(); + return memberThemeRepository.existsByMemberId(memberId) && + preferredRegionRepository.existsByMemberId(memberId) && + studyReasonRepository.existsByMemberId(memberId); + } + /** * 현재 SPOT에 가입되어 있지 않은 회원에 한해 회원 정보를 생성하여 DB에 저장합니다. * @param memberDTO : naverCallback을 바탕으로 생성된 프로필 객체 diff --git a/src/main/java/com/example/spot/service/member/MemberServiceImpl.java b/src/main/java/com/example/spot/service/member/MemberServiceImpl.java index cc3bfc35..131e191a 100644 --- a/src/main/java/com/example/spot/service/member/MemberServiceImpl.java +++ b/src/main/java/com/example/spot/service/member/MemberServiceImpl.java @@ -111,7 +111,10 @@ public MemberResponseDTO.SocialLoginSignInDTO signUpByKAKAO(String accessToken) updateMemberProfileImage(member, kaKaoUser); - isSpotMember = true; + if (isMemberExistsByCheckList(member)) { + isSpotMember = true; + } + // JWT 토큰 생성 TokenDTO token = jwtTokenProvider.createToken(member.getId()); @@ -322,7 +325,9 @@ public SocialLoginSignInDTO signUpByKAKAOForTest(String code) throws JsonProcess saveRefreshToken(member, token); - isSpotMember = true; + if (isMemberExistsByCheckList(member)) { + isSpotMember = true; + } // 로그인 DTO 반환 MemberSignInDTO memberSignInDto = MemberSignInDTO.builder() @@ -674,6 +679,14 @@ private Long parseUsernameToMemberId(String username) { } } + public boolean isMemberExistsByCheckList(Member member) { + Long memberId = member.getId(); + return memberThemeRepository.existsByMemberId(memberId) && + preferredRegionRepository.existsByMemberId(memberId) && + studyReasonRepository.existsByMemberId(memberId); + } + + @Override @Transactional public void save(Member member) { diff --git a/src/main/java/com/example/spot/service/post/PostCommandServiceImpl.java b/src/main/java/com/example/spot/service/post/PostCommandServiceImpl.java index 0bd3106b..f5c54bf2 100644 --- a/src/main/java/com/example/spot/service/post/PostCommandServiceImpl.java +++ b/src/main/java/com/example/spot/service/post/PostCommandServiceImpl.java @@ -140,7 +140,7 @@ public PostCreateResponse updatePost(Long memberId, Long postId, PostUpdateReque } // 게시글 수정 - post.edit(postUpdateRequest, getImageUrls(postUpdateRequest.getImage())); + post.edit(postUpdateRequest, getImageUrls(postUpdateRequest.getImage()), postUpdateRequest.getExistingImage()); // 수정된 게시글 정보 반환 return PostCreateResponse.toDTO(post); diff --git a/src/main/java/com/example/spot/service/study/StudyQueryService.java b/src/main/java/com/example/spot/service/study/StudyQueryService.java index 6f92155a..50790fb3 100644 --- a/src/main/java/com/example/spot/service/study/StudyQueryService.java +++ b/src/main/java/com/example/spot/service/study/StudyQueryService.java @@ -2,7 +2,8 @@ import com.example.spot.domain.enums.StudySortBy; import com.example.spot.domain.enums.ThemeType; -import com.example.spot.web.dto.search.SearchRequestDTO.SearchRequestStudyDTO; +import com.example.spot.web.dto.search.SearchRequestStudyDTO; +import com.example.spot.web.dto.search.SearchRequestStudyWithThemeDTO; import com.example.spot.web.dto.search.SearchResponseDTO.HotKeywordDTO; import com.example.spot.web.dto.search.SearchResponseDTO.MyPageDTO; import com.example.spot.web.dto.search.SearchResponseDTO.StudyPreviewDTO; @@ -41,16 +42,16 @@ StudyPreviewDTO findInterestStudiesByConditionsSpecific(Pageable pageable, Long SearchRequestStudyDTO request, ThemeType theme, StudySortBy sortBy); // 내 관심 지역 스터디 페이징 조회 - StudyPreviewDTO findInterestRegionStudiesByConditionsAll(Pageable pageable, Long memberId, - SearchRequestStudyDTO request, StudySortBy sortBy); + StudyPreviewDTO findInterestRegionStudiesByConditionsAll( + Pageable pageable, Long memberId, SearchRequestStudyWithThemeDTO request, StudySortBy sortBy); // 내 특정 관심 지역 스터디 페이징 조회 - StudyPreviewDTO findInterestRegionStudiesByConditionsSpecific(Pageable pageable, Long memberId, - SearchRequestStudyDTO request, String regionCode, StudySortBy sortBy); + StudyPreviewDTO findInterestRegionStudiesByConditionsSpecific( + Pageable pageable, Long memberId, SearchRequestStudyWithThemeDTO request, String regionCode, StudySortBy sortBy); // 모집 중 스터디 조회 - StudyPreviewDTO findRecruitingStudiesByConditions(Pageable pageable, - SearchRequestStudyDTO request, StudySortBy sortBy); + StudyPreviewDTO findRecruitingStudiesByConditions( + Pageable pageable, SearchRequestStudyWithThemeDTO request, StudySortBy sortBy); // 찜한 스터디 조회 StudyPreviewDTO findLikedStudies(Long memberId, Pageable pageable); 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 b763dff6..c5996267 100644 --- a/src/main/java/com/example/spot/service/study/StudyQueryServiceImpl.java +++ b/src/main/java/com/example/spot/service/study/StudyQueryServiceImpl.java @@ -29,7 +29,8 @@ import com.example.spot.repository.StudyThemeRepository; import com.example.spot.repository.ThemeRepository; import com.example.spot.security.utils.SecurityUtils; -import com.example.spot.web.dto.search.SearchRequestDTO.SearchRequestStudyDTO; +import com.example.spot.web.dto.search.SearchRequestStudyDTO; +import com.example.spot.web.dto.search.SearchRequestStudyWithThemeDTO; import com.example.spot.web.dto.search.SearchResponseDTO; import com.example.spot.web.dto.search.SearchResponseDTO.HotKeywordDTO; import com.example.spot.web.dto.search.SearchResponseDTO.MyPageDTO; @@ -472,8 +473,8 @@ public StudyPreviewDTO findInterestStudiesByConditionsSpecific(Pageable pageable * */ @Override - public StudyPreviewDTO findInterestRegionStudiesByConditionsAll(Pageable pageable, - Long memberId, SearchRequestStudyDTO request, StudySortBy sortBy) { + public StudyPreviewDTO findInterestRegionStudiesByConditionsAll( + Pageable pageable, Long memberId, SearchRequestStudyWithThemeDTO request, StudySortBy sortBy) { // 회원이 참가하고 있는 스터디 ID 가져오기 List memberOngoingStudyIds = getOngoingStudyIds(memberId); @@ -498,7 +499,7 @@ public StudyPreviewDTO findInterestRegionStudiesByConditionsAll(Pageable pageabl throw new StudyHandler(ErrorStatus._STUDY_REGION_NOT_EXIST); // 검색 조건 맵 생성 - Map conditions = getSearchConditions(request); + Map conditions = getSearchConditionsWithTheme(request); // 검색 조건에 맞는 스터디 갯수 조회 @@ -535,8 +536,8 @@ public StudyPreviewDTO findInterestRegionStudiesByConditionsAll(Pageable pageabl * */ @Override - public StudyPreviewDTO findInterestRegionStudiesByConditionsSpecific(Pageable pageable, - Long memberId, SearchRequestStudyDTO request, String regionCode, StudySortBy sortBy) { + public StudyPreviewDTO findInterestRegionStudiesByConditionsSpecific( + Pageable pageable, Long memberId, SearchRequestStudyWithThemeDTO request, String regionCode, StudySortBy sortBy) { // 회원이 참가하고 있는 스터디 ID 가져오기 List memberOngoingStudyIds = getOngoingStudyIds(memberId); @@ -568,7 +569,7 @@ public StudyPreviewDTO findInterestRegionStudiesByConditionsSpecific(Pageable pa throw new StudyHandler(ErrorStatus._STUDY_REGION_NOT_EXIST); // 검색 조건 맵 생성 - Map conditions = getSearchConditions(request); + Map conditions = getSearchConditionsWithTheme(request); // 검색 조건에 맞는 스터디 갯수 조회 long totalElements = studyRepository.countStudyByConditionsAndRegionStudiesAndNotInIds( @@ -598,11 +599,11 @@ public StudyPreviewDTO findInterestRegionStudiesByConditionsSpecific(Pageable pa * */ @Override - public StudyPreviewDTO findRecruitingStudiesByConditions(Pageable pageable, - SearchRequestStudyDTO request, StudySortBy sortBy) { + public StudyPreviewDTO findRecruitingStudiesByConditions( + Pageable pageable, SearchRequestStudyWithThemeDTO request, StudySortBy sortBy) { // 검색 조건 맵 생성 - Map conditions = getSearchConditions(request); + Map conditions = getSearchConditionsWithTheme(request); // 검색 조건(모집 중)에 맞는 스터디 조회 List studies = studyRepository.findRecruitingStudyByConditions(conditions, @@ -884,6 +885,38 @@ private static Map getSearchConditions(SearchRequestStudyDTO req return search; } + /** + * 검색 조건을 입력 받아 검색 조건 맵을 생성하는 메서드입니다. + * + * @param request 검색 조건을 입력 받습니다. + * + * @return 검색 조건 맵을 반환합니다. + * + */ + private static Map getSearchConditionsWithTheme(SearchRequestStudyWithThemeDTO request) { + Map search = new HashMap<>(); + + if (request.getGender() != null) + search.put("gender", request.getGender()); + if (request.getMinAge() != null) + search.put("minAge", request.getMinAge()); + if (request.getMaxAge() != null) + search.put("maxAge", request.getMaxAge()); + if (request.getIsOnline() != null) + search.put("isOnline", request.getIsOnline()); + if (request.getHasFee() != null) + search.put("hasFee", request.getHasFee()); + if (request.getFee() != null) + search.put("fee", request.getFee()); + + if (request.getThemeTypes() != null && !request.getThemeTypes().isEmpty()) { + search.put("themeTypes", request.getThemeTypes()); + } + + return search; + } + + /** * 스터디 목록을 DTO로 변환하는 메서드입니다. * diff --git a/src/main/java/com/example/spot/service/studypost/StudyPostQueryService.java b/src/main/java/com/example/spot/service/studypost/StudyPostQueryService.java index 2c154d42..e4868bd5 100644 --- a/src/main/java/com/example/spot/service/studypost/StudyPostQueryService.java +++ b/src/main/java/com/example/spot/service/studypost/StudyPostQueryService.java @@ -11,7 +11,7 @@ public interface StudyPostQueryService { StudyPostResDTO.PostListDTO getAllPosts(PageRequest pageRequest, Long studyId, ThemeQuery themeQuery); // 스터디 게시글 불러오기 - StudyPostResDTO.PostDetailDTO getPost(Long studyId, Long postId); + StudyPostResDTO.PostDetailDTO getPost(Long studyId, Long postId, Boolean likeOrScrap); // 스터디 게시글 댓글 목록 불러오기 StudyPostCommentResponseDTO.CommentReplyListDTO getAllComments(Long studyId, Long postId); diff --git a/src/main/java/com/example/spot/service/studypost/StudyPostQueryServiceImpl.java b/src/main/java/com/example/spot/service/studypost/StudyPostQueryServiceImpl.java index 647a9bb3..45322bbf 100644 --- a/src/main/java/com/example/spot/service/studypost/StudyPostQueryServiceImpl.java +++ b/src/main/java/com/example/spot/service/studypost/StudyPostQueryServiceImpl.java @@ -93,13 +93,15 @@ public StudyPostResDTO.PostListDTO getAllPosts(PageRequest pageRequest, Long stu /** * 스터디 게시판의 특정 게시글을 조회하는 메서드입니다. - * @param studyId 게시글을 조회할 타겟 스터디의 아이디를 입력 받습니다. - * @param postId 조회할 타겟 게시글의 아이디를 입력 받습니다. + * + * @param studyId 게시글을 조회할 타겟 스터디의 아이디를 입력 받습니다. + * @param postId 조회할 타겟 게시글의 아이디를 입력 받습니다. + * @param likeOrScrap * @return 스터디 게시글의 정보를 반환합니다. */ @Override @Transactional(readOnly = false) - public StudyPostResDTO.PostDetailDTO getPost(Long studyId, Long postId) { + public StudyPostResDTO.PostDetailDTO getPost(Long studyId, Long postId, Boolean likeOrScrap) { //=== Exception ===// Long memberId = SecurityUtils.getCurrentUserId(); @@ -121,7 +123,12 @@ public StudyPostResDTO.PostDetailDTO getPost(Long studyId, Long postId) { .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); //=== Feature ===// - studyPost.plusHitNum(); + + // 조회수 증가는 일반 조회 시에만 실행 + if (!likeOrScrap) { + studyPost.plusHitNum(); + } + studyPost = studyPostRepository.save(studyPost); memberRepository.save(member); studyRepository.save(study); diff --git a/src/main/java/com/example/spot/web/controller/PostController.java b/src/main/java/com/example/spot/web/controller/PostController.java index 0ecceb7a..1a2e5416 100644 --- a/src/main/java/com/example/spot/web/controller/PostController.java +++ b/src/main/java/com/example/spot/web/controller/PostController.java @@ -138,7 +138,8 @@ public ApiResponse getPostAnnouncement() { @Tag(name = "게시판", description = "게시판 관련 API") @Operation( summary = "[게시판] 게시글 수정 API", - description = "게시글 Id를 받아 게시글을 수정합니다.", + description = "게시글 Id를 받아 게시글을 수정합니다. existingImage는 기존 이미지 URL입니다. 수정할 이미지가 없을 경우 null로 보내주세요. 요청 시, 요청 타입은 Multipart/form-data로 보내야 합니다." + + "\n" + "existingImage와 image 둘 중 하나만 보내주세요. 둘 다 보내면 기존 이미지로 덮어씌워집니다.", security = @SecurityRequirement(name = "accessToken") ) @PatchMapping(value = "/{postId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) diff --git a/src/main/java/com/example/spot/web/controller/SearchController.java b/src/main/java/com/example/spot/web/controller/SearchController.java index 60824af8..efc99469 100644 --- a/src/main/java/com/example/spot/web/controller/SearchController.java +++ b/src/main/java/com/example/spot/web/controller/SearchController.java @@ -7,8 +7,8 @@ import com.example.spot.security.utils.SecurityUtils; import com.example.spot.service.study.StudyCommandService; import com.example.spot.service.study.StudyQueryService; -import com.example.spot.validation.annotation.ExistMember; -import com.example.spot.web.dto.search.SearchRequestDTO.SearchRequestStudyDTO; +import com.example.spot.web.dto.search.SearchRequestStudyDTO; +import com.example.spot.web.dto.search.SearchRequestStudyWithThemeDTO; import com.example.spot.web.dto.search.SearchResponseDTO.HotKeywordDTO; import com.example.spot.web.dto.search.SearchResponseDTO.MyPageDTO; import com.example.spot.web.dto.search.SearchResponseDTO.StudyPreviewDTO; @@ -206,7 +206,7 @@ public ApiResponse interestStudiesByConditionsSpecific( @Parameter(name = "size", description = "조회할 페이지 크기를 입력 받습니다. 페이지 크기는 1 이상의 정수 입니다. ", required = true) @Parameter(name = "sortBy", description = "정렬 기준을 입력 받습니다.", required = true) public ApiResponse interestRegionStudiesByConditionsAll( - @ModelAttribute @Valid SearchRequestStudyDTO searchRequestStudyDTO, + @ModelAttribute @Valid SearchRequestStudyWithThemeDTO searchRequestStudyDTO, @RequestParam @Min(0) Integer page, @RequestParam @Min(1) Integer size, @RequestParam StudySortBy sortBy @@ -240,7 +240,7 @@ public ApiResponse interestRegionStudiesByConditionsAll( @Parameter(name = "sortBy", description = "정렬 기준을 입력 받습니다.", required = true) public ApiResponse interestRegionStudiesByConditionsSpecific( @RequestParam String regionCode, - @ModelAttribute @Valid SearchRequestStudyDTO searchRequestStudyDTO, + @ModelAttribute @Valid SearchRequestStudyWithThemeDTO searchRequestStudyDTO, @RequestParam @Min(0) Integer page, @RequestParam @Min(1) Integer size, @RequestParam StudySortBy sortBy @@ -274,7 +274,7 @@ public ApiResponse interestRegionStudiesByConditionsSpecific( @Parameter(name = "size", description = "조회할 페이지 크기를 입력 받습니다. 페이지 크기는 1 이상의 정수 입니다. ", required = true) @Parameter(name = "sortBy", description = "정렬 기준을 입력 받습니다.", required = true) public ApiResponse recruitingStudiesByConditions( - @ModelAttribute @Valid SearchRequestStudyDTO searchRequestStudyDTO, + @ModelAttribute @Valid SearchRequestStudyWithThemeDTO searchRequestStudyDTO, @RequestParam @Min(0) Integer page, @RequestParam @Min(1) Integer size, @RequestParam StudySortBy sortBy) { diff --git a/src/main/java/com/example/spot/web/controller/StudyPostController.java b/src/main/java/com/example/spot/web/controller/StudyPostController.java index f4a3b8e0..4ed48aff 100644 --- a/src/main/java/com/example/spot/web/controller/StudyPostController.java +++ b/src/main/java/com/example/spot/web/controller/StudyPostController.java @@ -127,8 +127,9 @@ public ApiResponse getAllPosts( @GetMapping("/studies/{studyId}/posts/{postId}") public ApiResponse getPost( @PathVariable @ExistStudy Long studyId, - @PathVariable @ExistStudyPost Long postId) { - StudyPostResDTO.PostDetailDTO postDetailDTO = studyPostQueryService.getPost(studyId, postId); + @PathVariable @ExistStudyPost Long postId, + @RequestParam Boolean likeOrScrap) { + StudyPostResDTO.PostDetailDTO postDetailDTO = studyPostQueryService.getPost(studyId, postId, likeOrScrap); return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_FOUND, postDetailDTO); } diff --git a/src/main/java/com/example/spot/web/dto/post/PostUpdateRequest.java b/src/main/java/com/example/spot/web/dto/post/PostUpdateRequest.java index dc46f189..3449d3cd 100644 --- a/src/main/java/com/example/spot/web/dto/post/PostUpdateRequest.java +++ b/src/main/java/com/example/spot/web/dto/post/PostUpdateRequest.java @@ -37,6 +37,8 @@ public class PostUpdateRequest { private MultipartFile image; + private String existingImage; + public Board getType() { return Board.findByValue(type); } diff --git a/src/main/java/com/example/spot/web/dto/search/BaseSearchRequestStudyDTO.java b/src/main/java/com/example/spot/web/dto/search/BaseSearchRequestStudyDTO.java new file mode 100644 index 00000000..519b6370 --- /dev/null +++ b/src/main/java/com/example/spot/web/dto/search/BaseSearchRequestStudyDTO.java @@ -0,0 +1,62 @@ +package com.example.spot.web.dto.search; + +import com.example.spot.domain.enums.Gender; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class BaseSearchRequestStudyDTO { + + @Schema(description = "성별을 입력 받습니다.", example = "MALE") + private Gender gender; + + @Schema(description = "최소 나이 (18 이상).", example = "18") + @Min(value = 18, message = "최소 나이는 18세 입니다.") + @NotNull(message = "최소 나이는 필수 입력 값입니다.") + private Integer minAge; + + @Schema(description = "최대 나이 (60 이하).", example = "60") + @Max(value = 60, message = "최대 나이는 60세 입니다.") + @NotNull(message = "최대 나이는 필수 입력 값입니다.") + private Integer maxAge; + + @Schema(description = "스터디 온라인 진행 여부 (true, false).", example = "true") + private Boolean isOnline; + + @Schema(description = "스터디 활동비 유무 (true, false).", example = "false") + private Boolean hasFee; + + @Schema(description = "스터디 최대 활동비.", example = "10000") + @Max(value = 1000000, message = "최대 활동비는 1,000,000원 입니다.") + private Integer fee; + + // 공통 검증 로직 + @AssertTrue(message = "최소 나이는 최대 나이보다 작아야 합니다.") + private boolean isValidAgeRange() { + if (minAge == null || maxAge == null) { + return true; + } + return minAge <= maxAge; + } + + @AssertTrue(message = "활동비가 없는 경우에는 활동비 금액을 입력 받지 않습니다.") + private boolean isValidFee() { + if (hasFee == null || hasFee) { + return true; + } + return fee == null; + } +} diff --git a/src/main/java/com/example/spot/web/dto/search/SearchRequestDTO.java b/src/main/java/com/example/spot/web/dto/search/SearchRequestDTO.java deleted file mode 100644 index c91c2b0f..00000000 --- a/src/main/java/com/example/spot/web/dto/search/SearchRequestDTO.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.example.spot.web.dto.search; - -import com.example.spot.domain.enums.Gender; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.AssertTrue; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -public class SearchRequestDTO { - - @Getter - @Setter - @Builder - @AllArgsConstructor - @NoArgsConstructor - public static class SearchRequestStudyDTO { - - @Schema(description = "성별을 입력 받습니다.", example = "MALE") - private Gender gender; - - @Schema(description = "최소 나이 (18 이상).", example = "18") - @Min(value = 18, message = "최소 나이는 18세 입니다.") - @NotNull(message = "최소 나이는 필수 입력 값입니다.") - private Integer minAge; - - @Schema(description = "최대 나이 (60 이하).", example = "60") - @Max(value = 60, message = "최대 나이는 60세 입니다.") - @NotNull(message = "최대 나이는 필수 입력 값입니다.") - private Integer maxAge; - - @Schema(description = "스터디 온라인 진행 여부 (true, false).", example = "true") - private Boolean isOnline; - - @Schema(description = "스터디 활동비 유무 (true, false).", example = "false") - private Boolean hasFee; - - @Schema(description = "스터디 최대 활동비.", example = "10000") - @Max(value = 1000000, message = "최대 활동비는 1,000,000원 입니다.") - private Integer fee; - - // 커스텀 검증 어노테이션 - @AssertTrue(message = "최소 나이는 최대 나이보다 작아야 합니다.") - private boolean isValidAgeRange() { - if (minAge == null || maxAge == null) { - return true; - } - return minAge <= maxAge; - } - - // hasFee가 false인데, fee가 있는 경우 - @AssertTrue(message = "활동비가 없는 경우에는 활동비 금액을 입력 받지 않습니다.") - private boolean isValidFee() { - if (hasFee == null || hasFee) { - return true; - } - return fee == null; - } - } - - -} diff --git a/src/main/java/com/example/spot/web/dto/search/SearchRequestStudyDTO.java b/src/main/java/com/example/spot/web/dto/search/SearchRequestStudyDTO.java new file mode 100644 index 00000000..42ecf96e --- /dev/null +++ b/src/main/java/com/example/spot/web/dto/search/SearchRequestStudyDTO.java @@ -0,0 +1,25 @@ +package com.example.spot.web.dto.search; + +import com.example.spot.domain.enums.Gender; +import com.example.spot.domain.enums.ThemeType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +public class SearchRequestStudyDTO extends BaseSearchRequestStudyDTO { + +} + diff --git a/src/main/java/com/example/spot/web/dto/search/SearchRequestStudyWithThemeDTO.java b/src/main/java/com/example/spot/web/dto/search/SearchRequestStudyWithThemeDTO.java new file mode 100644 index 00000000..0afe8b8f --- /dev/null +++ b/src/main/java/com/example/spot/web/dto/search/SearchRequestStudyWithThemeDTO.java @@ -0,0 +1,22 @@ +package com.example.spot.web.dto.search; + +import com.example.spot.domain.enums.Gender; +import com.example.spot.domain.enums.ThemeType; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@SuperBuilder +@NoArgsConstructor +public class SearchRequestStudyWithThemeDTO extends BaseSearchRequestStudyDTO { + + @Schema(description = "스터디 테마 리스트입니다. (예: HOBBY, PROJECT, EXAM)", example = "[\"HOBBY\", \"PROJECT\"]") + private List themeTypes; +} 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 eeb4f0cc..f2d38588 100644 --- a/src/test/java/com/example/spot/service/study/StudyQueryServiceTest.java +++ b/src/test/java/com/example/spot/service/study/StudyQueryServiceTest.java @@ -31,7 +31,8 @@ import com.example.spot.repository.StudyRepository; import com.example.spot.repository.StudyThemeRepository; import com.example.spot.repository.ThemeRepository; -import com.example.spot.web.dto.search.SearchRequestDTO.SearchRequestStudyDTO; +import com.example.spot.web.dto.search.SearchRequestStudyDTO; +import com.example.spot.web.dto.search.SearchRequestStudyWithThemeDTO; import com.example.spot.web.dto.search.SearchResponseDTO.MyPageDTO; import com.example.spot.web.dto.search.SearchResponseDTO.StudyPreviewDTO; import com.example.spot.web.dto.study.response.StudyInfoResponseDTO.StudyInfoDTO; @@ -100,6 +101,7 @@ class StudyQueryServiceTest { private static StudyTheme studyTheme1; private static StudyTheme studyTheme2; private static SearchRequestStudyDTO request; + private static SearchRequestStudyWithThemeDTO requestWithTheme; private static Region region1; private static Region region2; private static PreferredRegion preferredRegion1; @@ -145,6 +147,7 @@ void setUp() { study1.addMemberStudy(memberStudy1); request = getSearchRequestStudyDTO(); + requestWithTheme = getSearchRequestStudyWithThemeDTO(); // 사용자 인증 정보 생성 Authentication authentication = new UsernamePasswordAuthenticationToken("1", null, Collections.emptyList()); @@ -1027,7 +1030,7 @@ void findInterestRegionStudiesByConditionsAll() { List studyIds = List.of(); // Mock conditions - Map searchConditions = getStringObjectMap(); + Map searchConditions = getStringObjectMapWithThemeType(); when(preferredRegionRepository.findAllByMemberId(member.getId())) .thenReturn(List.of(preferredRegion1, preferredRegion2)); @@ -1047,7 +1050,7 @@ void findInterestRegionStudiesByConditionsAll() { when(memberRepository.existsById(member.getId())).thenReturn(true); // when - StudyPreviewDTO result = studyQueryService.findInterestRegionStudiesByConditionsAll(pageable, member.getId(), request, sortBy); + StudyPreviewDTO result = studyQueryService.findInterestRegionStudiesByConditionsAll(pageable, member.getId(), requestWithTheme, sortBy); // then assertNotNull(result); @@ -1072,7 +1075,7 @@ void findInterestRegionStudiesByConditionsAll() { // when & then assertThrows(StudyHandler.class, () -> { - studyQueryService.findInterestRegionStudiesByConditionsAll(pageable, member.getId(), request, sortBy); + studyQueryService.findInterestRegionStudiesByConditionsAll(pageable, member.getId(), requestWithTheme, sortBy); }); // then @@ -1105,7 +1108,7 @@ void findInterestRegionStudiesByConditionsAllOnFail() { // when & then assertThrows(StudyHandler.class, () -> { - studyQueryService.findInterestRegionStudiesByConditionsAll(pageable, member.getId(), request, sortBy); + studyQueryService.findInterestRegionStudiesByConditionsAll(pageable, member.getId(), requestWithTheme, sortBy); }); // then @@ -1132,7 +1135,7 @@ void shouldReturnPagedStudiesByRegion(){ // when StudyPreviewDTO result = studyQueryService.findInterestRegionStudiesByConditionsAll( - PageRequest.of(0, 10), 1L, getSearchRequestStudyDTO(), StudySortBy.ALL); + PageRequest.of(0, 10), 1L, requestWithTheme, StudySortBy.ALL); // then assertEquals(10, result.getSize()); @@ -1158,7 +1161,7 @@ void shouldFilterStudiesBasedOnSearchConditionsByRegion(){ // when // 검색 조건이 안맞는 경우, 검색 조건에 맞는 스터디가 조회 되면 안됨. StudyPreviewDTO result = studyQueryService.findInterestRegionStudiesByConditionsAll( - pageable, member.getId(), getSearchRequestStudyDTO(), StudySortBy.ALL); + pageable, member.getId(), requestWithTheme, StudySortBy.ALL); // then assertEquals(1, result.getTotalElements()); @@ -1183,7 +1186,7 @@ void shouldFilterRegionStudiesBasedOnSortConditionsByHit(){ // when StudyPreviewDTO result = studyQueryService.findInterestRegionStudiesByConditionsAll( - pageable, member.getId(), getSearchRequestStudyDTO(), sortBy); + pageable, member.getId(), requestWithTheme, sortBy); // then assertEquals(2, result.getTotalElements()); @@ -1208,7 +1211,7 @@ void shouldFilterRegionStudiesBasedOnSortConditionsByLiked(){ // when StudyPreviewDTO result = studyQueryService.findInterestRegionStudiesByConditionsAll( - pageable, member.getId(), getSearchRequestStudyDTO(), sortBy); + pageable, member.getId(), requestWithTheme, sortBy); // then assertEquals(2, result.getTotalElements()); @@ -1229,7 +1232,7 @@ void findInterestRegionStudiesByConditionsAllOnNoInterest() { // when & then assertThrows(MemberHandler.class, () -> { - studyQueryService.findInterestRegionStudiesByConditionsAll(pageable, member.getId(), request, sortBy); + studyQueryService.findInterestRegionStudiesByConditionsAll(pageable, member.getId(), requestWithTheme, sortBy); }); } @@ -1245,10 +1248,9 @@ void findInterestRegionStudiesByConditionsSpecific() { StudySortBy sortBy = StudySortBy.ALL; List studyIds = List.of(); - SearchRequestStudyDTO request = getSearchRequestStudyDTO(); // Mock conditions - Map searchConditions = getStringObjectMap(); + Map searchConditions = getStringObjectMapWithThemeType(); when(preferredRegionRepository.findAllByMemberId(member.getId())) .thenReturn(List.of(preferredRegion1)); @@ -1268,7 +1270,7 @@ void findInterestRegionStudiesByConditionsSpecific() { when(memberRepository.existsById(member.getId())).thenReturn(true); // when - StudyPreviewDTO result = studyQueryService.findInterestRegionStudiesByConditionsSpecific(pageable, member.getId(), request, regionCode, sortBy); + StudyPreviewDTO result = studyQueryService.findInterestRegionStudiesByConditionsSpecific(pageable, member.getId(), requestWithTheme, regionCode, sortBy); // then assertNotNull(result); @@ -1294,7 +1296,7 @@ void findInterestRegionStudiesByConditionsSpecific() { // when & then assertThrows(StudyHandler.class, () -> { - studyQueryService.findInterestRegionStudiesByConditionsSpecific(pageable, member.getId(), request, region1.getCode(), sortBy); + studyQueryService.findInterestRegionStudiesByConditionsSpecific(pageable, member.getId(), requestWithTheme, region1.getCode(), sortBy); }); // then @@ -1312,7 +1314,7 @@ void findInterestRegionStudiesByConditionsSpecificOnFail() { List studyIds = List.of(); // Mock conditions - Map searchConditions = getStringObjectMap(); + Map searchConditions = getStringObjectMapWithThemeType(); when(preferredRegionRepository.findAllByMemberId(member.getId())) .thenReturn(List.of(preferredRegion1)); @@ -1331,7 +1333,7 @@ void findInterestRegionStudiesByConditionsSpecificOnFail() { // when & then assertThrows(StudyHandler.class, () -> { - studyQueryService.findInterestRegionStudiesByConditionsSpecific(pageable, member.getId(), request, regionCode, sortBy); + studyQueryService.findInterestRegionStudiesByConditionsSpecific(pageable, member.getId(), requestWithTheme, regionCode, sortBy); }); verify(preferredRegionRepository).findAllByMemberId(member.getId()); verify(regionStudyRepository).findAllByRegion(region1); // Ensure the correct theme is queried @@ -1357,7 +1359,7 @@ void shouldReturnPagedStudiesInSpecificRegion(){ // when StudyPreviewDTO result = studyQueryService.findInterestRegionStudiesByConditionsSpecific( - PageRequest.of(0, 10), 1L, getSearchRequestStudyDTO(), region1.getCode(), StudySortBy.ALL); + PageRequest.of(0, 10), 1L, requestWithTheme, region1.getCode(), StudySortBy.ALL); // then assertEquals(10, result.getSize()); @@ -1382,7 +1384,7 @@ void shouldFilterStudiesBasedOnSearchConditionsInSpecificRegion(){ // when // 검색 조건이 안맞는 경우, 검색 조건에 맞는 스터디가 조회 되면 안됨. StudyPreviewDTO result = studyQueryService.findInterestRegionStudiesByConditionsSpecific( - pageable, member.getId(), getSearchRequestStudyDTO(), region1.getCode(), StudySortBy.ALL); + pageable, member.getId(), requestWithTheme, region1.getCode(), StudySortBy.ALL); // then assertEquals(2, result.getTotalElements()); @@ -1408,7 +1410,7 @@ void shouldFilterStudiesInSpecificRegionBasedOnSortConditionsByHit(){ // when StudyPreviewDTO result = studyQueryService.findInterestRegionStudiesByConditionsSpecific( - pageable, member.getId(), getSearchRequestStudyDTO(), region1.getCode(), sortBy); + pageable, member.getId(), requestWithTheme, region1.getCode(), sortBy); // then assertEquals(3, result.getTotalElements()); @@ -1435,7 +1437,7 @@ void shouldFilterStudiesInSpecificRegionBasedOnSortConditionsByLiked(){ // when StudyPreviewDTO result = studyQueryService.findInterestRegionStudiesByConditionsSpecific( - pageable, member.getId(), getSearchRequestStudyDTO(),region1.getCode() ,sortBy); + pageable, member.getId(), requestWithTheme,region1.getCode() ,sortBy); // then assertEquals(3, result.getTotalElements()); @@ -1459,7 +1461,7 @@ void noThemeInMemberInterestRegion() { // when & then assertThrows(StudyHandler.class, () -> { - studyQueryService.findInterestRegionStudiesByConditionsSpecific(pageable, member.getId(), request, region1.getCode(), sortBy); + studyQueryService.findInterestRegionStudiesByConditionsSpecific(pageable, member.getId(), requestWithTheme, region1.getCode(), sortBy); }); } @@ -1473,7 +1475,7 @@ void findRecruitingStudiesByConditions() { // given StudySortBy sortBy = StudySortBy.ALL; - Map searchConditions = getStringObjectMap(); + Map searchConditions = getStringObjectMapWithThemeType(); when(studyRepository.findRecruitingStudyByConditions(searchConditions, sortBy, pageable)) .thenReturn(List.of(study1, study2)); @@ -1491,7 +1493,7 @@ void findRecruitingStudiesByConditions() { SecurityContextHolder.setContext(securityContext); // when - StudyPreviewDTO result = studyQueryService.findRecruitingStudiesByConditions(pageable, request, sortBy); + StudyPreviewDTO result = studyQueryService.findRecruitingStudiesByConditions(pageable, requestWithTheme, sortBy); // then assertNotNull(result); @@ -1511,7 +1513,7 @@ void findRecruitingStudiesByConditions() { // when & then assertThrows(StudyHandler.class, () -> { - studyQueryService.findRecruitingStudiesByConditions(pageable, request, StudySortBy.ALL); + studyQueryService.findRecruitingStudiesByConditions(pageable, requestWithTheme, StudySortBy.ALL); }); } @@ -1955,6 +1957,19 @@ private static Map getStringObjectMap() { return searchConditions; } + private static Map getStringObjectMapWithThemeType() { + // Mock conditions + Map searchConditions = new HashMap<>(); + searchConditions.put("gender", Gender.MALE); + searchConditions.put("minAge", 20); + searchConditions.put("maxAge", 40); + searchConditions.put("isOnline", true); + searchConditions.put("hasFee", true); + searchConditions.put("fee", 10000); + searchConditions.put("themeTypes", List.of(ThemeType.어학, ThemeType.공모전)); + return searchConditions; + } + private static SearchRequestStudyDTO getSearchRequestStudyDTO() { return SearchRequestStudyDTO.builder() .gender(Gender.MALE) @@ -1966,4 +1981,16 @@ private static SearchRequestStudyDTO getSearchRequestStudyDTO() { .build(); } + private static SearchRequestStudyWithThemeDTO getSearchRequestStudyWithThemeDTO() { + return SearchRequestStudyWithThemeDTO.builder() + .gender(Gender.MALE) + .minAge(20) + .maxAge(40) + .fee(10000) + .isOnline(true) + .hasFee(true) + .themeTypes(List.of(ThemeType.어학, ThemeType.공모전)) + .build(); + } + } diff --git a/src/test/java/com/example/spot/service/studypost/StudyPostQueryServiceTest.java b/src/test/java/com/example/spot/service/studypost/StudyPostQueryServiceTest.java index 09fe76e1..55b0ab7a 100644 --- a/src/test/java/com/example/spot/service/studypost/StudyPostQueryServiceTest.java +++ b/src/test/java/com/example/spot/service/studypost/StudyPostQueryServiceTest.java @@ -259,8 +259,8 @@ void getAllPosts_NotCategorized_Fail() { /*-------------------------------------------------------- 게시글 조회 ------------------------------------------------------------------------*/ @Test - @DisplayName("스터디 게시글 단건 조회 - (성공)") - void getPost_Success() { + @DisplayName("스터디 게시글 단건 조회 - 일반 조회 (성공)") + void getPost_Common_Success() { // given Long studyId = 1L; @@ -280,7 +280,7 @@ void getPost_Success() { .thenReturn(false); // when - StudyPostResDTO.PostDetailDTO result = studyPostQueryService.getPost(studyId, postId); + StudyPostResDTO.PostDetailDTO result = studyPostQueryService.getPost(studyId, postId, false); // then assertNotNull(result); @@ -289,11 +289,43 @@ void getPost_Success() { assertThat(result.getTitle()).isEqualTo("잡담"); assertThat(result.getCommentNum()).isEqualTo(2); assertThat(result.getIsLiked()).isEqualTo(false); + } + + @Test + @DisplayName("스터디 게시글 단건 조회 - 스크랩 혹은 좋아요 후 업데이트 (성공)") + void getPost_LikeOrScrap_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, true); + // then + assertNotNull(result); + assertThat(result.getPostId()).isEqualTo(1L); + assertThat(result.getHitNum()).isEqualTo(10); + assertThat(result.getTitle()).isEqualTo("잡담"); + assertThat(result.getCommentNum()).isEqualTo(2); + assertThat(result.getIsLiked()).isEqualTo(false); } @Test - @DisplayName("스터디 게시글 단건 조회 - 스터디 회원이 아닌 경우(실패)") + @DisplayName("스터디 게시글 단건 조회 - 스터디 회원이 아닌 경우 (실패)") void getPost_NotStudyMember_Fail() { // given @@ -314,7 +346,7 @@ void getPost_NotStudyMember_Fail() { .thenReturn(false); // when & then - assertThrows(StudyHandler.class, () ->studyPostQueryService.getPost(studyId, postId)); + assertThrows(StudyHandler.class, () ->studyPostQueryService.getPost(studyId, postId, false)); } @Test @@ -334,7 +366,7 @@ void getPost_NotStudyPost_Fail() { .thenReturn(List.of()); // when & then - assertThrows(StudyHandler.class, () ->studyPostQueryService.getPost(studyId, postId)); + assertThrows(StudyHandler.class, () ->studyPostQueryService.getPost(studyId, postId, false)); } /*-------------------------------------------------------- 댓글 목록 조회 ------------------------------------------------------------------------*/