From 9c55ba1c28f7f46f177fbc4ddd07a44fc2a19a32 Mon Sep 17 00:00:00 2001 From: Yerin Chun <20211103@sungshin.ac.kr> Date: Mon, 8 Dec 2025 03:14:11 +0900 Subject: [PATCH 01/11] =?UTF-8?q?Feat:=20=EC=9D=B8=EC=A6=9D=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84=20#12?= =?UTF-8?q?8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 71 +++++++++++++++++++ .../user/dto/UserVerificationResponseDto.java | 43 +++++++++++ .../UserVerificationRepository.java | 57 +++++++++++++++ .../UserVerificationRepositoryCustom.java | 17 +++++ .../UserVerificationRepositoryCustomImpl.java | 65 +++++++++++++++++ .../user/service/UserVerificationService.java | 14 ++++ .../service/UserVerificationServiceImpl.java | 45 ++++++++++++ 7 files changed, 312 insertions(+) create mode 100644 src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java create mode 100644 src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepository.java create mode 100644 src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustom.java create mode 100644 src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustomImpl.java create mode 100644 src/main/java/com/hrr/backend/domain/user/service/UserVerificationService.java create mode 100644 src/main/java/com/hrr/backend/domain/user/service/UserVerificationServiceImpl.java 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 ac8c3cf9..dd75e8eb 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,8 +3,10 @@ import com.hrr.backend.domain.user.dto.UserResponseDto; import com.hrr.backend.domain.user.dto.UserNicknameRequestDto; import com.hrr.backend.domain.user.dto.UserNicknameResponseDto; +import com.hrr.backend.domain.user.dto.UserVerificationResponseDto; import com.hrr.backend.domain.user.entity.User; import com.hrr.backend.domain.user.service.UserService; +import com.hrr.backend.domain.user.service.UserVerificationService; import com.hrr.backend.global.config.CustomUserDetails; import com.hrr.backend.global.response.ApiResponse; import com.hrr.backend.global.response.SliceResponseDto; @@ -36,6 +38,7 @@ public class UserController { private final UserService userService; + private final UserVerificationService userVerificationService; // 닉네임 유효성 검사 API @@ -162,4 +165,72 @@ public ApiResponse> searchChallenge return ApiResponse.onSuccess(SuccessCode.OK, response); } + + @GetMapping("/challenges/history") + @Operation( + summary = "내 챌린지 인증 기록 조회", + description = "현재 로그인한 사용자가 참여한 모든 챌린지의 인증 기록을 최신순으로 조회합니다." + ) + @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": "미라클 모닝", + "title": "해피뉴이어! 올해 마지막 인증 올립니다", + "content": "여기엔 상세내용이 들어가유~", + "imageUrl": "https://example.com/verification_image_1.jpg", + "verifiedAt": "2025-09-18T08:00:00Z" + }, + { + "verificationId": 2, + "challengeId": 102, + "challengeTitle": "매일 책 10페이지 읽기", + "title": "오늘의 독서 인증", + "content": "몰입의 즐거움 완독!", + "imageUrl": "https://example.com/verification_image_2.jpg", + "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 = "0") + @Min(0) + @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") int page, + + @RequestParam(name = "size", defaultValue = "10") + @Min(1) @Max(100) + @Parameter(description = "페이지당 데이터 개수", example = "10") int size + ) { + Long userId = customUserDetails.getUser().getId(); + SliceResponseDto response = + userVerificationService.getVerificationHistory(userId, page, size); + + return ApiResponse.onSuccess(SuccessCode.OK, response); + } } diff --git a/src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java b/src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java new file mode 100644 index 00000000..bec103f2 --- /dev/null +++ b/src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java @@ -0,0 +1,43 @@ +package com.hrr.backend.domain.user.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +public class UserVerificationResponseDto { + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "개별 인증 기록 DTO") + public static class VerificationItemDto { + + @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 = "해피뉴이어! 올해 마지막 인증 올립니다") + private String title; + + @Schema(description = "인증 내용", example = "여기엔 상세내용이 들어가유~") + private String content; + + @Schema(description = "이미지 URL", example = "https://example.com/verification_image_1.jpg") + private String imageUrl; + + @Schema(description = "인증 일시", example = "2025-09-18T08:00:00Z") + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'") + private LocalDateTime verifiedAt; + } +} \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepository.java b/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepository.java new file mode 100644 index 00000000..ae01a5a6 --- /dev/null +++ b/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepository.java @@ -0,0 +1,57 @@ +package com.hrr.backend.domain.user.repository; + +import com.hrr.backend.domain.verification.entity.Verification; +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.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; + +public interface UserVerificationRepository extends JpaRepository, UserVerificationRepositoryCustom { + + // 오늘 완료된 인증이 있는지 확인 + @Query(""" + SELECT COUNT(v) > 0 + FROM Verification v + WHERE v.roundRecord.userChallenge.id = :userChallengeId + AND v.status = :status + AND v.createdAt >= :startOfDay + AND v.createdAt <= :endOfDay + """) + boolean existsTodayVerification( + @Param("userChallengeId") Long userChallengeId, + @Param("status") VerificationStatus status, + @Param("startOfDay") LocalDateTime startOfDay, + @Param("endOfDay") LocalDateTime endOfDay + ); + + @Query("SELECT v FROM Verification v " + + "JOIN v.roundRecord r " + + "JOIN r.userChallenge uc " + + "WHERE uc.user.id = :userId " + + "AND uc.challenge.id = :challengeId " + + "AND v.createdAt BETWEEN :start AND :end " + + "AND v.status = :status") + List findWeeklyVerifications( + @Param("userId") Long userId, + @Param("challengeId") Long challengeId, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end, + @Param("status") VerificationStatus status + ); + + // 챌린지 내 내 인증 조회 (기존 메서드) + @Query("SELECT v FROM Verification v " + + "WHERE v.roundRecord.userChallenge.id = :userChallengeId " + + "AND v.status = :status " + + "ORDER BY v.createdAt DESC") + Page findMyVerifications( + @Param("userChallengeId") Long userChallengeId, + @Param("status") VerificationStatus status, + Pageable pageable + ); +} \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustom.java b/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustom.java new file mode 100644 index 00000000..3089411f --- /dev/null +++ b/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustom.java @@ -0,0 +1,17 @@ +package com.hrr.backend.domain.user.repository; + +import com.hrr.backend.domain.user.dto.UserVerificationResponseDto; +import com.hrr.backend.domain.user.entity.User; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +public interface UserVerificationRepositoryCustom { + + /** + * 사용자의 전체 인증 기록 조회 (페이징) + * @param user 조회 대상 사용자 + * @param pageable 페이징 정보 + * @return 인증 기록 Slice + */ + Slice findVerificationHistoryByUser(User user, Pageable pageable); +} \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustomImpl.java b/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustomImpl.java new file mode 100644 index 00000000..48920be8 --- /dev/null +++ b/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustomImpl.java @@ -0,0 +1,65 @@ +package com.hrr.backend.domain.user.repository; + +import com.hrr.backend.domain.challenge.entity.QChallenge; +import com.hrr.backend.domain.round.entity.QRoundRecord; +import com.hrr.backend.domain.user.dto.UserVerificationResponseDto; +import com.hrr.backend.domain.user.entity.QUserChallenge; +import com.hrr.backend.domain.user.entity.User; +import com.hrr.backend.domain.verification.entity.QVerification; +import com.hrr.backend.domain.verification.entity.enums.VerificationStatus; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class UserVerificationRepositoryCustomImpl implements UserVerificationRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public Slice findVerificationHistoryByUser(User user, Pageable pageable) { + QVerification qVerification = QVerification.verification; + QRoundRecord qRoundRecord = QRoundRecord.roundRecord; + QUserChallenge qUserChallenge = QUserChallenge.userChallenge; + QChallenge qChallenge = QChallenge.challenge; + + // 데이터 조회 (size + 1로 hasNext 판단) + List content = jpaQueryFactory + .select(Projections.fields(UserVerificationResponseDto.VerificationItemDto.class, + qVerification.id.as("verificationId"), + qChallenge.id.as("challengeId"), + qChallenge.title.as("challengeTitle"), + qVerification.title, + qVerification.content, + qVerification.photoUrl.as("imageUrl"), + qVerification.createdAt.as("verifiedAt") + )) + .from(qVerification) + .join(qVerification.roundRecord, qRoundRecord) + .join(qRoundRecord.userChallenge, qUserChallenge) + .join(qUserChallenge.challenge, qChallenge) + .where( + qUserChallenge.user.eq(user), + qVerification.status.eq(VerificationStatus.COMPLETED) + ) + .orderBy(qVerification.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) // hasNext 판단을 위해 +1 + .fetch(); + + // Slice 생성 (hasNext 판단) + boolean hasNext = content.size() > pageable.getPageSize(); + if (hasNext) { + content.remove(pageable.getPageSize()); + } + + return new SliceImpl<>(content, pageable, hasNext); + } +} \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/user/service/UserVerificationService.java b/src/main/java/com/hrr/backend/domain/user/service/UserVerificationService.java new file mode 100644 index 00000000..e56b59ee --- /dev/null +++ b/src/main/java/com/hrr/backend/domain/user/service/UserVerificationService.java @@ -0,0 +1,14 @@ +package com.hrr.backend.domain.user.service; + +import com.hrr.backend.domain.user.dto.UserVerificationResponseDto; +import com.hrr.backend.global.response.SliceResponseDto; + +public interface UserVerificationService { + + // 내 전체 인증 기록 조회 + SliceResponseDto getVerificationHistory( + Long userId, + int page, + int size + ); +} \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/user/service/UserVerificationServiceImpl.java b/src/main/java/com/hrr/backend/domain/user/service/UserVerificationServiceImpl.java new file mode 100644 index 00000000..24cc922d --- /dev/null +++ b/src/main/java/com/hrr/backend/domain/user/service/UserVerificationServiceImpl.java @@ -0,0 +1,45 @@ +package com.hrr.backend.domain.user.service; + +import com.hrr.backend.domain.user.dto.UserVerificationResponseDto; +import com.hrr.backend.domain.user.entity.User; +import com.hrr.backend.domain.user.repository.UserRepository; +import com.hrr.backend.domain.user.repository.UserVerificationRepository; +import com.hrr.backend.global.exception.GlobalException; +import com.hrr.backend.global.response.ErrorCode; +import com.hrr.backend.global.response.SliceResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserVerificationServiceImpl implements UserVerificationService { + + private final UserRepository userRepository; + private final UserVerificationRepository userVerificationRepository; + + @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 slice = + userVerificationRepository.findVerificationHistoryByUser(user, pageable); + + // SliceResponseDto로 변환하여 반환 + return new SliceResponseDto<>(slice); + } +} \ No newline at end of file From 6f6eb7c8cd71be8359703d3f079f707be9a2404c Mon Sep 17 00:00:00 2001 From: Yerin Chun <20211103@sungshin.ac.kr> Date: Mon, 8 Dec 2025 03:26:26 +0900 Subject: [PATCH 02/11] =?UTF-8?q?Feat:=20response=EC=97=90=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=95=20=EC=88=98=EC=A0=95=20#128?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 39 ++++++++++--------- .../user/dto/UserVerificationResponseDto.java | 3 ++ .../UserVerificationRepositoryCustomImpl.java | 1 + 3 files changed, 25 insertions(+), 18 deletions(-) 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 dd75e8eb..f91e2624 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 @@ -95,9 +95,9 @@ public ApiResponse> getMyO @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails customUserDetails, - @RequestParam(name = "page", defaultValue = "0") - @Min(0) - @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") int page, + @RequestParam(name = "page", defaultValue = "1") + @Min(1) + @Parameter(description = "페이지 번호 (1부터 시작)", example = "1") int page, @RequestParam(name = "size", defaultValue = "10") @Min(1) @Max(100) @@ -105,7 +105,7 @@ public ApiResponse> getMyO ) { Long userId = customUserDetails.getUser().getId(); SliceResponseDto response = - userService.getOngoingChallenges(userId, page, size); + userService.getOngoingChallenges(userId, page-1, size); return ApiResponse.onSuccess(SuccessCode.OK, response); } @@ -122,16 +122,16 @@ public ApiResponse> getUse @PathVariable @Parameter(description = "조회할 사용자 ID", example = "999") Long userId, - @RequestParam(name = "page", defaultValue = "0") - @Min(0) - @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") int page, + @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 ) { SliceResponseDto response = - userService.getOngoingChallenges(userId, page, size); + userService.getOngoingChallenges(userId, page-1, size); return ApiResponse.onSuccess(SuccessCode.OK, response); } @@ -152,16 +152,16 @@ public ApiResponse> searchChallenge @NotBlank(message = "검색어는 필수입니다.") String keyword, // 페이징 - @Min(0) - @RequestParam(name = "page", defaultValue = "0") int page, // 페이지 번호 (0부터 시작) + @Min(1) + @RequestParam(name = "page", defaultValue = "1") int page, // 페이지 번호 (1부터 시작) @Min(1) - @RequestParam(name = "size", defaultValue = "10") int size, // 페이지 크기) + @RequestParam(name = "size", defaultValue = "10") int size, // 페이지 크기 @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails customUserDetails ) { - SliceResponseDto response = userService.searchChallengers(customUserDetails.getUser(), keyword, page, size); + SliceResponseDto response = userService.searchChallengers(customUserDetails.getUser(), keyword, page-1, size); return ApiResponse.onSuccess(SuccessCode.OK, response); } @@ -188,17 +188,19 @@ public ApiResponse> searchChallenge "verificationId": 1, "challengeId": 101, "challengeTitle": "미라클 모닝", + "type": "TEXT", "title": "해피뉴이어! 올해 마지막 인증 올립니다", "content": "여기엔 상세내용이 들어가유~", - "imageUrl": "https://example.com/verification_image_1.jpg", + "imageUrl": null, "verifiedAt": "2025-09-18T08:00:00Z" }, { "verificationId": 2, "challengeId": 102, "challengeTitle": "매일 책 10페이지 읽기", + "type": "PHOTO", "title": "오늘의 독서 인증", - "content": "몰입의 즐거움 완독!", + "content": null, "imageUrl": "https://example.com/verification_image_2.jpg", "verifiedAt": "2025-09-13T22:30:00Z" } @@ -215,13 +217,14 @@ public ApiResponse> searchChallenge ) ) }) + public ApiResponse> getVerificationHistory( @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails customUserDetails, - @RequestParam(name = "page", defaultValue = "0") - @Min(0) - @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") int page, + @RequestParam(name = "page", defaultValue = "1") + @Min(1) + @Parameter(description = "페이지 번호 (1부터 시작)", example = "1") int page, @RequestParam(name = "size", defaultValue = "10") @Min(1) @Max(100) @@ -229,7 +232,7 @@ public ApiResponse response = - userVerificationService.getVerificationHistory(userId, page, size); + userVerificationService.getVerificationHistory(userId, page-1, size); return ApiResponse.onSuccess(SuccessCode.OK, response); } diff --git a/src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java b/src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java index bec103f2..4a7abd6f 100644 --- a/src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java +++ b/src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java @@ -27,6 +27,9 @@ public static class VerificationItemDto { @Schema(description = "챌린지 제목", example = "미라클 모닝") private String challengeTitle; + @Schema(description = "인증 타입", example = "TEXT", allowableValues = {"TEXT", "PHOTO"}) + private String type; + @Schema(description = "인증 제목", example = "해피뉴이어! 올해 마지막 인증 올립니다") private String title; diff --git a/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustomImpl.java b/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustomImpl.java index 48920be8..ba891c16 100644 --- a/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustomImpl.java +++ b/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustomImpl.java @@ -36,6 +36,7 @@ public Slice findVerificationHi qVerification.id.as("verificationId"), qChallenge.id.as("challengeId"), qChallenge.title.as("challengeTitle"), + qVerification.type.stringValue().as("type"), qVerification.title, qVerification.content, qVerification.photoUrl.as("imageUrl"), From aa798cd2f513275c09a2041d74fafc1ac36017d6 Mon Sep 17 00:00:00 2001 From: Yerin Chun <20211103@sungshin.ac.kr> Date: Mon, 29 Dec 2025 21:39:51 +0900 Subject: [PATCH 03/11] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=ED=99=9C=EC=9A=A9=20#128?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 20 +++---- .../user/dto/UserVerificationResponseDto.java | 46 --------------- .../UserVerificationRepository.java | 57 ------------------- .../UserVerificationRepositoryCustom.java | 17 ------ .../user/service/UserVerificationService.java | 14 ----- .../service/UserVerificationServiceImpl.java | 45 --------------- .../converter/VerificationConverter.java | 17 ++++++ .../dto/VerificationResponseDto.java | 36 ++++++++++++ .../repository/VerificationRepository.java | 2 +- .../VerificationRepositoryCustom.java | 17 ++++++ .../VerificationRepositoryCustomImpl.java} | 30 ++++------ .../service/VerificationService.java | 9 +++ .../service/VerificationServiceImpl.java | 28 +++++++++ 13 files changed, 128 insertions(+), 210 deletions(-) delete mode 100644 src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java delete mode 100644 src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepository.java delete mode 100644 src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustom.java delete mode 100644 src/main/java/com/hrr/backend/domain/user/service/UserVerificationService.java delete mode 100644 src/main/java/com/hrr/backend/domain/user/service/UserVerificationServiceImpl.java create mode 100644 src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepositoryCustom.java rename src/main/java/com/hrr/backend/domain/{user/repository/UserVerificationRepositoryCustomImpl.java => verification/repository/VerificationRepositoryCustomImpl.java} (58%) 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 f91e2624..0bf877ee 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,16 +3,14 @@ import com.hrr.backend.domain.user.dto.UserResponseDto; import com.hrr.backend.domain.user.dto.UserNicknameRequestDto; import com.hrr.backend.domain.user.dto.UserNicknameResponseDto; -import com.hrr.backend.domain.user.dto.UserVerificationResponseDto; import com.hrr.backend.domain.user.entity.User; import com.hrr.backend.domain.user.service.UserService; -import com.hrr.backend.domain.user.service.UserVerificationService; +import com.hrr.backend.domain.verification.dto.VerificationResponseDto; 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; @@ -191,17 +189,19 @@ public ApiResponse> searchChallenge "type": "TEXT", "title": "해피뉴이어! 올해 마지막 인증 올립니다", "content": "여기엔 상세내용이 들어가유~", - "imageUrl": null, + "photoUrl": null, + "textUrl": "https://blog.example.com/post/123", "verifiedAt": "2025-09-18T08:00:00Z" }, { "verificationId": 2, "challengeId": 102, "challengeTitle": "매일 책 10페이지 읽기", - "type": "PHOTO", + "type": "CAMERA", "title": "오늘의 독서 인증", "content": null, - "imageUrl": "https://example.com/verification_image_2.jpg", + "photoUrl": "https://example.com/verification_image_2.jpg", + "textUrl": null, "verifiedAt": "2025-09-13T22:30:00Z" } ], @@ -217,8 +217,7 @@ public ApiResponse> searchChallenge ) ) }) - - public ApiResponse> getVerificationHistory( + public ApiResponse> getVerificationHistory( @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails customUserDetails, @@ -231,9 +230,10 @@ public ApiResponse response = - userVerificationService.getVerificationHistory(userId, page-1, size); + SliceResponseDto response = + verificationService.getVerificationHistory(userId, page-1, size); return ApiResponse.onSuccess(SuccessCode.OK, response); } } +} diff --git a/src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java b/src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java deleted file mode 100644 index 4a7abd6f..00000000 --- a/src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.hrr.backend.domain.user.dto; - -import com.fasterxml.jackson.annotation.JsonFormat; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -public class UserVerificationResponseDto { - - @Getter - @Builder - @NoArgsConstructor - @AllArgsConstructor - @Schema(description = "개별 인증 기록 DTO") - public static class VerificationItemDto { - - @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", "PHOTO"}) - 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 imageUrl; - - @Schema(description = "인증 일시", example = "2025-09-18T08:00:00Z") - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'") - private LocalDateTime verifiedAt; - } -} \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepository.java b/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepository.java deleted file mode 100644 index ae01a5a6..00000000 --- a/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepository.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.hrr.backend.domain.user.repository; - -import com.hrr.backend.domain.verification.entity.Verification; -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.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.time.LocalDateTime; -import java.util.List; - -public interface UserVerificationRepository extends JpaRepository, UserVerificationRepositoryCustom { - - // 오늘 완료된 인증이 있는지 확인 - @Query(""" - SELECT COUNT(v) > 0 - FROM Verification v - WHERE v.roundRecord.userChallenge.id = :userChallengeId - AND v.status = :status - AND v.createdAt >= :startOfDay - AND v.createdAt <= :endOfDay - """) - boolean existsTodayVerification( - @Param("userChallengeId") Long userChallengeId, - @Param("status") VerificationStatus status, - @Param("startOfDay") LocalDateTime startOfDay, - @Param("endOfDay") LocalDateTime endOfDay - ); - - @Query("SELECT v FROM Verification v " + - "JOIN v.roundRecord r " + - "JOIN r.userChallenge uc " + - "WHERE uc.user.id = :userId " + - "AND uc.challenge.id = :challengeId " + - "AND v.createdAt BETWEEN :start AND :end " + - "AND v.status = :status") - List findWeeklyVerifications( - @Param("userId") Long userId, - @Param("challengeId") Long challengeId, - @Param("start") LocalDateTime start, - @Param("end") LocalDateTime end, - @Param("status") VerificationStatus status - ); - - // 챌린지 내 내 인증 조회 (기존 메서드) - @Query("SELECT v FROM Verification v " + - "WHERE v.roundRecord.userChallenge.id = :userChallengeId " + - "AND v.status = :status " + - "ORDER BY v.createdAt DESC") - Page findMyVerifications( - @Param("userChallengeId") Long userChallengeId, - @Param("status") VerificationStatus status, - Pageable pageable - ); -} \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustom.java b/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustom.java deleted file mode 100644 index 3089411f..00000000 --- a/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustom.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.hrr.backend.domain.user.repository; - -import com.hrr.backend.domain.user.dto.UserVerificationResponseDto; -import com.hrr.backend.domain.user.entity.User; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; - -public interface UserVerificationRepositoryCustom { - - /** - * 사용자의 전체 인증 기록 조회 (페이징) - * @param user 조회 대상 사용자 - * @param pageable 페이징 정보 - * @return 인증 기록 Slice - */ - Slice findVerificationHistoryByUser(User user, Pageable pageable); -} \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/user/service/UserVerificationService.java b/src/main/java/com/hrr/backend/domain/user/service/UserVerificationService.java deleted file mode 100644 index e56b59ee..00000000 --- a/src/main/java/com/hrr/backend/domain/user/service/UserVerificationService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.hrr.backend.domain.user.service; - -import com.hrr.backend.domain.user.dto.UserVerificationResponseDto; -import com.hrr.backend.global.response.SliceResponseDto; - -public interface UserVerificationService { - - // 내 전체 인증 기록 조회 - SliceResponseDto getVerificationHistory( - Long userId, - int page, - int size - ); -} \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/user/service/UserVerificationServiceImpl.java b/src/main/java/com/hrr/backend/domain/user/service/UserVerificationServiceImpl.java deleted file mode 100644 index 24cc922d..00000000 --- a/src/main/java/com/hrr/backend/domain/user/service/UserVerificationServiceImpl.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.hrr.backend.domain.user.service; - -import com.hrr.backend.domain.user.dto.UserVerificationResponseDto; -import com.hrr.backend.domain.user.entity.User; -import com.hrr.backend.domain.user.repository.UserRepository; -import com.hrr.backend.domain.user.repository.UserVerificationRepository; -import com.hrr.backend.global.exception.GlobalException; -import com.hrr.backend.global.response.ErrorCode; -import com.hrr.backend.global.response.SliceResponseDto; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class UserVerificationServiceImpl implements UserVerificationService { - - private final UserRepository userRepository; - private final UserVerificationRepository userVerificationRepository; - - @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 slice = - userVerificationRepository.findVerificationHistoryByUser(user, pageable); - - // SliceResponseDto로 변환하여 반환 - return new SliceResponseDto<>(slice); - } -} \ 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 71dd96d6..f65a4cae 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 @@ -100,4 +100,21 @@ public VerificationResponseDto.MyProfileDto toMyProfileDto( .build(); } + /** + * 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()) + .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..8826c2ae 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:00Z") + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'") + 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 46ebe0aa..2e2a563f 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 @@ -12,7 +12,7 @@ import java.time.LocalDateTime; import java.util.List; -public interface VerificationRepository extends JpaRepository { +public interface VerificationRepository extends JpaRepository, VerificationRepositoryCustom { // 오늘 완료된 인증이 있는지 확인 @Query(""" diff --git a/src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepositoryCustom.java b/src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepositoryCustom.java new file mode 100644 index 00000000..3c432dbf --- /dev/null +++ b/src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepositoryCustom.java @@ -0,0 +1,17 @@ +package com.hrr.backend.domain.verification.repository; + +import com.hrr.backend.domain.user.entity.User; +import com.hrr.backend.domain.verification.entity.Verification; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +public interface VerificationRepositoryCustom { + + /** + * 사용자의 전체 챌린지 인증 기록 조회 (페이징) + * @param user 조회 대상 사용자 + * @param pageable 페이징 정보 + * @return 인증 엔티티 Slice + */ + Slice findVerificationHistoryByUser(User user, Pageable pageable); +} \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustomImpl.java b/src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepositoryCustomImpl.java similarity index 58% rename from src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustomImpl.java rename to src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepositoryCustomImpl.java index ba891c16..9d8b1bdd 100644 --- a/src/main/java/com/hrr/backend/domain/user/repository/UserVerificationRepositoryCustomImpl.java +++ b/src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepositoryCustomImpl.java @@ -1,13 +1,12 @@ -package com.hrr.backend.domain.user.repository; +package com.hrr.backend.domain.verification.repository; import com.hrr.backend.domain.challenge.entity.QChallenge; import com.hrr.backend.domain.round.entity.QRoundRecord; -import com.hrr.backend.domain.user.dto.UserVerificationResponseDto; import com.hrr.backend.domain.user.entity.QUserChallenge; import com.hrr.backend.domain.user.entity.User; import com.hrr.backend.domain.verification.entity.QVerification; +import com.hrr.backend.domain.verification.entity.Verification; import com.hrr.backend.domain.verification.entity.enums.VerificationStatus; -import com.querydsl.core.types.Projections; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; @@ -19,33 +18,24 @@ @Repository @RequiredArgsConstructor -public class UserVerificationRepositoryCustomImpl implements UserVerificationRepositoryCustom { +public class VerificationRepositoryCustomImpl implements VerificationRepositoryCustom { private final JPAQueryFactory jpaQueryFactory; @Override - public Slice findVerificationHistoryByUser(User user, Pageable pageable) { + public Slice findVerificationHistoryByUser(User user, Pageable pageable) { QVerification qVerification = QVerification.verification; QRoundRecord qRoundRecord = QRoundRecord.roundRecord; QUserChallenge qUserChallenge = QUserChallenge.userChallenge; QChallenge qChallenge = QChallenge.challenge; - // 데이터 조회 (size + 1로 hasNext 판단) - List content = jpaQueryFactory - .select(Projections.fields(UserVerificationResponseDto.VerificationItemDto.class, - qVerification.id.as("verificationId"), - qChallenge.id.as("challengeId"), - qChallenge.title.as("challengeTitle"), - qVerification.type.stringValue().as("type"), - qVerification.title, - qVerification.content, - qVerification.photoUrl.as("imageUrl"), - qVerification.createdAt.as("verifiedAt") - )) + // 엔티티 조회 (size + 1로 hasNext 판단) + List content = jpaQueryFactory + .select(qVerification) .from(qVerification) - .join(qVerification.roundRecord, qRoundRecord) - .join(qRoundRecord.userChallenge, qUserChallenge) - .join(qUserChallenge.challenge, qChallenge) + .join(qVerification.roundRecord, qRoundRecord).fetchJoin() + .join(qRoundRecord.userChallenge, qUserChallenge).fetchJoin() + .join(qUserChallenge.challenge, qChallenge).fetchJoin() .where( qUserChallenge.user.eq(user), qVerification.status.eq(VerificationStatus.COMPLETED) 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 a94336dc..defe259a 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 @@ -54,4 +54,13 @@ VerificationResponseDto.CreateResponseDto createPhotoVerification( String title, Boolean isQuestion ); + + /** + * 사용자 전체 챌린지 인증 기록 조회 + */ + 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 ab712ec8..c7367557 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 @@ -266,4 +266,32 @@ private LocalDateTime determineTargetDateTime(Challenge challenge, Long roundId) ); } } + + /** + * 사용자 전체 챌린지 인증 기록 조회 + */ + @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, pageable); + + // 엔티티를 DTO로 변환 (빌더 패턴 사용) + Slice dtoSlice = + verificationSlice.map(verificationConverter::toHistoryDto); + + // SliceResponseDto로 변환하여 반환 + return new SliceResponseDto<>(dtoSlice); + } } \ No newline at end of file From c98a4ccb3031714dccd67a95d258bed525aa4607 Mon Sep 17 00:00:00 2001 From: Yerin Chun <20211103@sungshin.ac.kr> Date: Tue, 30 Dec 2025 01:55:20 +0900 Subject: [PATCH 04/11] =?UTF-8?q?refactor:=20Custom=20Repository=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=EC=9D=84=20@Query=EB=A1=9C=20=EB=8B=A8?= =?UTF-8?q?=EC=88=9C=ED=99=94#128?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 4 +- .../converter/VerificationConverter.java | 1 - .../repository/VerificationRepository.java | 19 ++++++- .../VerificationRepositoryCustom.java | 17 ------ .../VerificationRepositoryCustomImpl.java | 56 ------------------- .../service/VerificationServiceImpl.java | 5 +- 6 files changed, 21 insertions(+), 81 deletions(-) delete mode 100644 src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepositoryCustom.java delete mode 100644 src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepositoryCustomImpl.java 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 0bf877ee..72db4c57 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 @@ -6,6 +6,7 @@ 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; @@ -36,7 +37,7 @@ public class UserController { private final UserService userService; - private final UserVerificationService userVerificationService; + private final VerificationService verificationService; // 닉네임 유효성 검사 API @@ -236,4 +237,3 @@ public ApiResponse> getVeri return ApiResponse.onSuccess(SuccessCode.OK, response); } } -} 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 f65a4cae..7839e28d 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 @@ -116,5 +116,4 @@ public VerificationResponseDto.HistoryDto toHistoryDto(Verification verification .verifiedAt(verification.getCreatedAt()) .build(); } -} } \ 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 2e2a563f..fd60d324 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.Query; import org.springframework.data.repository.query.Param; @@ -12,7 +14,7 @@ import java.time.LocalDateTime; import java.util.List; -public interface VerificationRepository extends JpaRepository, VerificationRepositoryCustom { +public interface VerificationRepository extends JpaRepository { // 오늘 완료된 인증이 있는지 확인 @Query(""" @@ -157,4 +159,19 @@ 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 = 'COMPLETED' " + + "ORDER BY v.createdAt DESC") + Slice findVerificationHistoryByUser( + @Param("user") User user, + Pageable pageable + ); + } \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepositoryCustom.java b/src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepositoryCustom.java deleted file mode 100644 index 3c432dbf..00000000 --- a/src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepositoryCustom.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.hrr.backend.domain.verification.repository; - -import com.hrr.backend.domain.user.entity.User; -import com.hrr.backend.domain.verification.entity.Verification; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; - -public interface VerificationRepositoryCustom { - - /** - * 사용자의 전체 챌린지 인증 기록 조회 (페이징) - * @param user 조회 대상 사용자 - * @param pageable 페이징 정보 - * @return 인증 엔티티 Slice - */ - Slice findVerificationHistoryByUser(User user, Pageable pageable); -} \ No newline at end of file diff --git a/src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepositoryCustomImpl.java b/src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepositoryCustomImpl.java deleted file mode 100644 index 9d8b1bdd..00000000 --- a/src/main/java/com/hrr/backend/domain/verification/repository/VerificationRepositoryCustomImpl.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.hrr.backend.domain.verification.repository; - -import com.hrr.backend.domain.challenge.entity.QChallenge; -import com.hrr.backend.domain.round.entity.QRoundRecord; -import com.hrr.backend.domain.user.entity.QUserChallenge; -import com.hrr.backend.domain.user.entity.User; -import com.hrr.backend.domain.verification.entity.QVerification; -import com.hrr.backend.domain.verification.entity.Verification; -import com.hrr.backend.domain.verification.entity.enums.VerificationStatus; -import com.querydsl.jpa.impl.JPAQueryFactory; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -@RequiredArgsConstructor -public class VerificationRepositoryCustomImpl implements VerificationRepositoryCustom { - - private final JPAQueryFactory jpaQueryFactory; - - @Override - public Slice findVerificationHistoryByUser(User user, Pageable pageable) { - QVerification qVerification = QVerification.verification; - QRoundRecord qRoundRecord = QRoundRecord.roundRecord; - QUserChallenge qUserChallenge = QUserChallenge.userChallenge; - QChallenge qChallenge = QChallenge.challenge; - - // 엔티티 조회 (size + 1로 hasNext 판단) - List content = jpaQueryFactory - .select(qVerification) - .from(qVerification) - .join(qVerification.roundRecord, qRoundRecord).fetchJoin() - .join(qRoundRecord.userChallenge, qUserChallenge).fetchJoin() - .join(qUserChallenge.challenge, qChallenge).fetchJoin() - .where( - qUserChallenge.user.eq(user), - qVerification.status.eq(VerificationStatus.COMPLETED) - ) - .orderBy(qVerification.createdAt.desc()) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize() + 1) // hasNext 판단을 위해 +1 - .fetch(); - - // Slice 생성 (hasNext 판단) - boolean hasNext = content.size() > pageable.getPageSize(); - if (hasNext) { - content.remove(pageable.getPageSize()); - } - - return new SliceImpl<>(content, pageable, hasNext); - } -} \ 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 c7367557..57344564 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 @@ -267,9 +267,6 @@ private LocalDateTime determineTargetDateTime(Challenge challenge, Long roundId) } } - /** - * 사용자 전체 챌린지 인증 기록 조회 - */ @Override public SliceResponseDto getVerificationHistory( Long userId, @@ -283,7 +280,7 @@ public SliceResponseDto getVerificationHisto // Pageable 객체 생성 Pageable pageable = PageRequest.of(page, size); - // Repository에서 인증 엔티티 조회 + // Repository에서 인증 엔티티 조회 (⭐ @Query 메서드 사용) Slice verificationSlice = verificationRepository.findVerificationHistoryByUser(user, pageable); From cfabda9b77073d468f82ab4ca1a06df2998ae878 Mon Sep 17 00:00:00 2001 From: Yerin Chun <20211103@sungshin.ac.kr> Date: Tue, 30 Dec 2025 08:10:01 +0900 Subject: [PATCH 05/11] =?UTF-8?q?refactor:=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EC=88=98=EC=A0=95=20#128?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/hrr/backend/domain/user/controller/UserController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 72db4c57..83106d16 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 @@ -165,7 +165,7 @@ public ApiResponse> searchChallenge return ApiResponse.onSuccess(SuccessCode.OK, response); } - @GetMapping("/challenges/history") + @GetMapping("/me/verifications/history") @Operation( summary = "내 챌린지 인증 기록 조회", description = "현재 로그인한 사용자가 참여한 모든 챌린지의 인증 기록을 최신순으로 조회합니다." From def003a78d2328cfef69e9c2b5580fbde28c98e9 Mon Sep 17 00:00:00 2001 From: Yerin Chun <20211103@sungshin.ac.kr> Date: Tue, 30 Dec 2025 08:42:07 +0900 Subject: [PATCH 06/11] =?UTF-8?q?fix:verifiedAt=EC=9D=98=20JsonFormat=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=EC=97=90=EC=84=9C=20=ED=83=80=EC=9E=84?= =?UTF-8?q?=EC=A1=B4=20=ED=91=9C=ED=98=84=20=EC=88=98=EC=A0=95=20#128?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/verification/dto/VerificationResponseDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8826c2ae..7441dd7c 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 @@ -170,7 +170,7 @@ public static class HistoryDto { private String textUrl; @Schema(description = "인증 일시", example = "2025-09-18T08:00:00Z") - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'") + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") private LocalDateTime verifiedAt; } } \ No newline at end of file From 0f1a6b8b61ec2c31324b0827a889e69ccc169629 Mon Sep 17 00:00:00 2001 From: Yerin Chun <20211103@sungshin.ac.kr> Date: Tue, 30 Dec 2025 08:49:31 +0900 Subject: [PATCH 07/11] =?UTF-8?q?fix:=20=ED=95=98=EB=93=9C=EC=BD=94?= =?UTF-8?q?=EB=94=A9=20=EC=88=98=EC=A0=95=20#128?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/verification/dto/VerificationResponseDto.java | 4 ++-- .../verification/repository/VerificationRepository.java | 3 ++- .../verification/service/VerificationServiceImpl.java | 7 +++++-- 3 files changed, 9 insertions(+), 5 deletions(-) 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 7441dd7c..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 @@ -169,8 +169,8 @@ public static class HistoryDto { @Schema(description = "글 URL (글 인증)", example = "https://blog.example.com/post/123") private String textUrl; - @Schema(description = "인증 일시", example = "2025-09-18T08:00:00Z") - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + @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 fd60d324..0643aa1a 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 @@ -167,10 +167,11 @@ Page findMyVerifications( "JOIN FETCH rr.userChallenge uc " + "JOIN FETCH uc.challenge c " + "WHERE uc.user = :user " + - "AND v.status = 'COMPLETED' " + + "AND v.status = :status " + "ORDER BY v.createdAt DESC") Slice findVerificationHistoryByUser( @Param("user") User user, + @Param("status") VerificationStatus status, Pageable pageable ); 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 57cba05c..a9602e92 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 @@ -283,9 +283,12 @@ public SliceResponseDto getVerificationHisto // Pageable 객체 생성 Pageable pageable = PageRequest.of(page, size); - // Repository에서 인증 엔티티 조회 (⭐ @Query 메서드 사용) + // Repository에서 인증 엔티티 조회 Slice verificationSlice = - verificationRepository.findVerificationHistoryByUser(user, pageable); + verificationRepository.findVerificationHistoryByUser( + user, + VerificationStatus.COMPLETED, + pageable); // 엔티티를 DTO로 변환 (빌더 패턴 사용) Slice dtoSlice = From 2de67f86c44ab6f20e14c431e3a06353eae82b52 Mon Sep 17 00:00:00 2001 From: Yerin Chun <20211103@sungshin.ac.kr> Date: Tue, 30 Dec 2025 08:53:55 +0900 Subject: [PATCH 08/11] =?UTF-8?q?fix:=20S3=20URL=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95=20#128?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/verification/converter/VerificationConverter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 b04d6f46..afebfe06 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 @@ -163,7 +163,7 @@ public VerificationDetailResponseDto toDetailDto( /** - * Verification 엔티티를 HistoryDto로 변환 (빌더 패턴) + * Verification 엔티티를 HistoryDto로 변환 */ public VerificationResponseDto.HistoryDto toHistoryDto(Verification verification) { return VerificationResponseDto.HistoryDto.builder() @@ -173,7 +173,8 @@ public VerificationResponseDto.HistoryDto toHistoryDto(Verification verification .type(verification.getType().name()) .title(verification.getTitle()) .content(verification.getContent()) - .photoUrl(verification.getPhotoUrl()) + .photoUrl(verification.getPhotoUrl() != null ? + s3UrlUtil.toFullUrl(verification.getPhotoUrl()) : null) .textUrl(verification.getTextUrl()) .verifiedAt(verification.getCreatedAt()) .build(); From bd41469e91486e3566a989afb84300d709be929e Mon Sep 17 00:00:00 2001 From: Yerin Chun <20211103@sungshin.ac.kr> Date: Tue, 30 Dec 2025 09:24:05 +0900 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20=EC=A4=91=EA=B4=84=ED=98=B8=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=20#128?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/VerificationServiceImpl.java | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) 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 3909985f..051de77d 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 @@ -276,10 +276,22 @@ private LocalDateTime determineTargetDateTime(Challenge challenge, Long roundId) } } - // 차단된 게시글 접근 시 예외 발생 - if (verification.getStatus() == VerificationStatus.BLOCKED) { - throw new GlobalException(ErrorCode.ACCESS_DENIED_REPORTED_POST); - } + @Override + 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); + } RoundRecord roundRecord = verification.getRoundRecord(); UserChallenge userChallenge = roundRecord.getUserChallenge(); @@ -466,4 +478,4 @@ public SliceResponseDto getVerificationHisto // SliceResponseDto로 변환하여 반환 return new SliceResponseDto<>(dtoSlice); } -} +} \ No newline at end of file From 4f123b9b6d8d9ae19861ab246b4178c6e1f49c6c Mon Sep 17 00:00:00 2001 From: Yerin Chun <20211103@sungshin.ac.kr> Date: Tue, 30 Dec 2025 09:28:05 +0900 Subject: [PATCH 10/11] =?UTF-8?q?fix:=20import=20=EB=88=84=EB=9D=BD=20#128?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/verification/service/VerificationServiceImpl.java | 1 + 1 file changed, 1 insertion(+) 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 051de77d..1684fe91 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 @@ -30,6 +30,7 @@ import com.hrr.backend.domain.verification.converter.VerificationConverter; 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.entity.Verification; import com.hrr.backend.domain.verification.entity.enums.VerificationPostType; import com.hrr.backend.domain.verification.entity.enums.VerificationStatus; From 835dd9787ae3b9bfcd65d6515826a33f374f07c4 Mon Sep 17 00:00:00 2001 From: Yerin Chun <20211103@sungshin.ac.kr> Date: Tue, 30 Dec 2025 09:41:01 +0900 Subject: [PATCH 11/11] =?UTF-8?q?fix:import=20=EB=88=84=EB=9D=BD=20#128?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/verification/service/VerificationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0f5d5023..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 @@ -3,6 +3,7 @@ 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; @@ -72,5 +73,4 @@ SliceResponseDto getVerificationHistory( int page, int size ); - } \ No newline at end of file