diff --git a/src/main/java/com/loopon/challenge/application/converter/ChallengeConverter.java b/src/main/java/com/loopon/challenge/application/converter/ChallengeConverter.java index e1e5adf3..dd040874 100644 --- a/src/main/java/com/loopon/challenge/application/converter/ChallengeConverter.java +++ b/src/main/java/com/loopon/challenge/application/converter/ChallengeConverter.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; public class ChallengeConverter { @@ -163,21 +164,36 @@ public static ChallengeCommentResponse commentChallenge( public static ChallengeGetCommentCommand getCommentChallenge( Long challengeId, + Long userId, Pageable pageable ) { return ChallengeGetCommentCommand.builder() .challengeId(challengeId) + .userId(userId) .pageable(pageable) .build(); } public static ChallengeGetCommentResponse getCommentChallenge( Comment comment, - List children + List children, + Long userId, + Set likedCommentIds ) { List childResponses = new ArrayList<>(); for (Comment child : children) { + boolean isMine = false; + boolean isLiked = false; + + if (child.getUser().getId().equals(userId)) { + isMine = true; + } + + if (likedCommentIds.contains(child.getId())) { + isLiked = true; + } + childResponses.add(ChallengeGetCommentResponse.builder() .commentId(child.getId()) .content(child.getContent()) @@ -185,9 +201,22 @@ public static ChallengeGetCommentResponse getCommentChallenge( .profileImageUrl(child.getUser().getProfileImageUrl()) .likeCount(child.getLikeCount()) .children(null) + .isMine(isMine) + .isLiked(isLiked) .build()); } + boolean isMine = false; + boolean isLiked = false; + + if (comment.getUser().getId().equals(userId)) { + isMine = true; + } + + if (likedCommentIds.contains(comment.getId())) { + isLiked = true; + } + return ChallengeGetCommentResponse.builder() .commentId(comment.getId()) .content(comment.getContent()) @@ -195,6 +224,8 @@ public static ChallengeGetCommentResponse getCommentChallenge( .profileImageUrl(comment.getUser().getProfileImageUrl()) .likeCount(comment.getLikeCount()) .children(childResponses) + .isMine(isMine) + .isLiked(isLiked) .build(); } @@ -242,29 +273,6 @@ public static ChallengeDeleteCommand deleteChallenge( .build(); } - - public static ChallengeMyCommand myChallenge( - Long userId, - Pageable pageable - ) { - return ChallengeMyCommand.builder() - .userId(userId) - .pageable(pageable) - .build(); - } - - public static ChallengeOthersCommand othersChallenge( - Long userId, - String nickname, - Pageable pageable - ) { - return ChallengeOthersCommand.builder() - .userId(userId) - .nickname(nickname) - .pageable(pageable) - .build(); - } - public static ChallengeViewCommand viewChallenge( Long userId, Pageable trendingPage, diff --git a/src/main/java/com/loopon/challenge/application/dto/command/ChallengeGetCommentCommand.java b/src/main/java/com/loopon/challenge/application/dto/command/ChallengeGetCommentCommand.java index f638724c..e744d9a4 100644 --- a/src/main/java/com/loopon/challenge/application/dto/command/ChallengeGetCommentCommand.java +++ b/src/main/java/com/loopon/challenge/application/dto/command/ChallengeGetCommentCommand.java @@ -6,6 +6,7 @@ @Builder public record ChallengeGetCommentCommand( Long challengeId, + Long userId, Pageable pageable ) { } diff --git a/src/main/java/com/loopon/challenge/application/dto/command/ChallengeMyCommand.java b/src/main/java/com/loopon/challenge/application/dto/command/ChallengeMyCommand.java deleted file mode 100644 index 72dd28e8..00000000 --- a/src/main/java/com/loopon/challenge/application/dto/command/ChallengeMyCommand.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.loopon.challenge.application.dto.command; - -import lombok.Builder; -import org.springframework.data.domain.Pageable; - -@Builder -public record ChallengeMyCommand( - Long userId, - Pageable pageable -) { -} diff --git a/src/main/java/com/loopon/challenge/application/dto/command/ChallengeOthersCommand.java b/src/main/java/com/loopon/challenge/application/dto/command/ChallengeOthersCommand.java deleted file mode 100644 index 7f7f9d10..00000000 --- a/src/main/java/com/loopon/challenge/application/dto/command/ChallengeOthersCommand.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.loopon.challenge.application.dto.command; - -import lombok.Builder; -import org.springframework.data.domain.Pageable; - - -@Builder -public record ChallengeOthersCommand( - Long userId, - String nickname, - Pageable pageable -) { -} diff --git a/src/main/java/com/loopon/challenge/application/dto/response/ChallengeGetCommentResponse.java b/src/main/java/com/loopon/challenge/application/dto/response/ChallengeGetCommentResponse.java index 1d073d7b..d93e0904 100644 --- a/src/main/java/com/loopon/challenge/application/dto/response/ChallengeGetCommentResponse.java +++ b/src/main/java/com/loopon/challenge/application/dto/response/ChallengeGetCommentResponse.java @@ -11,6 +11,8 @@ public record ChallengeGetCommentResponse( String profileImageUrl, String content, Integer likeCount, + Boolean isMine, + Boolean isLiked, List children ) { } diff --git a/src/main/java/com/loopon/challenge/application/service/ChallengeQueryService.java b/src/main/java/com/loopon/challenge/application/service/ChallengeQueryService.java index 93222f70..f07dc4ed 100644 --- a/src/main/java/com/loopon/challenge/application/service/ChallengeQueryService.java +++ b/src/main/java/com/loopon/challenge/application/service/ChallengeQueryService.java @@ -3,10 +3,7 @@ import com.loopon.challenge.application.converter.ChallengeConverter; import com.loopon.challenge.application.dto.command.*; import com.loopon.challenge.application.dto.response.*; -import com.loopon.challenge.domain.Challenge; -import com.loopon.challenge.domain.ChallengeHashtag; -import com.loopon.challenge.domain.ChallengeImage; -import com.loopon.challenge.domain.Comment; +import com.loopon.challenge.domain.*; import com.loopon.challenge.domain.repository.ChallengeRepository; import com.loopon.global.domain.ErrorCode; import com.loopon.global.domain.dto.SliceResponse; @@ -23,13 +20,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.stream.Stream; @Service @@ -73,56 +64,53 @@ public ChallengeGetResponse getChallenge( @Transactional(readOnly = true) public SliceResponse getCommentChallenge( - ChallengeGetCommentCommand commandDto + ChallengeGetCommentCommand dto ) { - Challenge challenge = challengeRepository.findById(commandDto.challengeId()) + Challenge challenge = challengeRepository.findById(dto.challengeId()) .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND)); - Slice comments = challengeRepository.findCommentsWithUserByChallengeId(challenge.getId(), commandDto.pageable()); - List parentIds = new ArrayList<>(); - for (Comment comment : comments.getContent()) { - parentIds.add(comment.getId()); - } + Slice comments = challengeRepository.findCommentsWithUserByChallengeId(challenge.getId(), dto.pageable()); + + List parentIds = comments.getContent().stream() + .map(Comment::getId) + .toList(); List children = challengeRepository.findAllCommentWithUserByParentIdIn(parentIds); + + List allCommentIds = new ArrayList<>(parentIds); + children.forEach(c -> allCommentIds.add(c.getId())); + + List commentLikes = challengeRepository.findAllCommentLikeByUserIdAndCommentIdIn(dto.userId(), allCommentIds); + + Set likedCommentIds = new HashSet<>(); + + for (CommentLike like : commentLikes) { + Long commentId = like.getComment().getId(); + likedCommentIds.add(commentId); + } + + Map> childrenMap = new HashMap<>(); + for (Comment child : children) { Long parentId = child.getParent().getId(); childrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(child); } + return SliceResponse.from(comments.map(comment -> - ChallengeConverter.getCommentChallenge(comment, childrenMap.getOrDefault(comment.getId(), new ArrayList<>())) + ChallengeConverter.getCommentChallenge( + comment, + childrenMap.getOrDefault(comment.getId(), Collections.emptyList()), + dto.userId(), + likedCommentIds + ) )); } - @Transactional(readOnly = true) - public SliceResponse myChallenge( - ChallengeMyCommand commandDto - ) { - User user = userRepository.findById(commandDto.userId()) - .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND)); - - return SliceResponse.from(challengeRepository.findViewByUserId(user.getId(), commandDto.pageable())); - } - - @Transactional(readOnly = true) - public SliceResponse othersChallenge( - ChallengeOthersCommand commandDto - ) { - User myself = userRepository.findById(commandDto.userId()) - .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); - User target = userRepository.findByNickname(commandDto.nickname()) - .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); - - checkAllowed(target, myself); - - return SliceResponse.from(challengeRepository.findViewByUserId(target.getId(), commandDto.pageable())); - } - @Transactional(readOnly = true) public ChallengeCombinedViewResponse viewChallenge( ChallengeViewCommand commandDto @@ -133,7 +121,7 @@ public ChallengeCombinedViewResponse viewChallenge( LocalDateTime threeDaysAgo = LocalDateTime.now().minusDays(3); Slice trendingChallenges = challengeRepository.findTrendingChallenges( - threeDaysAgo, commandDto.trendingPage()); + threeDaysAgo, user.getId(), commandDto.trendingPage()); List trendingIds = new ArrayList<>(); diff --git a/src/main/java/com/loopon/challenge/domain/repository/ChallengeRepository.java b/src/main/java/com/loopon/challenge/domain/repository/ChallengeRepository.java index 3e9ccc13..74a5460e 100644 --- a/src/main/java/com/loopon/challenge/domain/repository/ChallengeRepository.java +++ b/src/main/java/com/loopon/challenge/domain/repository/ChallengeRepository.java @@ -1,11 +1,8 @@ package com.loopon.challenge.domain.repository; -import com.loopon.challenge.domain.*; import org.springframework.data.domain.Page; -import com.loopon.challenge.application.dto.response.ChallengePreviewResponse; import com.loopon.challenge.domain.Challenge; import com.loopon.challenge.domain.ChallengeHashtag; -import com.loopon.challenge.domain.ChallengeHashtagId; import com.loopon.challenge.domain.ChallengeImage; import com.loopon.challenge.domain.ChallengeLike; import com.loopon.challenge.domain.Comment; @@ -13,7 +10,6 @@ import com.loopon.challenge.domain.Hashtag; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; @@ -28,26 +24,18 @@ public interface ChallengeRepository { Long save(Challenge challenge); - ChallengeHashtagId saveChallengeHashtag(ChallengeHashtag challengeHashtag); - Long saveChallengeImage(ChallengeImage challengeImage); Hashtag saveHashtag(Hashtag hashtag); - List findAllChallengeHashtagByChallengeId(Long id); - List findAllChallengeHashtagWithHashtagByChallengeId(Long id); Optional findHashtagByName(String name); - List findAllHashtagByNameIn(List hashtagList); - List findAllImageByChallengeId(Long challengeId); Optional findById(Long challengeId); - void deleteChallengeHashtag(ChallengeHashtag challengeHashtag); - void deleteAllByExpeditionId(Long expeditionId); Slice findAllWithJourneyAndUserByExpeditionId(Long expeditionId, Pageable pageable); @@ -55,8 +43,6 @@ public interface ChallengeRepository { Boolean existsChallengeLikeByIdAndUserId(Long challengeId, Long userId); Page findThumbnailsByUserId(Long userId, Pageable pageable); - - void saveAllHashtags(List hashtagList); Optional findChallengeLikeByUserIdAndId(Long userId, Long challengeId); @@ -80,11 +66,9 @@ public interface ChallengeRepository { void delete(Challenge challenge); - Slice findViewByUserId(Long userId, Pageable pageable); - List findAllCommentWithUserByParentIdIn(List parentIds); - Slice findTrendingChallenges(LocalDateTime threeDaysAgo, Pageable pageable); + Slice findTrendingChallenges(LocalDateTime threeDaysAgo, Long userId, Pageable pageable); Slice findFriendsChallenges(List friendsIds, List trendingIds, Pageable pageable); @@ -95,4 +79,6 @@ public interface ChallengeRepository { Boolean existsCommentLikeByCommentIdAndUserId(Long commentId, Long userId); Slice findAllWithJourneyAndUserByUserId(Long userId, Pageable pageable); + + List findAllCommentLikeByUserIdAndCommentIdIn(Long userId, List commentIds); } diff --git a/src/main/java/com/loopon/challenge/infrastructure/ChallengeRepositoryImpl.java b/src/main/java/com/loopon/challenge/infrastructure/ChallengeRepositoryImpl.java index 5f3f9239..d6b0a51f 100644 --- a/src/main/java/com/loopon/challenge/infrastructure/ChallengeRepositoryImpl.java +++ b/src/main/java/com/loopon/challenge/infrastructure/ChallengeRepositoryImpl.java @@ -1,9 +1,7 @@ package com.loopon.challenge.infrastructure; -import com.loopon.challenge.application.dto.response.ChallengePreviewResponse; import com.loopon.challenge.domain.Challenge; import com.loopon.challenge.domain.ChallengeHashtag; -import com.loopon.challenge.domain.ChallengeHashtagId; import com.loopon.challenge.domain.ChallengeImage; import com.loopon.challenge.domain.ChallengeLike; import com.loopon.challenge.domain.Comment; @@ -56,11 +54,6 @@ public Long save(Challenge challenge) { return challengeJpaRepository.save(challenge).getId(); } - @Override - public ChallengeHashtagId saveChallengeHashtag(ChallengeHashtag challengeHashtag) { - return challengeHashtagJpaRepository.save(challengeHashtag).getId(); - } - @Override public Long saveChallengeImage(ChallengeImage challengeImage) { return challengeImageJpaRepository.save(challengeImage).getId(); @@ -77,10 +70,6 @@ public Optional findHashtagByName(String name) { return hashtagJpaRepository.findByName(name); } - @Override - public List findAllChallengeHashtagByChallengeId(Long challengeId) { - return challengeHashtagJpaRepository.findAllByChallengeId(challengeId); - } @Override public List findAllChallengeHashtagWithHashtagByChallengeId(Long challengeId) { @@ -92,11 +81,6 @@ public List findAllImageByChallengeId(Long challengeId) { return challengeImageJpaRepository.findAllByChallengeId(challengeId); } - @Override - public void deleteChallengeHashtag(ChallengeHashtag challengeHashtag) { - challengeHashtagJpaRepository.delete(challengeHashtag); - } - @Override public void deleteAllByExpeditionId(Long expeditionId) { challengeJpaRepository.deleteAllByExpeditionId(expeditionId); @@ -116,16 +100,6 @@ public Boolean existsChallengeLikeByIdAndUserId(Long challengeId, Long userId) { public Page findThumbnailsByUserId(Long userId, Pageable pageable) { return challengeImageJpaRepository.findThumbnailsByUserId(userId, pageable); } - - @Override - public List findAllHashtagByNameIn(List hashtagList) { - return hashtagJpaRepository.findAllByNameIn(hashtagList); - } - - @Override - public void saveAllHashtags(List hashtagList) { - hashtagJpaRepository.saveAll(hashtagList); - } @Override public void deleteChallengeLikeById(Long challengeLikeId) { @@ -183,19 +157,14 @@ public void delete(Challenge challenge) { challengeJpaRepository.delete(challenge); } - @Override - public Slice findViewByUserId(Long userId, Pageable pageable) { - return challengeJpaRepository.findViewByUserId(userId, pageable); - } - @Override public List findAllCommentWithUserByParentIdIn(List parentIds) { return commentJpaRepository.findAllWithUserByParentIdIn(parentIds); } @Override - public Slice findTrendingChallenges(LocalDateTime threeDaysAgo, Pageable pageable) { - return challengeJpaRepository.findTrendingChallenges(threeDaysAgo, pageable); + public Slice findTrendingChallenges(LocalDateTime threeDaysAgo, Long userId, Pageable pageable) { + return challengeJpaRepository.findTrendingChallenges(threeDaysAgo, userId, pageable); } @Override @@ -222,4 +191,9 @@ public Boolean existsCommentLikeByCommentIdAndUserId(Long commentId, Long userId public Slice findAllWithJourneyAndUserByUserId(Long userId, Pageable pageable) { return challengeJpaRepository.findAllWithJourneyAndUserByUserId(userId, pageable); } + + @Override + public List findAllCommentLikeByUserIdAndCommentIdIn(Long userId, List commentIds) { + return commentLikeJpaRepository.findAllByUserIdAndCommentIdIn(userId, commentIds); + } } diff --git a/src/main/java/com/loopon/challenge/infrastructure/jpa/ChallengeHashtagJpaRepository.java b/src/main/java/com/loopon/challenge/infrastructure/jpa/ChallengeHashtagJpaRepository.java index faf9c1cc..15274900 100644 --- a/src/main/java/com/loopon/challenge/infrastructure/jpa/ChallengeHashtagJpaRepository.java +++ b/src/main/java/com/loopon/challenge/infrastructure/jpa/ChallengeHashtagJpaRepository.java @@ -7,7 +7,5 @@ import java.util.List; public interface ChallengeHashtagJpaRepository extends JpaRepository { - List findAllByChallengeId(Long challengeId); - List findAllWithHashtagByChallengeId(Long challengeId); } diff --git a/src/main/java/com/loopon/challenge/infrastructure/jpa/ChallengeJpaRepository.java b/src/main/java/com/loopon/challenge/infrastructure/jpa/ChallengeJpaRepository.java index 9b3158e7..c8f62489 100644 --- a/src/main/java/com/loopon/challenge/infrastructure/jpa/ChallengeJpaRepository.java +++ b/src/main/java/com/loopon/challenge/infrastructure/jpa/ChallengeJpaRepository.java @@ -1,6 +1,5 @@ package com.loopon.challenge.infrastructure.jpa; -import com.loopon.challenge.application.dto.response.ChallengePreviewResponse; import com.loopon.challenge.domain.Challenge; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -26,21 +25,17 @@ Slice findAllWithJourneyAndUserByExpeditionId( Pageable pageable ); - @Query("SELECT new com.loopon.challenge.application.dto.response.ChallengePreviewResponse(c.id, ci.imageUrl) " + - "FROM Challenge c " + - "JOIN c.challengeImages ci " + - "WHERE c.user.id = :userId " + - "AND ci.displayOrder = 0") - Slice findViewByUserId(@Param("userId") Long userId, Pageable pageable); @Query("SELECT c FROM Challenge c " + "JOIN FETCH c.user u " + "JOIN FETCH c.journey j " + "WHERE c.createdAt >= :threeDaysAgo " + "AND c.user.visibility = 'PUBLIC' " + + "AND c.user.id != :userId " + "ORDER BY (c.likeCount * 2 + c.commentCount * 5) DESC, c.createdAt DESC") Slice findTrendingChallenges( @Param("threeDaysAgo") LocalDateTime threeDaysAgo, + @Param("userId") Long userId, Pageable pageable ); diff --git a/src/main/java/com/loopon/challenge/infrastructure/jpa/CommentLikeJpaRepository.java b/src/main/java/com/loopon/challenge/infrastructure/jpa/CommentLikeJpaRepository.java index c6d792fd..cccaefc5 100644 --- a/src/main/java/com/loopon/challenge/infrastructure/jpa/CommentLikeJpaRepository.java +++ b/src/main/java/com/loopon/challenge/infrastructure/jpa/CommentLikeJpaRepository.java @@ -3,10 +3,13 @@ import com.loopon.challenge.domain.CommentLike; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface CommentLikeJpaRepository extends JpaRepository { Optional findByCommentIdAndUserId(Long commentId, Long userId); Boolean existsByIdAndUserId(Long commentId, Long userId); + + List findAllByUserIdAndCommentIdIn(Long userId, List commentIds); } diff --git a/src/main/java/com/loopon/challenge/infrastructure/jpa/HashtagJpaRepository.java b/src/main/java/com/loopon/challenge/infrastructure/jpa/HashtagJpaRepository.java index 553cea71..e94aa63f 100644 --- a/src/main/java/com/loopon/challenge/infrastructure/jpa/HashtagJpaRepository.java +++ b/src/main/java/com/loopon/challenge/infrastructure/jpa/HashtagJpaRepository.java @@ -3,11 +3,8 @@ import com.loopon.challenge.domain.Hashtag; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; import java.util.Optional; public interface HashtagJpaRepository extends JpaRepository { Optional findByName(String hashtag); - - List findAllByNameIn(List hashtagList); } diff --git a/src/main/java/com/loopon/challenge/presentation/ChallengeApiController.java b/src/main/java/com/loopon/challenge/presentation/ChallengeApiController.java index c004d05e..f1d99164 100644 --- a/src/main/java/com/loopon/challenge/presentation/ChallengeApiController.java +++ b/src/main/java/com/loopon/challenge/presentation/ChallengeApiController.java @@ -99,9 +99,10 @@ public ResponseEntity> commentChallenge @GetMapping("/api/challenges/{challengeId}/comments") public ResponseEntity>> getCommentChallenge( @PathVariable Long challengeId, + @AuthenticationPrincipal PrincipalDetails principalDetails, Pageable pageable ) { - ChallengeGetCommentCommand commandDto = ChallengeConverter.getCommentChallenge(challengeId, pageable); + ChallengeGetCommentCommand commandDto = ChallengeConverter.getCommentChallenge(challengeId, principalDetails.getUserId(), pageable); return ResponseEntity.ok(CommonResponse.onSuccess(challengeQueryService.getCommentChallenge(commandDto))); } @@ -137,27 +138,6 @@ public ResponseEntity> deleteChallenge( return ResponseEntity.ok(CommonResponse.onSuccess(challengeCommandService.deleteChallenge(commandDto))); } - @Override - @GetMapping("/api/challenges/users/me") - public ResponseEntity>> myChallenge( - @AuthenticationPrincipal PrincipalDetails principalDetails, - Pageable pageable - ) { - ChallengeMyCommand commandDto = ChallengeConverter.myChallenge(principalDetails.getUserId(), pageable); - return ResponseEntity.ok(CommonResponse.onSuccess(challengeQueryService.myChallenge(commandDto))); - } - - @Override - @GetMapping("/api/challenges/users/{nickname}") - public ResponseEntity>> othersChallenge( - @PathVariable String nickname, - Pageable pageable, - @AuthenticationPrincipal PrincipalDetails principalDetails - ) { - ChallengeOthersCommand commandDto = ChallengeConverter.othersChallenge(principalDetails.getUserId(), nickname, pageable); - return ResponseEntity.ok(CommonResponse.onSuccess(challengeQueryService.othersChallenge(commandDto))); - } - @Override @GetMapping("/api/challenges") public ResponseEntity> viewChallenge( diff --git a/src/main/java/com/loopon/challenge/presentation/docs/ChallengeApiDocs.java b/src/main/java/com/loopon/challenge/presentation/docs/ChallengeApiDocs.java index 6c541f65..ef693693 100644 --- a/src/main/java/com/loopon/challenge/presentation/docs/ChallengeApiDocs.java +++ b/src/main/java/com/loopon/challenge/presentation/docs/ChallengeApiDocs.java @@ -20,6 +20,7 @@ import org.springframework.data.web.PageableDefault; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; @@ -82,6 +83,7 @@ ResponseEntity> commentChallenge( @Operation(summary = "챌린지 댓글 목록 조회") ResponseEntity>> getCommentChallenge( @PathVariable("challengeId") Long challengeId, + @AuthenticationPrincipal PrincipalDetails principalDetails, @PageableDefault Pageable pageable ); @@ -105,19 +107,6 @@ ResponseEntity> deleteChallenge( PrincipalDetails principalDetails ); - @Operation(summary = "내 챌린지 모아보기") - ResponseEntity>> myChallenge( - PrincipalDetails principalDetails, - @PageableDefault Pageable pageable - ); - - @Operation(summary = "타인의 챌린지 모아보기") - ResponseEntity>> othersChallenge( - @PathVariable("nickname") String nickname, - @PageableDefault Pageable pageable, - PrincipalDetails principalDetails - ); - @Operation(summary = "여정광장 챌린지 조회.", description = "트렌딩 챌린지와 친구 챌린지의 비율은 기본적으로 1:3을 유지합니다.") ResponseEntity> viewChallenge( PrincipalDetails principalDetails, diff --git a/src/main/java/com/loopon/expedition/application/converter/ExpeditionConverter.java b/src/main/java/com/loopon/expedition/application/converter/ExpeditionConverter.java index 7b8adff0..544576f3 100644 --- a/src/main/java/com/loopon/expedition/application/converter/ExpeditionConverter.java +++ b/src/main/java/com/loopon/expedition/application/converter/ExpeditionConverter.java @@ -231,12 +231,15 @@ public static ExpeditionChallengesResponse challengesExpedition( ) { return ExpeditionChallengesResponse.builder() .challengeId(challenge.getId()) - .journeyNumber(challenge.getJourney().getJourneyOrder()) + .journeyNumber(challenge.getJourney().getJourneyOrder()) // 여정 번호 필요! .imageUrls(imageUrls) .content(challenge.getContent()) .hashtags(hashtags) .createdAt(challenge.getCreatedAt()) + .nickname(challenge.getUser().getNickname()) + .profileImageUrl(challenge.getUser().getProfileImageUrl()) .isLiked(isLiked) + .likeCount(challenge.getLikeCount()) .build(); } diff --git a/src/main/java/com/loopon/expedition/application/dto/response/ExpeditionChallengesResponse.java b/src/main/java/com/loopon/expedition/application/dto/response/ExpeditionChallengesResponse.java index 1394158c..bd43350f 100644 --- a/src/main/java/com/loopon/expedition/application/dto/response/ExpeditionChallengesResponse.java +++ b/src/main/java/com/loopon/expedition/application/dto/response/ExpeditionChallengesResponse.java @@ -13,8 +13,9 @@ public record ExpeditionChallengesResponse( String content, List hashtags, LocalDateTime createdAt, - String nickName, + String nickname, String profileImageUrl, - Boolean isLiked + Boolean isLiked, + Integer likeCount ) { -} +} \ No newline at end of file diff --git a/src/main/java/com/loopon/user/application/UserQueryService.java b/src/main/java/com/loopon/user/application/UserQueryService.java index 1d10112a..410c4491 100644 --- a/src/main/java/com/loopon/user/application/UserQueryService.java +++ b/src/main/java/com/loopon/user/application/UserQueryService.java @@ -7,8 +7,11 @@ import com.loopon.global.domain.dto.PageResponse; import com.loopon.global.exception.BusinessException; import com.loopon.user.application.dto.response.UserDuplicateCheckResponse; +import com.loopon.user.application.dto.response.UserOthersProfileResponse; import com.loopon.user.application.dto.response.UserProfileResponse; +import com.loopon.user.domain.FriendStatus; import com.loopon.user.domain.User; +import com.loopon.user.domain.repository.FriendRepository; import com.loopon.user.domain.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -22,6 +25,7 @@ public class UserQueryService { private final UserRepository userRepository; private final ChallengeRepository challengeRepository; + private final FriendRepository friendRepository; public UserDuplicateCheckResponse isEmailAvailable(String email) { boolean isAvailable = !userRepository.existsByEmail(email); @@ -45,4 +49,22 @@ public UserProfileResponse getUserProfile(Long userId, Pageable pageable) { return UserProfileResponse.of(user, pageResponse); } + + public UserOthersProfileResponse getOthersProfile(Long userId, String nickname, Pageable pageable) { + User me = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + User target = userRepository.findByNickname(nickname) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + Boolean isFriend = friendRepository.existsFriendship(me.getId(), target.getId(), FriendStatus.ACCEPTED); + + Page imagePage = challengeRepository.findThumbnailsByUserId(userId, pageable); + + Page dtoPage = imagePage.map(ChallengeThumbnailResponse::from); + + PageResponse pageResponse = PageResponse.from(dtoPage); + + return UserOthersProfileResponse.of(target, isFriend, pageResponse); + } } diff --git a/src/main/java/com/loopon/user/application/dto/response/UserOthersProfileResponse.java b/src/main/java/com/loopon/user/application/dto/response/UserOthersProfileResponse.java new file mode 100644 index 00000000..b559cf5f --- /dev/null +++ b/src/main/java/com/loopon/user/application/dto/response/UserOthersProfileResponse.java @@ -0,0 +1,35 @@ +package com.loopon.user.application.dto.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.loopon.challenge.application.dto.response.ChallengeThumbnailResponse; +import com.loopon.global.domain.dto.PageResponse; +import com.loopon.user.domain.User; + +public record UserOthersProfileResponse( + Long userId, + String nickname, + String bio, + String statusMessage, + String profileImageUrl, + Boolean isFriend, + + @JsonInclude(JsonInclude.Include.NON_NULL) + PageResponse thumbnailResponse +) { + public static UserOthersProfileResponse of( + User user, + Boolean isFriend, + PageResponse challenges + ) { + + return new UserOthersProfileResponse( + user.getId(), + user.getNickname(), + user.getBio(), + user.getStatusMessage(), + user.getProfileImageUrl(), + isFriend, + challenges + ); + } +} diff --git a/src/main/java/com/loopon/user/presentation/UserApiController.java b/src/main/java/com/loopon/user/presentation/UserApiController.java index 6d6aa583..cb773c06 100644 --- a/src/main/java/com/loopon/user/presentation/UserApiController.java +++ b/src/main/java/com/loopon/user/presentation/UserApiController.java @@ -9,6 +9,7 @@ import com.loopon.user.application.dto.request.UpdateProfileRequest; import com.loopon.user.application.dto.request.UserSignUpRequest; import com.loopon.user.application.dto.response.UserDuplicateCheckResponse; +import com.loopon.user.application.dto.response.UserOthersProfileResponse; import com.loopon.user.application.dto.response.UserProfileResponse; import com.loopon.user.application.validator.ImageValidator; import com.loopon.user.presentation.docs.UserApiDocs; @@ -20,14 +21,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @RestController @@ -80,6 +74,18 @@ public ResponseEntity> getUserProfile( return ResponseEntity.ok(CommonResponse.onSuccess(response)); } + @Override + @GetMapping("/{nickname}") + public ResponseEntity> getOthersProfile( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable String nickname, + @PageableDefault(size = 20, sort = "id", direction = Sort.Direction.DESC) + Pageable pageable + ) { + UserOthersProfileResponse response = userQueryService.getOthersProfile(principalDetails.getUserId(), nickname, pageable); + return ResponseEntity.ok(CommonResponse.onSuccess(response)); + } + @Override @PatchMapping("/profile") public ResponseEntity> updateProfile( diff --git a/src/main/java/com/loopon/user/presentation/docs/UserApiDocs.java b/src/main/java/com/loopon/user/presentation/docs/UserApiDocs.java index e1d334b9..ab2788d9 100644 --- a/src/main/java/com/loopon/user/presentation/docs/UserApiDocs.java +++ b/src/main/java/com/loopon/user/presentation/docs/UserApiDocs.java @@ -8,6 +8,7 @@ import com.loopon.user.application.dto.request.UpdateProfileRequest; import com.loopon.user.application.dto.request.UserSignUpRequest; import com.loopon.user.application.dto.response.UserDuplicateCheckResponse; +import com.loopon.user.application.dto.response.UserOthersProfileResponse; import com.loopon.user.application.dto.response.UserProfileResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -76,6 +77,21 @@ ResponseEntity> getUserProfile( @Parameter(hidden = true) Pageable pageable ); + @Operation(summary = "타인 프로필 조회", description = "공개/친구 사용자의 프로필 정보(닉네임, 이미지, 한줄 소개 등)를 조회합니다.") + @Parameters({ + @Parameter(name = "page", description = "페이지 번호 (0부터 시작)", in = ParameterIn.QUERY, example = "0"), + @Parameter(name = "size", description = "한 페이지 크기", in = ParameterIn.QUERY, example = "10"), + @Parameter(name = "sort", description = "정렬 기준 (예: createdAt,desc)", in = ParameterIn.QUERY, example = "createdAt,desc") + }) + @ApiResponse(responseCode = "200", description = "조회 성공", useReturnTypeSchema = true) + @CommonBadRequestResponseDocs + @CommonInternalServerErrorResponseDocs + ResponseEntity> getOthersProfile( + @Parameter(hidden = true) PrincipalDetails principalDetails, + @Parameter String nickname, + @Parameter(hidden = true) Pageable pageable + ); + @Operation(summary = "프로필 수정", description = "닉네임, Bio, 상태메시지, 프로필 이미지를 수정합니다.") @ApiResponse(responseCode = "200", description = "수정 성공 (변경된 프로필 정보 반환)", useReturnTypeSchema = true) @CommonBadRequestResponseDocs diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index ee2b6b34..c4a2f52e 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -3,12 +3,24 @@ spring: activate: on-profile: dev + datasource: + hikari: + maximum-pool-size: 15 + connection-timeout: 3000 + idle-timeout: 600000 + max-lifetime: 1800000 + # JPA jpa: hibernate: ddl-auto: validate show-sql: false +server: + tomcat: + threads: + max: 200 + # 로그 레벨 logging: level: diff --git a/src/test/java/com/loopon/challenge/application/ChallengeQueryServiceTest.java b/src/test/java/com/loopon/challenge/application/ChallengeQueryServiceTest.java index 8112c980..f213fb39 100644 --- a/src/test/java/com/loopon/challenge/application/ChallengeQueryServiceTest.java +++ b/src/test/java/com/loopon/challenge/application/ChallengeQueryServiceTest.java @@ -130,7 +130,7 @@ class GetCommentChallengeTest { @DisplayName("성공: 부모 댓글과 그에 달린 대댓글들이 맵핑되어 반환된다.") void success() { // given - ChallengeGetCommentCommand command = new ChallengeGetCommentCommand(1L, PageRequest.of(0, 10)); + ChallengeGetCommentCommand command = new ChallengeGetCommentCommand(1L, 1L, PageRequest.of(0, 10)); Challenge challenge = createTestChallenge(1L); User parentUser = createTestUser(1L, "parent", UserVisibility.PUBLIC); @@ -163,58 +163,6 @@ void success() { } } - @Nested - @DisplayName("타인 챌린지 조회 (othersChallenge)") - class OthersChallengeTest { - - @Test - @DisplayName("성공: 비공개 계정이라도 친구 상태가 ACCEPTED라면 조회가 가능하다.") - void success_private_friend() { - // 1. Given - Long myId = 1L; - Long targetUserId = 2L; - - // 유저 생성 시 ID 확실히 주입 (createTestUser 메서드가 mockUser.getId() -> id 를 리턴하게 되어있어야 함) - User myself = createTestUser(myId, "me", UserVisibility.PUBLIC); - User target = createTestUser(targetUserId, "target", UserVisibility.PRIVATE); - - ChallengeOthersCommand command = new ChallengeOthersCommand(myId, "target", PageRequest.of(0, 10)); - - given(userRepository.findById(myId)).willReturn(Optional.of(myself)); - given(userRepository.findByNickname("target")).willReturn(Optional.of(target)); - - given(friendRepository.existsFriendship(eq(targetUserId), eq(myId), eq(FriendStatus.ACCEPTED))) - .willReturn(true); - - given(challengeRepository.findViewByUserId(anyLong(), any(Pageable.class))) - .willReturn(new SliceImpl<>(List.of())); - - // 2. When - challengeQueryService.othersChallenge(command); - - // 3. Then - verify(challengeRepository).findViewByUserId(eq(targetUserId), any(Pageable.class)); - } - - @Test - @DisplayName("실패: 비공개 계정인데 친구가 아니면 CHALLENGE_FORBIDDEN 예외 발생") - void fail_private_not_friend() { - // given - User myself = createTestUser(1L, "me", UserVisibility.PUBLIC); - User target = createTestUser(2L, "target", UserVisibility.PRIVATE); - ChallengeOthersCommand command = new ChallengeOthersCommand(1L, "target", PageRequest.of(0, 10)); - - given(userRepository.findById(1L)).willReturn(Optional.of(myself)); - given(userRepository.findByNickname("target")).willReturn(Optional.of(target)); - given(friendRepository.existsFriendship(2L, 1L, FriendStatus.ACCEPTED)).willReturn(false); - - // when & then - assertThatThrownBy(() -> challengeQueryService.othersChallenge(command)) - .isInstanceOf(BusinessException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCode.CHALLENGE_FORBIDDEN); - } - } - @Nested @DisplayName("메인 피드 조회 (viewChallenge)") class ViewChallengeTest { @@ -234,7 +182,7 @@ void success_combined_view() { when(friend.getId()).thenReturn(200L); given(userRepository.findById(1L)).willReturn(Optional.of(user)); - given(challengeRepository.findTrendingChallenges(any(LocalDateTime.class), any(Pageable.class))) + given(challengeRepository.findTrendingChallenges(any(LocalDateTime.class), anyLong(), any(Pageable.class))) .willReturn(new SliceImpl<>(List.of(trending))); // 친구 목록 mock @@ -269,54 +217,6 @@ void fail_user_not_found() { } } - @Nested - @DisplayName("내 챌린지 조회 (myChallenge)") - class MyChallengeTest { - - @Test - @DisplayName("성공: 유저가 존재하면 해당 유저의 챌린지 목록을 Slice로 반환한다.") - void success() { - // given - Long userId = 1L; - Pageable pageable = PageRequest.of(0, 10); - ChallengeMyCommand command = new ChallengeMyCommand(userId, pageable); - - User user = createTestUser(userId, "me", UserVisibility.PUBLIC); - - // 가짜 결과물(Slice) 생성 - ChallengePreviewResponse preview = new ChallengePreviewResponse(100L, "thumb.jpg"); // 필드 구성은 DTO에 맞게 조정 - Slice expectedSlice = new SliceImpl<>(List.of(preview), pageable, false); - - given(userRepository.findById(userId)).willReturn(Optional.of(user)); - given(challengeRepository.findViewByUserId(userId, pageable)).willReturn(expectedSlice); - - // when - SliceResponse result = challengeQueryService.myChallenge(command); - - // then - assertThat(result).isNotNull(); - assertThat(result.content()).hasSize(1); - assertThat(result.content().getFirst().challengeId()).isEqualTo(100L); - - // 리포지토리가 정확한 userId로 조회했는지 검증 - verify(challengeRepository).findViewByUserId(eq(userId), eq(pageable)); - } - - @Test - @DisplayName("실패: 존재하지 않는 유저 ID로 요청 시 NOT_FOUND 예외가 발생한다.") - void fail_user_not_found() { - // given - Long userId = 999L; - ChallengeMyCommand command = new ChallengeMyCommand(userId, PageRequest.of(0, 10)); - - given(userRepository.findById(userId)).willReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> challengeQueryService.myChallenge(command)) - .isInstanceOf(BusinessException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCode.NOT_FOUND); - } - } @Test @DisplayName("챌린지 상세 목록 조회 성공 - 연관 엔티티 포함") diff --git a/src/test/java/com/loopon/expedition/application/ExpeditionQueryServiceTest.java b/src/test/java/com/loopon/expedition/application/ExpeditionQueryServiceTest.java index 87ea960b..4528b771 100644 --- a/src/test/java/com/loopon/expedition/application/ExpeditionQueryServiceTest.java +++ b/src/test/java/com/loopon/expedition/application/ExpeditionQueryServiceTest.java @@ -246,6 +246,7 @@ void success() { Challenge challenge = mock(Challenge.class); lenient().when(challenge.getId()).thenReturn(challengeId); lenient().when(challenge.getJourney()).thenReturn(mock(Journey.class)); + lenient().when(challenge.getUser()).thenReturn(user); // 이미지 모킹 ChallengeImage img = mock(ChallengeImage.class);