Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ public ApiResponse<SliceResponseDto<UserResponseDto.ProfileDto>> searchChallenge

return ApiResponse.onSuccess(SuccessCode.OK, response);
}


// 사용자 기본 정보 수정
@PatchMapping("/me")
Expand Down Expand Up @@ -245,11 +246,11 @@ public ApiResponse<SliceResponseDto<VerificationResponseDto.HistoryDto>> getVeri
) {
Long userId = customUserDetails.getUser().getId();
SliceResponseDto<VerificationResponseDto.HistoryDto> response =
verificationService.getVerificationHistory(userId, page-1, size);
verificationService.getVerificationHistory(userId, page - 1, size);

return ApiResponse.onSuccess(SuccessCode.OK, response);
}

@GetMapping("/{userId}/verifications/history")
@Operation(
summary = "다른 사용자 인증 기록 조회",
Expand All @@ -274,4 +275,53 @@ public ApiResponse<VerificationResponseDto.OtherUserHistoryResponse> getOtherUse

return ApiResponse.onSuccess(SuccessCode.OK, response);
}

@GetMapping("/challenges/liked")
@Operation(
summary = "찜한 챌린지 목록 조회",
description = "현재 로그인한 사용자가 찜한 챌린지 목록을 조회합니다."
)
public ApiResponse<SliceResponseDto<UserResponseDto.LikedChallengeDto>> 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.LikedChallengeDto> 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);
}
}

