Skip to content

Conversation

@yerinchun
Copy link
Contributor

@yerinchun yerinchun commented Dec 31, 2025

#️⃣ 연관된 이슈

관련된 이슈 번호를 적어주세요.
Close #168

✨ 작업 내용 (Summary)

이번 PR에서 작업한 내용을 간략히 설명해주세요. (이미지 첨부 가능)

사용자가 찜한 챌린지와 완료한 챌린지 이력을 조회할 수 있는 API를 구현했습니다.

  1. 찜한 챌린지 조회
    ChallengeLike 테이블 기반 조회
    찜한 시간 최신순 정렬

  2. 종료한 챌린지 조회
    Challenge.status = FINISHED 필터링
    종료 시간 최신순 정렬


✅ 변경 사항 체크리스트

다음 항목들을 확인하고 체크해주세요.

  • 코드에 영향이 있는 모든 부분에 대한 테스트를 작성하고 실행했나요?
  • 문서를 작성하거나 수정했나요? (필요한 경우)
  • 중요한 변경 사항이 팀에 공유되었나요?

🧪 테스트 결과

코드 변경에 대해 테스트를 수행한 결과를 요약해주세요.

  • 테스트 환경: 로컬
  • 테스트 방법: 스웨거
  • 결과 요약: 정상 작동

📸 스크린샷

관련된 스크린샷 또는 GIF가 있다면 여기에 첨부해주세요.

image 찜한 챌린지 image 종료한 챌린지

💬 리뷰 요구사항

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요.


📎 참고 자료

관련 문서, 레퍼런스 링크 등이 있다면 여기에 첨부해주세요.

Summary by CodeRabbit

  • 새로운 기능
    • 로그인한 사용자의 표시(좋아요/마크)된 챌린지와 완료한 챌린지를 페이지 단위로 조회하는 API 엔드포인트 추가
    • 해당 응답용 챌린지 DTO(챌린지 ID·제목·설명·이미지) 추가 및 이미지 경로를 전체 URL로 변환해 반환
  • 버그 수정
    • 검증 기록 조회의 페이지 파라미터 처리 및 응답 형식(ApiResponse) 일관성 개선

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 31, 2025

📝 Walkthrough

Walkthrough

인증 사용자 기준으로 찜(liked) 챌린지와 완료(completed) 챌린지를 Slice 기반 페이징으로 조회하는 엔드포인트·DTO·QueryDSL 커스텀 리포지토리·서비스 및 S3 이미지 URL 변환 로직이 추가되었습니다. 서비스 구현에 중복 선언 의심이 있습니다. (≤50단어)

Changes

Cohort / File(s) 변경 요약
컨트롤러
src/main/java/com/hrr/backend/domain/user/controller/UserController.java
인증된 사용자에서 userId 추출해 GET /api/v1/user/challenges/likedGET /api/v1/user/challenges/completed 엔드포인트 추가. page/size 유효성 검사(@Min/@Max), 내부 호출은 page - 1, ApiResponse 래핑
응답 DTO
src/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.java
LikedChallengeDto, CompletedChallengeDto static inner class 추가(필드: challengeId, title, description, image, Swagger/Lombok 어노테이션)
리포지토리 인터페이스
src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustom.java
페이징 기반 메서드 추가: findMarkedChallengesByUser(User, Pageable)findCompletedChallengesByUser(User, Pageable) (각각 Slice<...> 반환)
리포지토리 구현
src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java
QueryDSL 쿼리 추가: 찜(ChallengeLike) 조회와 완료(UserChallenge+Challenge 상태 필터) 조회 구현. cursor 스타일 페이징(pageSize+1 조회, hasNext 계산) 적용
서비스 인터페이스 & 구현
src/main/java/com/hrr/backend/domain/user/service/UserService.java
src/main/java/com/hrr/backend/domain/user/service/UserServiceImpl.java
시그니처/구현 추가: getMarkedChallenges(Long userId, int page, int size)LikedChallengeDto 반환, getCompletedChallenges(Long userId, int page, int size) 추가. 사용자 검증, 리포지토리 호출, s3UrlUtil.toFullUrl로 이미지 변환, SliceResponseDto 반환. 동일 파일 내 메서드 중복 선언 의심(검토 필요).

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

찜한 것과 끝낸 것, 두 줄로 남긴 발자국 ✨
QueryDSL이 모아오고 S3가 반짝여요 📸
페이지를 넘기면 다음 이야기가 숨어 있네 ▶️
중복 한 줄만 정리하면 더 깔끔하겠죠? 🎈

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.87% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 '찜한 챌린지, 종료한 챌린지 조회 API 구현'으로 변경사항의 핵심을 정확하게 반영합니다.
Linked Issues check ✅ Passed 모든 요구사항 구현 완료: DTO(LikedChallengeDto, CompletedChallengeDto), Repository 메서드, Service 메서드, Controller 엔드포인트, S3 이미지 URL 변환, 페이지 검증 적용됨.
Out of Scope Changes check ✅ Passed 모든 변경사항이 issue #168의 요구사항과 직접 관련되어 있으며, 범위 내 변경만 포함됩니다.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 445eebe and b06dc90.

📒 Files selected for processing (1)
  • src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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: 중복 코드 제거를 고려해주세요.

MarkedChallengeDtoCompletedChallengeDto가 완전히 동일한 구조를 가지고 있어 코드 중복이 발생하고 있습니다. 두 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: 코드 중복을 줄일 수 있는 리팩토링 기회가 있습니다.

getMarkedChallengesgetCompletedChallenges 두 메서드가 거의 동일한 구조를 가지고 있습니다. 현재는 두 개지만, 향후 유사한 챌린지 조회 메서드가 추가될 경우 중복이 계속 증가할 수 있습니다.

