diff --git a/src/main/java/com/hrr/backend/domain/user/controller/UserController.java b/src/main/java/com/hrr/backend/domain/user/controller/UserController.java index 6cee693e..a2de66f0 100644 --- a/src/main/java/com/hrr/backend/domain/user/controller/UserController.java +++ b/src/main/java/com/hrr/backend/domain/user/controller/UserController.java @@ -163,6 +163,7 @@ public ApiResponse> searchChallenge return ApiResponse.onSuccess(SuccessCode.OK, response); } + // 사용자 기본 정보 수정 @PatchMapping("/me") @@ -245,11 +246,11 @@ public ApiResponse> getVeri ) { Long userId = customUserDetails.getUser().getId(); SliceResponseDto response = - verificationService.getVerificationHistory(userId, page-1, size); + verificationService.getVerificationHistory(userId, page - 1, size); return ApiResponse.onSuccess(SuccessCode.OK, response); } - + @GetMapping("/{userId}/verifications/history") @Operation( summary = "다른 사용자 인증 기록 조회", @@ -274,4 +275,53 @@ public ApiResponse getOtherUse return ApiResponse.onSuccess(SuccessCode.OK, response); } + + @GetMapping("/challenges/liked") + @Operation( + summary = "찜한 챌린지 목록 조회", + description = "현재 로그인한 사용자가 찜한 챌린지 목록을 조회합니다." + ) + public ApiResponse> getMarkedChallenges( + @Parameter(hidden = true) + @AuthenticationPrincipal CustomUserDetails customUserDetails, + + @RequestParam(name = "page", defaultValue = "1") + @Min(1) + @Parameter(description = "페이지 번호 (1부터 시작)", example = "1") int page, + + @RequestParam(name = "size", defaultValue = "10") + @Min(1) @Max(100) + @Parameter(description = "페이지당 데이터 개수", example = "10") int size + ) { + Long userId = customUserDetails.getUser().getId(); + SliceResponseDto response = + userService.getMarkedChallenges(userId, page - 1, size); + + return ApiResponse.onSuccess(SuccessCode.OK, response); + } + + @GetMapping("/challenges/completed") + @Operation( + summary = "종료한 챌린지 목록 조회", + description = "현재 로그인한 사용자가 참여했던 종료된 챌린지 목록을 조회합니다." + ) + public ApiResponse> getCompletedChallenges( + @Parameter(hidden = true) + @AuthenticationPrincipal CustomUserDetails customUserDetails, + + @RequestParam(name = "page", defaultValue = "1") + @Min(1) + @Parameter(description = "페이지 번호 (1부터 시작)", example = "1") int page, + + @RequestParam(name = "size", defaultValue = "10") + @Min(1) @Max(100) + @Parameter(description = "페이지당 데이터 개수", example = "10") int size + ) { + Long userId = customUserDetails.getUser().getId(); + SliceResponseDto response = + userService.getCompletedChallenges(userId, page - 1, size); + + return ApiResponse.onSuccess(SuccessCode.OK, response); + } } + diff --git a/src/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.java b/src/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.java index a65bcf00..6e7ebd18 100644 --- a/src/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.java +++ b/src/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.java @@ -159,4 +159,48 @@ public static MyInfoDto from(User user) { .build(); } } + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "찜한 챌린지 정보 DTO") + public static class LikedChallengeDto { + + @Schema(description = "챌린지 아이디", example = "301") + private Long challengeId; + + @Schema(description = "챌린지 제목", example = "자잘자잘") + private String title; + + @Schema(description = "챌린지 간단 설명", example = "하루 5분씩 무엇이든 꼭 해야...") + private String description; + + @JsonProperty("image") + @Schema(description = "챌린지 대표 이미지 URL", example = "http://example.com/challenge_301.jpg") + private String image; + } + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "종료한 챌린지 정보 DTO") + public static class CompletedChallengeDto { + + @Schema(description = "챌린지 아이디", example = "301") + private Long challengeId; + + @Schema(description = "챌린지 제목", example = "자잘자잘") + private String title; + + @Schema(description = "챌린지 간단 설명", example = "하루 5분씩 무엇이든 꼭 해야...") + private String description; + + @JsonProperty("image") + @Schema(description = "챌린지 대표 이미지 URL", example = "http://example.com/challenge_301.jpg") + private String image; + } } diff --git a/src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustom.java b/src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustom.java index 07269026..5fcbd1e2 100644 --- a/src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustom.java +++ b/src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustom.java @@ -14,4 +14,20 @@ public interface UserChallengeRepositoryCustom { * @return 참가중인 챌린지 정보 Slice */ Slice findOngoingChallengesByUser(User user, Pageable pageable); + + /** + * 사용자가 찜한 챌린지 목록 조회 (페이징) + * @param user 조회 대상 사용자 + * @param pageable 페이징 정보 + * @return 찜한 챌린지 정보 Slice + */ + Slice findMarkedChallengesByUser(User user, Pageable pageable); + + /** + * 사용자가 종료한 챌린지 목록 조회 (페이징) + * @param user 조회 대상 사용자 + * @param pageable 페이징 정보 + * @return 종료한 챌린지 정보 Slice + */ + Slice findCompletedChallengesByUser(User user, Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java b/src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java index cd846718..d7823940 100644 --- a/src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java +++ b/src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java @@ -1,6 +1,7 @@ package com.hrr.backend.domain.user.repository; import com.hrr.backend.domain.challenge.entity.QChallenge; +import com.hrr.backend.domain.challenge.entity.QChallengeLike; import com.hrr.backend.domain.round.entity.QRound; import com.hrr.backend.domain.round.entity.QRoundRecord; import com.hrr.backend.domain.user.dto.UserResponseDto; @@ -65,4 +66,70 @@ public Slice findOngoingChallengesByUser(Us return new SliceImpl<>(content, pageable, hasNext); } + + @Override + public Slice findMarkedChallengesByUser(User user, Pageable pageable) { + QChallengeLike qChallengeLike = QChallengeLike.challengeLike; + QChallenge qChallenge = QChallenge.challenge; + + // 찜한 챌린지 정보 조회 + List content = jpaQueryFactory + .select(Projections.fields(UserResponseDto.LikedChallengeDto.class, + qChallenge.id.as("challengeId"), + qChallenge.title, + qChallenge.description, + qChallenge.imageKey.as("image") + )) + .from(qChallengeLike) + .join(qChallengeLike.challenge, qChallenge) + .where(qChallengeLike.user.eq(user)) + .orderBy(qChallengeLike.createdAt.desc()) // 찜한 순서 (최신순) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) + .fetch(); + + // Slice 객체 생성 및 반환 + boolean hasNext = content.size() > pageable.getPageSize(); + if (hasNext) { + content.remove(pageable.getPageSize()); + } + + return new SliceImpl<>(content, pageable, hasNext); + } + + @Override + public Slice findCompletedChallengesByUser(User user, Pageable pageable) { + QUserChallenge qUserChallenge = QUserChallenge.userChallenge; + QChallenge qChallenge = QChallenge.challenge; + + // 종료한 챌린지 정보 조회 + // UserChallenge status는 완주한(JOINED) 챌린지만 그룹화하여 조회 + // 한 챌린지에 여러 라운드 참여해도 챌린지는 1개만 조회 + List content = jpaQueryFactory + .select(Projections.fields(UserResponseDto.CompletedChallengeDto.class, + qChallenge.id.as("challengeId"), + qChallenge.title, + qChallenge.description, + qChallenge.imageKey.as("image") + )) + .from(qUserChallenge) + .join(qUserChallenge.challenge, qChallenge) + .where( + qUserChallenge.user.eq(user), + qUserChallenge.status.eq(ChallengeJoinStatus.DROPPED) + ) + .groupBy(qChallenge.id) + .orderBy(qUserChallenge.updatedAt.max().desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) + .fetch(); + + // Slice 객체 생성 및 반환 + boolean hasNext = content.size() > pageable.getPageSize(); + if (hasNext) { + content.remove(pageable.getPageSize()); + } + + return new SliceImpl<>(content, pageable, hasNext); + } } diff --git a/src/main/java/com/hrr/backend/domain/user/service/UserService.java b/src/main/java/com/hrr/backend/domain/user/service/UserService.java index e0c49678..5e0c7e7f 100644 --- a/src/main/java/com/hrr/backend/domain/user/service/UserService.java +++ b/src/main/java/com/hrr/backend/domain/user/service/UserService.java @@ -18,6 +18,7 @@ SliceResponseDto getOngoingChallenges( int page, int size ); + // 내 정보 조회 UserResponseDto.MyInfoDto getMyInfo(Long userId); @@ -31,16 +32,34 @@ SliceResponseDto getOngoingChallenges( */ UserNicknameResponseDto setNickname(User user, UserNicknameRequestDto request); + /** + * 키워드가 닉네임에 포함된 사용자 조회 + */ + SliceResponseDto searchChallengers( + User user, + String keyword, + int page, + int size + ); + /** - * 키워드가 닉네임에 포함된 사용자 조회 + * 찜한 챌린지 목록 조회 (페이징) */ - SliceResponseDto searchChallengers( - User user, - String keyword, + SliceResponseDto getMarkedChallenges( + Long userId, int page, int size ); + /** + * 종료한 챌린지 목록 조회 (페이징) + */ + SliceResponseDto getCompletedChallenges( + Long userId, + int page, + int size + ); + /** * 사용자 기본 정보 수정 */ diff --git a/src/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java b/src/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java index be719e41..953b422c 100644 --- a/src/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java @@ -93,7 +93,7 @@ public SliceResponseDto getOngoingChallenge slice.getContent().forEach(dto -> dto.setThumbnailUrl(s3UrlUtil.toFullUrl(dto.getThumbnailUrl())) ); - + // 인증 완료 여부 추가 slice.getContent().forEach(dto -> { Long challengeId = dto.getChallengeId(); @@ -288,6 +288,60 @@ private LocalDate findLastestDateIncludingToday(Challenge challenge) { return null; } + // 찜한 챌린지 목록 조회 + @Override + public SliceResponseDto getMarkedChallenges( + Long userId, + int page, + int size + ) { + // 사용자 조회 + User user = userRepository.findById(userId) + .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND)); + + // Pageable 객체 생성 + Pageable pageable = PageRequest.of(page, size); + + // Repository에서 찜한 챌린지 조회 + Slice slice = + userChallengeRepository.findMarkedChallengesByUser(user, pageable); + + // S3 URL 변환 + slice.getContent().forEach(dto -> + dto.setImage(s3UrlUtil.toFullUrl(dto.getImage())) + ); + + // SliceResponseDto로 변환하여 반환 + return new SliceResponseDto<>(slice); + } + + // 종료한 챌린지 목록 조회 + @Override + public SliceResponseDto getCompletedChallenges( + Long userId, + int page, + int size + ) { + // 사용자 조회 + User user = userRepository.findById(userId) + .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND)); + + // Pageable 객체 생성 + Pageable pageable = PageRequest.of(page, size); + + // Repository에서 종료한 챌린지 조회 + Slice slice = + userChallengeRepository.findCompletedChallengesByUser(user, pageable); + + // S3 URL 변환 + slice.getContent().forEach(dto -> + dto.setImage(s3UrlUtil.toFullUrl(dto.getImage())) + ); + + // SliceResponseDto로 변환하여 반환 + return new SliceResponseDto<>(slice); + } + // 내 정보 수정 @Override @Transactional