44 changes: 44 additions & 0 deletions src/main/java/com/hrr/backend/domain/user/dto/UserResponseDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,48 @@ public static MyInfoDto from(User user) {
.build();
}
}

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "찜한 챌린지 정보 DTO")
public static class LikedChallengeDto {

@Schema(description = "챌린지 아이디", example = "301")
private Long challengeId;

@Schema(description = "챌린지 제목", example = "자잘자잘")
private String title;

@Schema(description = "챌린지 간단 설명", example = "하루 5분씩 무엇이든 꼭 해야...")
private String description;

@JsonProperty("image")
@Schema(description = "챌린지 대표 이미지 URL", example = "http://example.com/challenge_301.jpg")
private String image;
}

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "종료한 챌린지 정보 DTO")
public static class CompletedChallengeDto {

@Schema(description = "챌린지 아이디", example = "301")
private Long challengeId;

@Schema(description = "챌린지 제목", example = "자잘자잘")
private String title;

@Schema(description = "챌린지 간단 설명", example = "하루 5분씩 무엇이든 꼭 해야...")
private String description;

@JsonProperty("image")
@Schema(description = "챌린지 대표 이미지 URL", example = "http://example.com/challenge_301.jpg")
private String image;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,20 @@ public interface UserChallengeRepositoryCustom {
* @return 참가중인 챌린지 정보 Slice
*/
Slice<UserResponseDto.OngoingChallengeDto> findOngoingChallengesByUser(User user, Pageable pageable);

/**
* 사용자가 찜한 챌린지 목록 조회 (페이징)
* @param user 조회 대상 사용자
* @param pageable 페이징 정보
* @return 찜한 챌린지 정보 Slice
*/
Slice<UserResponseDto.LikedChallengeDto> findMarkedChallengesByUser(User user, Pageable pageable);

/**
* 사용자가 종료한 챌린지 목록 조회 (페이징)
* @param user 조회 대상 사용자
* @param pageable 페이징 정보
* @return 종료한 챌린지 정보 Slice
*/
Slice<UserResponseDto.CompletedChallengeDto> findCompletedChallengesByUser(User user, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.hrr.backend.domain.user.repository;

import com.hrr.backend.domain.challenge.entity.QChallenge;
import com.hrr.backend.domain.challenge.entity.QChallengeLike;
import com.hrr.backend.domain.round.entity.QRound;
import com.hrr.backend.domain.round.entity.QRoundRecord;
import com.hrr.backend.domain.user.dto.UserResponseDto;
Expand Down Expand Up @@ -65,4 +66,70 @@ public Slice<UserResponseDto.OngoingChallengeDto> findOngoingChallengesByUser(Us

return new SliceImpl<>(content, pageable, hasNext);
}

@Override
public Slice<UserResponseDto.LikedChallengeDto> findMarkedChallengesByUser(User user, Pageable pageable) {
QChallengeLike qChallengeLike = QChallengeLike.challengeLike;
QChallenge qChallenge = QChallenge.challenge;

// 찜한 챌린지 정보 조회
List<UserResponseDto.LikedChallengeDto> content = jpaQueryFactory
.select(Projections.fields(UserResponseDto.LikedChallengeDto.class,
qChallenge.id.as("challengeId"),
qChallenge.title,
qChallenge.description,
qChallenge.imageKey.as("image")
))
.from(qChallengeLike)
.join(qChallengeLike.challenge, qChallenge)
.where(qChallengeLike.user.eq(user))
.orderBy(qChallengeLike.createdAt.desc()) // 찜한 순서 (최신순)
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1)
.fetch();

// Slice 객체 생성 및 반환
boolean hasNext = content.size() > pageable.getPageSize();
if (hasNext) {
content.remove(pageable.getPageSize());
}

return new SliceImpl<>(content, pageable, hasNext);
}

@Override
public Slice<UserResponseDto.CompletedChallengeDto> findCompletedChallengesByUser(User user, Pageable pageable) {
QUserChallenge qUserChallenge = QUserChallenge.userChallenge;
QChallenge qChallenge = QChallenge.challenge;

// 종료한 챌린지 정보 조회
// UserChallenge status는 완주한(JOINED) 챌린지만 그룹화하여 조회
// 한 챌린지에 여러 라운드 참여해도 챌린지는 1개만 조회
Comment on lines +106 to +107
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

주석과 코드가 불일치합니다.

주석에는 "완주한(JOINED) 챌린지만"이라고 적혀 있지만, 실제 코드(Line 119)는 ChallengeJoinStatus.DROPPED로 필터링하고 있습니다.

문제점:

  • 향후 유지보수 시 혼란 야기 가능
  • 코드 리뷰어나 신규 개발자가 의도를 잘못 파악할 수 있음

권장 수정:
PR 목표와 yc3697님의 코멘트를 고려할 때 DROPPED 필터링이 의도된 동작인 것으로 보입니다. 주석을 아래와 같이 수정하세요:

-// UserChallenge status는 완주한(JOINED) 챌린지만 그룹화하여 조회
+// UserChallenge status가 DROPPED인 챌린지만 조회 (한 라운드 이상 참여 후 종료한 챌린지)
🔎 수정 제안
-        // 종료한 챌린지 정보 조회
-        // UserChallenge status는 완주한(JOINED) 챌린지만 그룹화하여 조회
-        // 한 챌린지에 여러 라운드 참여해도 챌린지는 1개만 조회
+        // 종료한 챌린지 정보 조회
+        // UserChallenge status가 DROPPED인 챌린지만 조회 (한 라운드 이상 참여 후 종료)
+        // 한 챌린지에 여러 라운드 참여해도 챌린지는 1개만 조회 (groupBy 사용)
🤖 Prompt for AI Agents
src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java
lines 106-107 (code at ~119): 주석이 "완주한(JOINED) 챌린지만 그룹화하여 조회"로 되어 있는데 실제 코드에서는
ChallengeJoinStatus.DROPPED를 필터링하고 있어 불일치합니다; 주석을 코드 의도에 맞게 DROPPED 필터링을 설명하도록
변경하세요(예: "UserChallenge는 DROPPED 상태인 챌린지를 그룹화하여 조회 — 한 챌린지에 여러 라운드 참여해도 챌린지는 1개만
조회"). Ensure the updated comment clearly states DROPPED is being filtered and
retains the note about grouping per challenge.

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),
qUserChallenge.status.eq(ChallengeJoinStatus.DROPPED)
)
.groupBy(qChallenge.id)
.orderBy(qUserChallenge.updatedAt.max().desc())
Comment on lines +117 to +122
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:

# First, find the Challenge entity and ChallengeStatus enum
fd -t f "Challenge.java" | grep -E "(domain|entity)" | head -20

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 214


🏁 Script executed:

# Search for ChallengeStatus enum
rg "enum ChallengeStatus|class ChallengeStatus" --type java -A 10

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 636


🏁 Script executed:

# Look at the complete findCompletedChallengesByUser method (context around lines 117-122)
cat -n src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java | sed -n '100,130p'

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 1735


🏁 Script executed:

# Search for how Challenge.status is used in queries
rg "qChallenge\.status|challenge\.status" --type java -B 2 -A 2 | head -60

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 60


🏁 Script executed:

# Check other methods in the same repository to see if they filter by Challenge.status
rg "ChallengeJoinStatus\.|qChallenge\." src/main/java/com/hrr/backend/domain/user/repository/ -A 3 -B 1

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 6381


🏁 Script executed:

# Find and examine ChallengeJoinStatus enum
rg "enum ChallengeJoinStatus|class ChallengeJoinStatus" --type java -A 10

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 575


🏁 Script executed:

# Search for how this method is used
rg "findCompletedChallengesByUser" --type java -B 2 -A 2

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 1864


🏁 Script executed:

# Check if there are other similar methods for dropped challenges
rg "DROPPED|JOINED" src/main/java/com/hrr/backend/domain/user/ --type java -B 1 -A 1 | grep -E "(def|public|DROPPED|JOINED)" | head -40

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 1373


🏁 Script executed:

# Look for PR or commit comments explaining the logic
git log --oneline --all --grep="completed\|DROPPED\|JOINED" 2>/dev/null | head -10

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 176


라인 106의 인라인 주석이 실제 코드와 맞지 않습니다.

주석에는 "완주한(JOINED) 챌린지만"이라고 명시되어 있으나, 실제 코드는 ChallengeJoinStatus.DROPPED 상태를 필터링합니다. 커밋 메시지(#168)에 따르면 이는 의도된 동작이므로, 주석을 코드와 일치하도록 수정하면 됩니다.

권장 수정:

// 종료한 챌린지 정보 조회
// UserChallenge status는 완주하거나 하차한(JOINED, DROPPED) 챌린지를 그룹화하여 조회
// 한 챌린지에 여러 라운드 참여해도 챌린지는 1개만 조회

Challenge.status 필터링은 필요하지 않습니다. 사용자가 챌린지를 하차했더라도 아직 진행 중인 챌린지를 표시하는 것이 올바른 비즈니스 로직입니다.

🤖 Prompt for AI Agents
In
src/main/java/com/hrr/backend/domain/user/repository/UserChallengeRepositoryCustomImpl.java
around lines 117-122, the inline comment at line 106 incorrectly states
"완주한(JOINED) 챌린지만" while the code filters for ChallengeJoinStatus.DROPPED;
update the comment to match intended behavior (e.g., "종료한 챌린지 정보 조회" and note
that UserChallenge status groups JOINED and DROPPED so a challenge appears once
even if user participated in multiple rounds) and ensure no Challenge.status
filter is added or required per the business logic described in commit #168.

.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);
}
}
27 changes: 23 additions & 4 deletions src/main/java/com/hrr/backend/domain/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ SliceResponseDto<UserResponseDto.OngoingChallengeDto> getOngoingChallenges(
int page,
int size
);

