-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 찜한 챌린지, 종료한 챌린지 조회 API 구현 #172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
📝 WalkthroughWalkthrough인증 사용자 기준으로 찜(liked) 챌린지와 완료(completed) 챌린지를 Slice 기반 페이징으로 조회하는 엔드포인트·DTO·QueryDSL 커스텀 리포지토리·서비스 및 S3 이미지 URL 변환 로직이 추가되었습니다. 서비스 구현에 중복 선언 의심이 있습니다. (≤50단어) Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Controller as UserController
participant Service as UserService
participant Repository as UserChallengeRepository
participant DB as Database
participant S3 as S3UrlUtil
rect rgba(200,230,255,0.18)
Note over User,Controller: 찜된 챌린지 조회 (/liked)
User->>Controller: GET /api/v1/user/challenges/liked?page=1&size=10
Controller->>Controller: 인증정보에서 userId 추출\npage/size 검증
Controller->>Service: getMarkedChallenges(userId, page-1, size)
Service->>Repository: findMarkedChallengesByUser(user, pageable)
Repository->>DB: QueryDSL (ChallengeLike join, projection, limit pageSize+1)
DB-->>Repository: 결과 리스트
Repository-->>Service: Slice<LikedChallengeDto> (hasNext 계산)
Service->>S3: toFullUrl(image) 반복 변환
Service-->>Controller: SliceResponseDto<LikedChallengeDto>
Controller-->>User: ApiResponse (SuccessCode.OK)
end
rect rgba(255,240,210,0.18)
Note over User,Controller: 완료된 챌린지 조회 (/completed)
User->>Controller: GET /api/v1/user/challenges/completed?page=1&size=10
Controller->>Controller: 인증정보에서 userId 추출\npage/size 검증
Controller->>Service: getCompletedChallenges(userId, page-1, size)
Service->>Repository: findCompletedChallengesByUser(user, pageable)
Repository->>DB: QueryDSL (UserChallenge join + Challenge status filter, limit pageSize+1)
DB-->>Repository: 결과 리스트
Repository-->>Service: Slice<CompletedChallengeDto> (hasNext 계산)
Service->>S3: toFullUrl(image) 반복 변환
Service-->>Controller: SliceResponseDto<CompletedChallengeDto>
Controller-->>User: ApiResponse (SuccessCode.OK)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (4)
src/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.java (2)
163-205: 중복 코드 제거를 고려해주세요.
MarkedChallengeDto와CompletedChallengeDto가 완전히 동일한 구조를 가지고 있어 코드 중복이 발생하고 있습니다. 두 DTO를 하나의 공통 DTO로 통합하는 것을 권장합니다.제안하는 리팩토링:
🔎 공통 DTO로 통합하는 방안
+ @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "챌린지 요약 정보 DTO") + public static class ChallengeSummaryDto { + + @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 MarkedChallengeDto { - ... - } - - @Getter - @Setter - @Builder - @NoArgsConstructor - @AllArgsConstructor - @Schema(description = "종료한 챌린지 정보 DTO") - public static class CompletedChallengeDto { - ... - }이렇게 하면:
- 코드 중복이 제거됩니다
- 향후 필드 추가/수정 시 한 곳만 수정하면 됩니다
- 타입 안정성은 제네릭이나 메서드 시그니처로 보장할 수 있습니다
참고: DRY(Don't Repeat Yourself) 원칙에 대해서는 Effective Java 3/E - Item 3를 참고하세요.
180-182: 필드명 일관성을 개선해주세요.같은 파일의
OngoingChallengeDto는 이미지 필드명으로thumbnailUrl을 사용하는데, 새로 추가된 DTO들은image를 사용하고 있습니다. 팀 내 네이밍 컨벤션에 맞춰 통일하는 것을 권장합니다.제안:
- 모든 챌린지 DTO에서
thumbnailUrl로 통일하거나- 또는 모두
image로 통일일관된 네이밍은 API 사용자 경험과 코드 가독성을 높여줍니다. 🎯
Also applies to: 202-204
src/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java (1)
288-340: 코드 중복을 줄일 수 있는 리팩토링 기회가 있습니다.
getMarkedChallenges와getCompletedChallenges두 메서드가 거의 동일한 구조를 가지고 있습니다. 현재는 두 개지만, 향후 유사한 챌린지 조회 메서드가 추가될 경우 중복이 계속 증가할 수 있습니다.현재 구조:
- 사용자 조회 → Pageable 생성 → Repository 호출 → S3 URL 변환 → SliceResponseDto 반환
이미
getOngoingChallenges(lines 73-139)도 유사한 패턴을 따르고 있어서, 공통 로직을 추출하면 유지보수성이 향상됩니다.💡 제네릭 헬퍼 메서드 예시
private <T> SliceResponseDto<T> getChallengeSlice( Long userId, int page, int size, BiFunction<User, Pageable, Slice<T>> repositoryMethod, Consumer<T> imageConverter ) { User user = userRepository.findById(userId) .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND)); Pageable pageable = PageRequest.of(page, size); Slice<T> slice = repositoryMethod.apply(user, pageable); slice.getContent().forEach(imageConverter); return new SliceResponseDto<>(slice); } // 사용 예: public SliceResponseDto<UserResponseDto.MarkedChallengeDto> getMarkedChallenges( Long userId, int page, int size) { return getChallengeSlice( userId, page, size, userChallengeRepository::findMarkedChallengesByUser, dto -> dto.setImage(s3UrlUtil.toFullUrl(dto.getImage())) ); }다만 현재 3개 메서드만 있고
getOngoingChallenges는 추가 로직(인증 완료 여부 체크)이 있어서, 당장 리팩토링하지 않고 나중에 메서드가 더 늘어날 때 고려해도 됩니다. 😊src/main/java/com/hrr/backend/domain/user/controller/UserController.java (1)
166-188: 엔드포인트와 코드 네이밍 일관성을 확인해주세요.API 경로는
/challenges/liked를 사용하는데, 메서드명과 DTO는Marked를 사용하고 있습니다.현재 상황:
- API 경로:
/challenges/liked- 메서드명:
getMarkedChallenges- DTO:
MarkedChallengeDto- 리포지토리:
findMarkedChallengesByUser제안:
둘 다 의미상 맞지만, 전체 코드베이스에서 일관성을 유지하는 것이 좋습니다.
- API와 맞추려면: 모든 코드를
Liked로 변경- 코드와 맞추려면: API 경로를
/challenges/marked로 변경팀 내에서 "찜하다"를 영어로 표현할 때 "like"와 "mark" 중 어떤 용어를 표준으로 할지 합의하면 좋겠습니다. 🎯
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/main/java/com/hrr/backend/domain/user/controller/UserController.javasrc/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.javasrc/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustom.javasrc/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.javasrc/main/java/com/hrr/backend/domain/user/service/UserService.javasrc/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java
🔇 Additional comments (6)
src/main/java/com/hrr/backend/domain/user/service/UserService.java (1)
47-63: 인터페이스 메서드 선언이 깔끔합니다! 👍두 메서드 모두 기존
getOngoingChallenges패턴과 일관성 있게 설계되어 있고, Javadoc도 명확하게 작성되었습니다.src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustom.java (1)
18-32: 리포지토리 인터페이스 확장이 적절합니다! ✨기존
findOngoingChallengesByUser와 일관된 패턴으로 새로운 조회 메서드들이 추가되었고, Javadoc도 명확합니다.src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java (1)
70-98: 찜한 챌린지 조회 로직이 잘 구현되었습니다! 👏ChallengeLike 테이블을 활용한 쿼리 구조가 적절하고,
createdAt기준 정렬도 직관적입니다. Slice 기반 페이징 처리도 정확합니다.src/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java (2)
288-313: 찜한 챌린지 조회 서비스 로직이 견고합니다! ✅사용자 존재 검증, 페이징 처리, S3 URL 변환이 모두 적절하게 구현되어 있습니다.
315-340: 종료한 챌린지 조회 서비스 로직이 깔끔합니다! ✅전반적인 구현이 기존 패턴과 일관성 있게 작성되었습니다.
src/main/java/com/hrr/backend/domain/user/controller/UserController.java (1)
166-212: 두 엔드포인트 구현이 탄탄합니다! 🚀인증 처리, 페이징 검증, 응답 래핑이 기존 패턴과 일관성 있게 잘 구현되어 있습니다. Swagger 문서화도 적절하게 추가되었네요.
src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java
Show resolved
Hide resolved
src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java (1)
339-374: [긴급] 컴파일 오류: 클래스 닫는 괄호 위치 오류Line 339의 닫는 괄호
}가 클래스를 조기에 종료시켜,updateUserInfo메서드(lines 341-373)가 클래스 외부에 정의되고 있습니다. 이는 Java 구문 오류이며 파이프라인 실패의 원인입니다.문제 원인:
- Line 339가
UserServiceImpl클래스를 닫음- Lines 340-373의
updateUserInfo메서드가 클래스 밖에 위치- 컴파일러 오류: "class, interface, or enum expected"
해결 방법:
Line 339의 닫는 괄호를 제거하세요. 클래스의 실제 닫는 괄호는 line 374에 있어야 합니다.🔧 수정 제안
// SliceResponseDto로 변환하여 반환 return new SliceResponseDto<>(slice); } -} + // 내 정보 수정 @Override @Transactional
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/main/java/com/hrr/backend/domain/user/controller/UserController.javasrc/main/java/com/hrr/backend/domain/user/service/UserService.javasrc/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java
🧰 Additional context used
🪛 GitHub Actions: CI
src/main/java/com/hrr/backend/domain/user/controller/UserController.java
[error] 242-285: Compilation error: numerous syntax issues (e.g., ';' expected', ' expected', illegal start of expression) around getMarkedChallenges and related request parameter annotations. Likely caused by missing/imbalanced braces or incorrect annotation placement. End of file parsing reached.
src/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java
[error] 343-373: Compilation error: multiple 'class, interface, or enum expected' issues likely due to mismatched braces or misplaced method definitions in updateUserInfo. See lines 343-373 for related errors.
🔇 Additional comments (7)
src/main/java/com/hrr/backend/domain/user/service/UserService.java (3)
35-43: 메서드 선언 확인 완료!
searchChallengers메서드 시그니처가 올바르게 선언되었습니다. Javadoc도 적절하게 작성되어 있네요.
45-52: 찜한 챌린지 조회 메서드 선언 확인!메서드 시그니처가 올바르게 선언되었습니다. PR 목표와 일치하며, Slice 기반 페이징을 적절히 사용하고 있습니다.
54-62: 종료한 챌린지 조회 메서드 선언 확인!메서드 시그니처가 올바르게 선언되었고, 페이징 파라미터도 일관되게 정의되었습니다.
src/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java (2)
286-311: 찜한 챌린지 조회 로직 구현 확인!구현이 깔끔하고 올바릅니다:
- 사용자 조회 및 예외 처리 ✓
- 페이지네이션 설정 ✓
- S3 URL 변환 로직 적용 ✓
- 기존
getOngoingChallenges메서드와 일관된 패턴 사용 ✓
313-338: 종료한 챌린지 조회 로직 구현 확인!구현이 올바르며
getMarkedChallenges와 일관된 패턴을 따릅니다. 코드 중복이 있지만, 현재 규모에서는 적절한 수준입니다.src/main/java/com/hrr/backend/domain/user/controller/UserController.java (2)
237-259: 찜한 챌린지 엔드포인트 구현 확인!Line 234의 닫는 괄호 문제가 수정되면, 이 엔드포인트는 올바르게 작동할 것입니다:
- 경로
/challenges/liked가 PR 요구사항과 일치 ✓- 페이지네이션 검증 (
@Min,@Max) 적절 ✓- 인덱스 변환 (
page - 1) 올바름 ✓- Swagger 문서화 포함 ✓
261-284: 종료한 챌린지 엔드포인트 구현 확인!Line 234의 닫는 괄호 문제가 수정되면, 이 엔드포인트도 정상 작동할 것입니다:
- 경로
/challenges/completed가 PR 요구사항과 일치 ✓getMarkedChallenges와 일관된 구조 ✓- 페이지네이션 처리 올바름 ✓
- Swagger 문서화 포함 ✓
src/main/java/com/hrr/backend/domain/user/controller/UserController.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/main/java/com/hrr/backend/domain/user/controller/UserController.java (1)
238-260: 찜한 챌린지 조회 엔드포인트 구현 양호 👍엔드포인트 구조가 기존 패턴과 일관성 있게 구현되었습니다:
참고: Line 237의 추가 공백 라인은 제거해도 무방합니다(선택사항).
🔎 코드 스타일 제안 (선택사항)
} - + @GetMapping("/challenges/liked")
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/main/java/com/hrr/backend/domain/user/controller/UserController.javasrc/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java
🔇 Additional comments (2)
src/main/java/com/hrr/backend/domain/user/controller/UserController.java (2)
233-236: 이전 리뷰 이슈 해결 확인 ✅이전 리뷰에서 지적된 메서드 닫는 괄호 누락 문제가 정상적으로 해결되었습니다. 메서드가 올바르게 종료되고 있으며, 코드 구조가 깔끔합니다.
262-284: 종료한 챌린지 조회 엔드포인트 구현 완료 ✨엔드포인트 구현이 명세와 일치하며, 기존 코드 패턴을 잘 따르고 있습니다:
- 페이지네이션 파라미터 검증 적절
- 인증 처리 및 사용자 ID 추출 안전
- 응답 구조 일관성 유지
두 개의 새로운 챌린지 조회 API가 깔끔하게 추가되었습니다. 👌
yc3697
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
코드래빗 리뷰처럼 '종료한 챌린지'의 기획을 확인해봐야할 것 같아요. 아마 유저가 참여했고 퇴출당하지 않은 채 챌린지가 종료된 걸 의미할 것 같은데 그럼 UserChallenge도 확인을 해야합니다. PM님께 기획 문의 후 수정 부탁드립니다.
정렬 기준도 Challenge의 updatedAt보다는 사용자의 관점에서 UserChallenge의 updatedAt이 나을 것 같아요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java (1)
289-341: 코드 중복 제거를 고려해보세요.
getMarkedChallenges(lines 289-314)와getCompletedChallenges(lines 316-341) 메서드가 구조적으로 거의 동일합니다. 공통 로직을 추출하여 제네릭 메서드로 리팩토링하면 유지보수성이 향상됩니다.🔎 리팩토링 제안
+private <T> SliceResponseDto<T> getChallengeSlice( + Long userId, + int page, + int size, + BiFunction<User, Pageable, Slice<T>> repositoryMethod, + Consumer<T> imageTransformer +) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND)); + + Pageable pageable = PageRequest.of(page, size); + Slice<T> slice = repositoryMethod.apply(user, pageable); + + slice.getContent().forEach(imageTransformer); + + return new SliceResponseDto<>(slice); +} + @Override public SliceResponseDto<UserResponseDto.MarkedChallengeDto> 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<UserResponseDto.MarkedChallengeDto> slice = - userChallengeRepository.findMarkedChallengesByUser(user, pageable); - - // S3 URL 변환 - slice.getContent().forEach(dto -> - dto.setImage(s3UrlUtil.toFullUrl(dto.getImage())) - ); - - // SliceResponseDto로 변환하여 반환 - return new SliceResponseDto<>(slice); + return getChallengeSlice( + userId, + page, + size, + userChallengeRepository::findMarkedChallengesByUser, + dto -> dto.setImage(s3UrlUtil.toFullUrl(dto.getImage())) + ); } @Override public SliceResponseDto<UserResponseDto.CompletedChallengeDto> 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<UserResponseDto.CompletedChallengeDto> slice = - userChallengeRepository.findCompletedChallengesByUser(user, pageable); - - // S3 URL 변환 - slice.getContent().forEach(dto -> - dto.setImage(s3UrlUtil.toFullUrl(dto.getImage())) - ); - - // SliceResponseDto로 변환하여 반환 - return new SliceResponseDto<>(slice); + return getChallengeSlice( + userId, + page, + size, + userChallengeRepository::findCompletedChallengesByUser, + dto -> dto.setImage(s3UrlUtil.toFullUrl(dto.getImage())) + ); }이렇게 리팩토링하면 DRY 원칙을 준수하면서도 타입 안정성을 유지할 수 있습니다.
src/main/java/com/hrr/backend/domain/user/controller/UserController.java (1)
269-269: 코드 포맷팅: 불필요한 공백 제거Line 269의
userId;뒤에 trailing whitespace가 있습니다. 제거해주세요.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/main/java/com/hrr/backend/domain/user/controller/UserController.javasrc/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.javasrc/main/java/com/hrr/backend/domain/user/service/UserService.javasrc/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.java
🔇 Additional comments (3)
src/main/java/com/hrr/backend/domain/user/service/UserService.java (1)
35-61: 메서드 시그니처 변경이 잘 적용되었습니다.세 가지 메서드(
searchChallengers,getMarkedChallenges,getCompletedChallenges)의 시그니처가 명확하게 정의되었고, Javadoc 주석도 적절합니다. 페이지네이션 파라미터(page,size) 처리 방식도 기존 패턴과 일관성 있게 구성되어 있습니다.src/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java (1)
289-314: 찜한 챌린지 조회 로직이 올바르게 구현되었습니다.사용자 검증, 페이지네이션 처리, S3 URL 변환이 모두 적절하게 구현되어 있습니다. 기존
getOngoingChallenges메서드와 동일한 패턴을 따르고 있어 코드의 일관성도 좋습니다.src/main/java/com/hrr/backend/domain/user/controller/UserController.java (1)
252-274: 엔드포인트 구현 패턴이 기존 코드와 일관성 있게 작성되었습니다.두 엔드포인트 모두 다음 사항을 올바르게 처리하고 있습니다:
- 인증된 사용자 정보 추출 (
CustomUserDetails)- 페이지 번호 조정 (
page - 1)- 파라미터 검증 (
@Min,@Max)- 표준 응답 래퍼 사용 (
ApiResponse)기존
/me/challenge/ongoing엔드포인트와 동일한 패턴을 따라 코드의 일관성이 우수합니다.Also applies to: 276-298
| @GetMapping("/challenges/liked") | ||
| @Operation( | ||
| summary = "찜한 챌린지 목록 조회", | ||
| description = "현재 로그인한 사용자가 찜한 챌린지 목록을 조회합니다." | ||
| ) | ||
| public ApiResponse<SliceResponseDto<UserResponseDto.MarkedChallengeDto>> 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<UserResponseDto.MarkedChallengeDto> response = | ||
| userService.getMarkedChallenges(userId, page - 1, size); | ||
|
|
||
| return ApiResponse.onSuccess(SuccessCode.OK, response); | ||
| } | ||
|
|
||
| @GetMapping("/challenges/completed") | ||
| @Operation( | ||
| summary = "종료한 챌린지 목록 조회", | ||
| description = "현재 로그인한 사용자가 참여했던 종료된 챌린지 목록을 조회합니다." | ||
| ) | ||
| public ApiResponse<SliceResponseDto<UserResponseDto.CompletedChallengeDto>> 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<UserResponseDto.CompletedChallengeDto> response = | ||
| userService.getCompletedChallenges(userId, page - 1, size); | ||
|
|
||
| return ApiResponse.onSuccess(SuccessCode.OK, response); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: ChallengeLike 테이블을 사용하는지 확인하고 "liked" vs "marked" 용어 사용 패턴 조사
echo "=== Checking for ChallengeLike entity references ==="
rg -n "ChallengeLike" --type=java -C2
echo -e "\n=== Checking for 'marked' terminology in DTOs and repositories ==="
rg -n "Marked|marked" --type=java -g '**/user/**' -C1
echo -e "\n=== Checking for 'liked' terminology in code ==="
rg -n "Liked|liked" --type=java -g '**/user/**' -C1Repository: Hrr-HabbitRoutineResult/hrr-server
Length of output: 21805
용어 일관성 문제: "liked" vs "marked" 불일치
엔드포인트 경로는 /challenges/liked이고 실제 데이터베이스 엔티티도 ChallengeLike를 사용하지만, 메서드명은 getMarkedChallenges이고 DTO는 MarkedChallengeDto입니다.
코드를 살펴보면 UserChallengeRepositoryCustomImpl.findMarkedChallengesByUser()가 QChallengeLike 테이블을 쿼리하고 있어서, 구현부와 명명이 분리되어 있습니다. 이러한 용어 불일치는 다음과 같은 혼란을 야기합니다:
- API 명세에서 "liked"를 기대하는 프론트엔드
- 백엔드 코드에서 "marked"를 사용하는 메서드/DTO
- 데이터베이스에서 사용 중인 "ChallengeLike" 엔티티
권장사항:
메서드명과 DTO를 getLikedChallenges(), LikedChallengeDto로 통일하여 API 엔드포인트와 데이터베이스 엔티티, 백엔드 구현부 간의 일관성을 확보하세요. 도메인 언어의 일관성은 코드 이해도와 유지보수성에 직접적인 영향을 미칩니다.
🤖 Prompt for AI Agents
In src/main/java/com/hrr/backend/domain/user/controller/UserController.java
around lines 252 to 298, the endpoint path and DB entity use "liked" /
ChallengeLike while the controller method and DTO use "marked", causing
inconsistent terminology; rename the controller method and DTO to match the
endpoint and entity: change getMarkedChallenges(...) to getLikedChallenges(...),
rename MarkedChallengeDto to LikedChallengeDto (and update all
references/imports), and update any service method names or mappings accordingly
(e.g., userService.getMarkedChallenges -> userService.getLikedChallenges) so the
API path, method names, DTOs, and repository/entity names are consistent.
…com/Hrr-HabbitRoutineResult/hrr-server into feat/168-liked-completed-challenges
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/main/java/com/hrr/backend/domain/user/controller/UserController.java (1)
252-274: 용어 불일치 문제가 여전히 남아있습니다.이전 리뷰에서 지적된 용어 일관성 문제가 해결되지 않았습니다:
- 엔드포인트 경로:
/challenges/liked- 메서드명:
getMarkedChallenges- 서비스 호출:
userService.getMarkedChallenges- DTO:
LikedChallengeDto✓DTO는 "Liked"로 수정되었으나, 컨트롤러 메서드명과 서비스 메서드 호출은 여전히 "Marked"를 사용하고 있습니다. API 명세와 코드 전반의 일관성을 위해 메서드명을
getLikedChallenges로 변경하고, 서비스 레이어의 해당 메서드명도 함께 수정하는 것을 권장합니다.참고: 도메인 용어의 일관성은 코드 가독성과 유지보수성에 직접적인 영향을 미칩니다. Ubiquitous Language 원칙을 참고하세요.
🔎 제안하는 수정안
- public ApiResponse<SliceResponseDto<UserResponseDto.LikedChallengeDto>> getMarkedChallenges( + public ApiResponse<SliceResponseDto<UserResponseDto.LikedChallengeDto>> getLikedChallenges( @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<UserResponseDto.LikedChallengeDto> response = - userService.getMarkedChallenges(userId, page - 1, size); + userService.getLikedChallenges(userId, page - 1, size); return ApiResponse.onSuccess(SuccessCode.OK, response); }서비스 레이어에서도 동일하게
getMarkedChallenges→getLikedChallenges로 메서드명을 변경해야 합니다.
🧹 Nitpick comments (2)
src/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.java (2)
163-205: 두 DTO가 완전히 동일합니다 - 통합을 고려하세요.
LikedChallengeDto와CompletedChallengeDto가 필드 구성이 100% 동일합니다. 현재 구조에서는 타입 안전성 외에는 분리할 이유가 없어 보입니다.개선 방안:
- 단일 DTO 사용 (권장):
ChallengeBasicDto같은 공통 DTO 하나로 통합- 공통 추상 클래스: 필드를 상속받는 구조로 변경
- 현행 유지: 향후 각 DTO에 고유한 필드가 추가될 예정이라면 현재 구조 유지
향후 비즈니스 요구사항에 따라 각 DTO에 고유 필드(예: 찜한 날짜, 완료율 등)가 추가될 가능성이 있다면 현재 구조가 합리적이지만, 그렇지 않다면 통합을 권장합니다.
🔎 통합 예시 (옵션 1)
- @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; - } + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "챌린지 기본 정보 DTO (찜한/종료한 챌린지용)") + public static class ChallengeBasicDto { + + @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; + }서비스 계층과 리포지토리 계층에서도 타입을
ChallengeBasicDto로 변경해야 합니다.
180-182: 필드명 일관성 개선을 고려하세요.같은 파일 내
OngoingChallengeDto는 이미지 필드를thumbnailUrl로 명명했는데, 새로 추가된 두 DTO는image를 사용합니다.권장사항:
- 챌린지 이미지를 나타내는 필드명을 프로젝트 전체에서 통일
- 예: 모두
thumbnailUrl또는 모두image로 통일일관된 명명 규칙은 코드 가독성과 유지보수성을 높입니다. 🎯
Also applies to: 202-204
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/main/java/com/hrr/backend/domain/user/controller/UserController.javasrc/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.javasrc/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustom.javasrc/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.javasrc/main/java/com/hrr/backend/domain/user/service/UserService.javasrc/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.java (3)
src/main/java/com/hrr/backend/domain/follow/dto/FollowListResponseDto.java (1)
Getter(8-37)src/main/java/com/hrr/backend/domain/follow/dto/FollowRequestDto.java (1)
Getter(12-43)src/main/java/com/hrr/backend/domain/user/dto/UpdateUserInfoResponseDto.java (1)
Getter(12-39)
🔇 Additional comments (6)
src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustom.java (1)
18-32: LGTM! 👍인터페이스 정의가 명확하고 Javadoc도 잘 작성되어 있습니다. Slice 기반 페이징 사용도 적절합니다.
src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java (1)
70-98: LGTM! 찜한 챌린지 조회 로직이 정확합니다.
ChallengeLike테이블을 기준으로 조회하며,createdAt기준 내림차순 정렬도 적절합니다. Slice 페이징 구현도 올바릅니다.src/main/java/com/hrr/backend/domain/user/service/UserService.java (1)
35-61: LGTM! 인터페이스 선언이 명확합니다.새로 추가된 메서드 시그니처들이 잘 정의되어 있고, Javadoc 주석도 충실합니다. 페이징 파라미터도 일관되게 적용되었습니다.
src/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java (2)
289-314: LGTM! 찜한 챌린지 조회 구현이 적절합니다.사용자 검증, 페이징 처리, S3 URL 변환까지 모든 단계가 올바르게 구현되었습니다. 기존
getOngoingChallenges메서드와 일관된 패턴을 따르고 있어 유지보수성이 좋습니다.
316-341: LGTM! 종료한 챌린지 조회 구현이 적절합니다.구현 로직이 명확하고 오류 처리도 일관성 있게 되어 있습니다. 단, 리포지토리 계층(UserChallengeRepositoryCustomImpl)에서 UserChallenge 상태 필터링 로직을 재검토할 필요가 있습니다 (해당 파일의 별도 코멘트 참조).
| @Override | ||
| public Slice<UserResponseDto.CompletedChallengeDto> findCompletedChallengesByUser(User user, Pageable pageable) { | ||
| QUserChallenge qUserChallenge = QUserChallenge.userChallenge; | ||
| QChallenge qChallenge = QChallenge.challenge; | ||
|
|
||
| // 종료한 챌린지 정보 조회 | ||
| // UserChallenge status는 상관없이, Challenge status가 FINISHED인 것만 조회 | ||
| List<UserResponseDto.CompletedChallengeDto> 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), | ||
| qChallenge.status.eq(ChallengeStatus.FINISHED), | ||
| qUserChallenge.status.ne(ChallengeJoinStatus.KICKED) | ||
| ) | ||
| .orderBy(qUserChallenge.updatedAt.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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# ChallengeJoinStatus enum 전체 내용 확인
find . -name "ChallengeJoinStatus.java" -type f -exec cat {} \;
# UserChallengeRepositoryCustomImpl 전체 파일 확인 (특히 다른 메서드들 비교)
cat src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java | head -200
# 관련된 메서드 사용처 확인
rg "findCompletedChallengesByUser|ChallengeJoinStatus" -A 3 -B 1 --type java | head -150Repository: Hrr-HabbitRoutineResult/hrr-server
Length of output: 20003
UserChallenge 상태 필터링 논리를 명확히 해야 합니다.
코드 분석 결과, findCompletedChallengesByUser는 119줄의 qUserChallenge.status.ne(ChallengeJoinStatus.KICKED) 필터만 있어서 DROPPED(하차) 상태의 챌린지까지 포함되고 있습니다. 이는 메서드 이름인 "CompletedChallenge"(완주한 챌린지)와 의도적으로 일치하지 않을 가능성이 높습니다.
확인된 문제점:
- 메서드명과 동작의 불일치: "완주한 챌린지"가 사용자가 중도 하차한 챌린지까지 포함
- 메서드 간 필터링 일관성 부재:
findOngoingChallengesByUser는.eq(ChallengeJoinStatus.JOINED)로 명시적 필터링, 이 메서드는 암시적 필터링 - 오도하는 주석: "UserChallenge status는 상관없이"라는 주석은 실제로 KICKED를 제외하는 로직과 맞지 않음
권장 수정:
- 비즈니스 의도 확인 후 다음 중 하나 선택:
- 완주만 포함:
qUserChallenge.status.eq(ChallengeJoinStatus.JOINED) - 완주 + 하차 포함: 명시적으로
.in(ChallengeJoinStatus.JOINED, ChallengeJoinStatus.DROPPED)사용하고 메서드명을 "ParticipatedChallenges"로 변경 - 주석을 현재 동작에 맞게 수정
- 완주만 포함:
🤖 Prompt for AI Agents
In
src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java
around lines 100-133, the filtering for UserChallenge currently only excludes
KICKED which allows DROPPED (midway exits) to be returned and therefore
mismatches the "CompletedChallenge" intent; update the logic to reflect the
intended business rule: if only fully completed challenges are desired change
the predicate to qUserChallenge.status.eq(ChallengeJoinStatus.JOINED); if both
completed and dropped should be returned change it to an explicit
.in(ChallengeJoinStatus.JOINED, ChallengeJoinStatus.DROPPED) and consider
renaming the method to reflect "Participated" semantics; also update the comment
to accurately describe the chosen behavior so it matches the implemented filter
and be consistent with other repository methods.
yc3697
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
종료한 챌린지의 기획 의도가 어떤 거죠??
#️⃣ 연관된 이슈
✨ 작업 내용 (Summary)
사용자가 찜한 챌린지와 완료한 챌린지 이력을 조회할 수 있는 API를 구현했습니다.
찜한 챌린지 조회
ChallengeLike 테이블 기반 조회
찜한 시간 최신순 정렬
종료한 챌린지 조회
Challenge.status = FINISHED 필터링
종료 시간 최신순 정렬
✅ 변경 사항 체크리스트
🧪 테스트 결과
📸 스크린샷
💬 리뷰 요구사항
📎 참고 자료
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.