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 3a081302..b3f5b793 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 @@ -3,12 +3,13 @@ import com.hrr.backend.domain.user.dto.*; import com.hrr.backend.domain.user.entity.User; import com.hrr.backend.domain.user.service.UserService; +import com.hrr.backend.domain.verification.dto.VerificationResponseDto; +import com.hrr.backend.domain.verification.service.VerificationService; import com.hrr.backend.global.config.CustomUserDetails; import com.hrr.backend.global.response.ApiResponse; import com.hrr.backend.global.response.SliceResponseDto; import com.hrr.backend.global.response.SuccessCode; -import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -34,6 +35,7 @@ public class UserController { private final UserService userService; + private final VerificationService verificationService; // 닉네임 유효성 검사 API @@ -140,37 +142,96 @@ public ApiResponse getMyInfo( return ApiResponse.onSuccess(SuccessCode.OK, myInfo); } - @GetMapping("/search") - @Operation(summary = "챌린저 검색", description = "검색 키워드가 닉네임에 포함된 사용자를 조회합니다.") - public ApiResponse> searchChallengers( - @RequestParam(name = "keyword") - @NotBlank(message = "검색어는 필수입니다.") String keyword, + @GetMapping("/search") + @Operation(summary = "챌린저 검색", description = "검색 키워드가 닉네임에 포함된 사용자를 조회합니다.") + public ApiResponse> searchChallengers( + @RequestParam(name = "keyword") + @NotBlank(message = "검색어는 필수입니다.") String keyword, - // 페이징 - @Min(1) - @RequestParam(name = "page", defaultValue = "1") int page, // 페이지 번호 (0부터 시작) - @Min(1) - @RequestParam(name = "size", defaultValue = "10") int size, // 페이지 크기) + // 페이징 + @Min(1) + @RequestParam(name = "page", defaultValue = "1") int page, // 페이지 번호 (1부터 시작) + @Min(1) + @RequestParam(name = "size", defaultValue = "10") int size, // 페이지 크기 - @Parameter(hidden = true) - @AuthenticationPrincipal CustomUserDetails customUserDetails - ) - { - SliceResponseDto response = userService.searchChallengers(customUserDetails.getUser(), keyword, page-1, size); + @Parameter(hidden = true) + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) + { + SliceResponseDto response = userService.searchChallengers(customUserDetails.getUser(), keyword, page-1, size); - return ApiResponse.onSuccess(SuccessCode.OK, response); - } + return ApiResponse.onSuccess(SuccessCode.OK, response); + } - @PatchMapping("/me") + @GetMapping("/me/verifications/history") @Operation( - summary = "내 기본 정보 수정", - description = "사용자가 자신의 기본 정보(닉네임, 프로필 이미지, 프로필 공개여부)를 수정합니다" + summary = "내 챌린지 인증 기록 조회", + description = "현재 로그인한 사용자가 참여한 모든 챌린지의 인증 기록을 최신순으로 조회합니다." ) - public ApiResponse updateUserInfo( - @AuthenticationPrincipal CustomUserDetails userDetails, - @Valid @RequestBody UpdateUserInfoRequestDto requestDto + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "resultType": "SUCCESS", + "error": null, + "success": { + "content": [ + { + "verificationId": 1, + "challengeId": 101, + "challengeTitle": "미라클 모닝", + "type": "TEXT", + "title": "해피뉴이어! 올해 마지막 인증 올립니다", + "content": "여기엔 상세내용이 들어가유~", + "photoUrl": null, + "textUrl": "https://blog.example.com/post/123", + "verifiedAt": "2025-09-18T08:00:00Z" + }, + { + "verificationId": 2, + "challengeId": 102, + "challengeTitle": "매일 책 10페이지 읽기", + "type": "CAMERA", + "title": "오늘의 독서 인증", + "content": null, + "photoUrl": "https://example.com/verification_image_2.jpg", + "textUrl": null, + "verifiedAt": "2025-09-13T22:30:00Z" + } + ], + "currentPage": 0, + "size": 10, + "first": true, + "last": false, + "hasNext": true + } + } + """ + ) + ) + ) + }) + public ApiResponse> getVerificationHistory( + @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 ) { - UpdateUserInfoResponseDto response = userService.updateUserInfo(userDetails.getUser().getId(), requestDto); - return ApiResponse.onSuccess(SuccessCode.USER_UPDATE_OK, response); + Long userId = customUserDetails.getUser().getId(); + SliceResponseDto response = + verificationService.getVerificationHistory(userId, page-1, size); + + return ApiResponse.onSuccess(SuccessCode.OK, response); } -} \ No newline at end of file +} diff --git a/src/main/java/com/hrr/backend/domain/verification/converter/VerificationConverter.java b/src/main/java/com/hrr/backend/domain/verification/converter/VerificationConverter.java index 62e685a4..8e5c5835 100644 --- a/src/main/java/com/hrr/backend/domain/verification/converter/VerificationConverter.java +++ b/src/main/java/com/hrr/backend/domain/verification/converter/VerificationConverter.java @@ -169,4 +169,21 @@ public VerificationDetailResponseDto toDetailDto( } -} + /** + * Verification 엔티티를 HistoryDto로 변환 + */ + public VerificationResponseDto.HistoryDto toHistoryDto(Verification verification) { + return VerificationResponseDto.HistoryDto.builder() + .verificationId(verification.getId()) + .challengeId(verification.getRoundRecord().getUserChallenge().getChallenge().getId()) + .challengeTitle(verification.getRoundRecord().getUserChallenge().getChallenge().getTitle()) + .type(verification.getType().name()) + .title(verification.getTitle()) + .content(verification.getContent()) + .photoUrl(verification.getPhotoUrl() != null ? + s3UrlUtil.toFullUrl(verification.getPhotoUrl()) : null) + .textUrl(verification.getTextUrl()) + .verifiedAt(verification.getCreatedAt()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/verification/dto/VerificationResponseDto.java b/src/main/java/com/hrr/backend/domain/verification/dto/VerificationResponseDto.java index 1489397d..07ffa726 100644 --- a/src/main/java/com/hrr/backend/domain/verification/dto/VerificationResponseDto.java +++ b/src/main/java/com/hrr/backend/domain/verification/dto/VerificationResponseDto.java @@ -137,4 +137,40 @@ public static class CreateResponseDto { /** 현재 라운드 인증 횟수 */ private Integer verificationCount; } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "사용자 전체 인증 기록 DTO") + public static class HistoryDto { + + @Schema(description = "인증 ID", example = "1") + private Long verificationId; + + @Schema(description = "챌린지 ID", example = "101") + private Long challengeId; + + @Schema(description = "챌린지 제목", example = "미라클 모닝") + private String challengeTitle; + + @Schema(description = "인증 타입", example = "TEXT", allowableValues = {"TEXT", "CAMERA"}) + private String type; + + @Schema(description = "인증 제목", example = "해피뉴이어! 올해 마지막 인증 올립니다") + private String title; + + @Schema(description = "인증 내용", example = "여기엔 상세내용이 들어가유~") + private String content; + + @Schema(description = "사진 URL (사진 인증)", example = "https://example.com/verification_image_1.jpg") + private String photoUrl; + + @Schema(description = "글 URL (글 인증)", example = "https://blog.example.com/post/123") + private String textUrl; + + @Schema(description = "인증 일시", example = "2025-09-18T08:00:00") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime verifiedAt; + } } \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepository.java b/src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepository.java index 12497aa4..b499dbb8 100644 --- a/src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepository.java +++ b/src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepository.java @@ -1,10 +1,12 @@ package com.hrr.backend.domain.verification.repository; +import com.hrr.backend.domain.user.entity.User; import com.hrr.backend.domain.verification.entity.Verification; import com.hrr.backend.domain.verification.entity.enums.VerificationPostType; import com.hrr.backend.domain.verification.entity.enums.VerificationStatus; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; @@ -169,4 +171,20 @@ Page findMyVerifications( Pageable pageable ); -} + /** + * 사용자의 전체 챌린지 인증 기록 조회 (페이징) + */ + @Query("SELECT v FROM Verification v " + + "JOIN FETCH v.roundRecord rr " + + "JOIN FETCH rr.userChallenge uc " + + "JOIN FETCH uc.challenge c " + + "WHERE uc.user = :user " + + "AND v.status = :status " + + "ORDER BY v.createdAt DESC") + Slice findVerificationHistoryByUser( + @Param("user") User user, + @Param("status") VerificationStatus status, + Pageable pageable + ); + +} \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/verification/service/VerificationService.java b/src/main/java/com/hrr/backend/domain/verification/service/VerificationService.java index 949630ca..0de21eac 100644 --- a/src/main/java/com/hrr/backend/domain/verification/service/VerificationService.java +++ b/src/main/java/com/hrr/backend/domain/verification/service/VerificationService.java @@ -1,9 +1,9 @@ package com.hrr.backend.domain.verification.service; -import com.hrr.backend.domain.verification.dto.VerificationDetailResponseDto; import com.hrr.backend.domain.user.entity.User; import com.hrr.backend.domain.verification.dto.VerificationRequestDto; import com.hrr.backend.domain.verification.dto.VerificationResponseDto; +import com.hrr.backend.domain.verification.dto.VerificationDetailResponseDto; import com.hrr.backend.domain.verification.dto.VerificationUpdateRequestDto; import com.hrr.backend.global.response.SliceResponseDto; @@ -56,7 +56,7 @@ VerificationResponseDto.CreateResponseDto createPhotoVerification( String title, Boolean isQuestion ); - + VerificationDetailResponseDto getVerificationDetail(Long verificationId, Long currentUserId, int page, int size); void adoptComment(Long verificationId, Long commentId, Long currentUserId); @@ -64,5 +64,13 @@ VerificationResponseDto.CreateResponseDto createPhotoVerification( VerificationDetailResponseDto updateVerification(Long verificationId, Long currentUserId, VerificationUpdateRequestDto requestDto); void deleteVerification(Long verificationId, Long currentUserId); - + + /** + * 사용자 전체 챌린지 인증 기록 조회 + */ + SliceResponseDto getVerificationHistory( + Long userId, + int page, + int size + ); } \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/verification/service/VerificationServiceImpl.java b/src/main/java/com/hrr/backend/domain/verification/service/VerificationServiceImpl.java index 1f29f214..aa73b3a3 100644 --- a/src/main/java/com/hrr/backend/domain/verification/service/VerificationServiceImpl.java +++ b/src/main/java/com/hrr/backend/domain/verification/service/VerificationServiceImpl.java @@ -106,7 +106,7 @@ public VerificationResponseDto.StatDto getVerificationStat(Long challengeId) { Integer totalParticipantCount = challenge.getCurrentParticipants(); Round currentRound = challenge.getCurrentRound(); - + // 라운드 미시작 시 0명 반환 if (currentRound == null) { return verificationConverter.toStatDto(0, totalParticipantCount, null); @@ -278,16 +278,21 @@ private LocalDateTime determineTargetDateTime(Challenge challenge, Long roundId) } @Override - @Transactional(readOnly = true) - public VerificationDetailResponseDto getVerificationDetail(Long verificationId, Long currentUserId, int page, int size) { - + public VerificationDetailResponseDto getVerificationDetail( + Long verificationId, + Long currentUserId, + int page, + int size + ) { + // 인증글 조회 Verification verification = verificationRepository.findById(verificationId) .orElseThrow(() -> new GlobalException(ErrorCode.VERIFICATION_NOT_FOUND)); - // 차단된 게시글 접근 시 예외 발생 - if (verification.getStatus() == VerificationStatus.BLOCKED) { - throw new GlobalException(ErrorCode.ACCESS_DENIED_REPORTED_POST); - } + + // 차단된 게시글 접근 시 예외 발생 + if (verification.getStatus() == VerificationStatus.BLOCKED) { + throw new GlobalException(ErrorCode.ACCESS_DENIED_REPORTED_POST); + } RoundRecord roundRecord = verification.getRoundRecord(); UserChallenge userChallenge = roundRecord.getUserChallenge(); @@ -421,7 +426,6 @@ public VerificationDetailResponseDto updateVerification(Long verificationId, Lon ); } - @Override @Transactional public void deleteVerification(Long verificationId, Long currentUserId) { @@ -445,5 +449,34 @@ public void deleteVerification(Long verificationId, Long currentUserId) { verificationRepository.delete(verification); } + /** + * 사용자 전체 챌린지 인증 기록 조회 + */ + @Override + public SliceResponseDto getVerificationHistory( + 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 verificationSlice = + verificationRepository.findVerificationHistoryByUser( + user, + VerificationStatus.COMPLETED, + pageable); + + // 엔티티를 DTO로 변환 (빌더 패턴 사용) + Slice dtoSlice = + verificationSlice.map(verificationConverter::toHistoryDto); + + // SliceResponseDto로 변환하여 반환 + return new SliceResponseDto<>(dtoSlice); + } +} \ No newline at end of file