// 내 정보 조회
UserResponseDto.MyInfoDto getMyInfo(Long userId);

Expand All @@ -31,16 +32,34 @@ SliceResponseDto<UserResponseDto.OngoingChallengeDto> getOngoingChallenges(
*/
UserNicknameResponseDto setNickname(User user, UserNicknameRequestDto request);

/**
* 키워드가 닉네임에 포함된 사용자 조회
*/
SliceResponseDto<UserResponseDto.ProfileDto> searchChallengers(
User user,
String keyword,
int page,
int size
);

/**
* 키워드가 닉네임에 포함된 사용자 조회
* 찜한 챌린지 목록 조회 (페이징)
*/
SliceResponseDto<UserResponseDto.ProfileDto> searchChallengers(
User user,
String keyword,
SliceResponseDto<UserResponseDto.LikedChallengeDto> getMarkedChallenges(
Long userId,
int page,
int size
);

/**
* 종료한 챌린지 목록 조회 (페이징)
*/
SliceResponseDto<UserResponseDto.CompletedChallengeDto> getCompletedChallenges(
Long userId,
int page,
int size
);

/**
* 사용자 기본 정보 수정
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public SliceResponseDto<UserResponseDto.OngoingChallengeDto> getOngoingChallenge
slice.getContent().forEach(dto ->
dto.setThumbnailUrl(s3UrlUtil.toFullUrl(dto.getThumbnailUrl()))
);

// 인증 완료 여부 추가
slice.getContent().forEach(dto -> {
Long challengeId = dto.getChallengeId();
Expand Down Expand Up @@ -288,6 +288,60 @@ private LocalDate findLastestDateIncludingToday(Challenge challenge) {
return null;
}

// 찜한 챌린지 목록 조회
@Override
public SliceResponseDto<UserResponseDto.LikedChallengeDto> 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.LikedChallengeDto> slice =
userChallengeRepository.findMarkedChallengesByUser(user, pageable);

// S3 URL 변환
slice.getContent().forEach(dto ->
dto.setImage(s3UrlUtil.toFullUrl(dto.getImage()))
);

// SliceResponseDto로 변환하여 반환
return new SliceResponseDto<>(slice);
}

// 종료한 챌린지 목록 조회
@Override
public SliceResponseDto<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);
}

// 내 정보 수정
@Override
@Transactional
Expand Down