현재 구조:

  • 사용자 조회 → 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

제안:
둘 다 의미상 맞지만, 전체 코드베이스에서 일관성을 유지하는 것이 좋습니다.

  1. API와 맞추려면: 모든 코드를 Liked로 변경
  2. 코드와 맞추려면: API 경로를 /challenges/marked로 변경

팀 내에서 "찜하다"를 영어로 표현할 때 "like"와 "mark" 중 어떤 용어를 표준으로 할지 합의하면 좋겠습니다. 🎯

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d42966 and 8f7d6ab.

📒 Files selected for processing (6)
  • src/main/java/com/hrr/backend/domain/user/controller/UserController.java
  • src/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.java
  • src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustom.java
  • src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java
  • src/main/java/com/hrr/backend/domain/user/service/UserService.java
  • src/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 문서화도 적절하게 추가되었네요.

Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8f7d6ab and f084cd7.

📒 Files selected for processing (3)
  • src/main/java/com/hrr/backend/domain/user/controller/UserController.java
  • src/main/java/com/hrr/backend/domain/user/service/UserService.java
  • src/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 문서화 포함 ✓

@yerinchun yerinchun closed this Dec 31, 2025
@yerinchun yerinchun reopened this Dec 31, 2025
@yerinchun yerinchun self-assigned this Dec 31, 2025
@yerinchun yerinchun added 🌟 feat 새로운 기능 개발 예린 labels Dec 31, 2025
Copy link

@coderabbitai coderabbitai bot left a 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: 찜한 챌린지 조회 엔드포인트 구현 양호 👍

엔드포인트 구조가 기존 패턴과 일관성 있게 구현되었습니다:

  • 페이지네이션 검증 (@min, @max) 적절
  • 인증된 사용자 ID만 사용하여 보안상 안전
  • page-1 변환으로 0-based 인덱싱 처리 일관성 유지

참고: Line 237의 추가 공백 라인은 제거해도 무방합니다(선택사항).

🔎 코드 스타일 제안 (선택사항)
     }
- 
+
     @GetMapping("/challenges/liked")
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f084cd7 and cbf0e49.

📒 Files selected for processing (2)
  • src/main/java/com/hrr/backend/domain/user/controller/UserController.java
  • src/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가 깔끔하게 추가되었습니다. 👌

Copy link
Contributor

@yc3697 yc3697 left a 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이 나을 것 같아요.

Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between cbf0e49 and a4777f7.

📒 Files selected for processing (4)
  • src/main/java/com/hrr/backend/domain/user/controller/UserController.java
  • src/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.java
  • src/main/java/com/hrr/backend/domain/user/service/UserService.java
  • src/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

Comment on lines 252 to 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);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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/**' -C1

Repository: 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.

Copy link

@coderabbitai coderabbitai bot left a 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);
     }

서비스 레이어에서도 동일하게 getMarkedChallengesgetLikedChallenges로 메서드명을 변경해야 합니다.

🧹 Nitpick comments (2)
src/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.java (2)

163-205: 두 DTO가 완전히 동일합니다 - 통합을 고려하세요.

LikedChallengeDtoCompletedChallengeDto가 필드 구성이 100% 동일합니다. 현재 구조에서는 타입 안전성 외에는 분리할 이유가 없어 보입니다.

개선 방안:

  1. 단일 DTO 사용 (권장): ChallengeBasicDto 같은 공통 DTO 하나로 통합
  2. 공통 추상 클래스: 필드를 상속받는 구조로 변경
  3. 현행 유지: 향후 각 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

📥 Commits

Reviewing files that changed from the base of the PR and between a4777f7 and 445eebe.

📒 Files selected for processing (6)
  • src/main/java/com/hrr/backend/domain/user/controller/UserController.java
  • src/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.java
  • src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustom.java
  • src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java
  • src/main/java/com/hrr/backend/domain/user/service/UserService.java
  • src/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 상태 필터링 로직을 재검토할 필요가 있습니다 (해당 파일의 별도 코멘트 참조).

src/main/java/com/hrr/backend/domain/user/controller/UserController.java (1)

276-298: 구현이 명확하고 일관성이 있습니다! 👍

종료된 챌린지 조회 엔드포인트의 구현이 잘 되어 있습니다:

  • 용어 일관성 유지 (completed → CompletedChallengeDto)
  • 적절한 validation 적용 (@min, @max)
  • 페이지 인덱스 변환 로직이 기존 패턴과 일치
  • 인증 처리 및 응답 래핑이 정확함

Comment on lines 100 to 133
@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);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -150

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 20003


UserChallenge 상태 필터링 논리를 명확히 해야 합니다.

코드 분석 결과, findCompletedChallengesByUser는 119줄의 qUserChallenge.status.ne(ChallengeJoinStatus.KICKED) 필터만 있어서 DROPPED(하차) 상태의 챌린지까지 포함되고 있습니다. 이는 메서드 이름인 "CompletedChallenge"(완주한 챌린지)와 의도적으로 일치하지 않을 가능성이 높습니다.

확인된 문제점:

  1. 메서드명과 동작의 불일치: "완주한 챌린지"가 사용자가 중도 하차한 챌린지까지 포함
  2. 메서드 간 필터링 일관성 부재: findOngoingChallengesByUser.eq(ChallengeJoinStatus.JOINED)로 명시적 필터링, 이 메서드는 암시적 필터링
  3. 오도하는 주석: "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.

@yerinchun yerinchun requested a review from yc3697 January 3, 2026 11:46
Copy link
Contributor

@yc3697 yc3697 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

종료한 챌린지의 기획 의도가 어떤 거죠??

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

예린 🌟 feat 새로운 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 찜한 챌린지, 종료한 챌린지 API

3 participants