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 @@ -5,14 +5,12 @@
import com.example.umc9th.domain.mission.service.MissionService;
import com.example.umc9th.global.apiPayload.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDate;
import java.time.LocalDateTime;

@RestController
@RequestMapping("/api/missions")
Expand All @@ -23,14 +21,25 @@ public class MissionController {

@GetMapping("/my")
@Operation(summary = "나의 미션 목록 조회 API", description = "진행중 또는 완료된 미션 목록을 커서 기반으로 조회합니다.")
public ApiResponse<MissionResponse.MyMissionListDTO> getMyMissions(
public ApiResponse<MissionResponse.MissionListDTO> getMyMissions(
@RequestParam MissionStatus status,
@RequestParam(required = false) LocalDate cursorDeadline,
@RequestParam(required = false) Long cursorId,
@RequestParam(defaultValue = "10") int size
) {
MissionResponse.MyMissionListDTO response = missionService.getMyMissions(1L, status, cursorDeadline, cursorId, size);
MissionResponse.MissionListDTO response = missionService.getMyMissions(1L, status, cursorDeadline, cursorId, size);
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

하드코딩된 memberId 수정 필요

memberId1L로 하드코딩되어 있습니다. 실제 환경에서는 인증된 사용자의 ID를 Security Context나 JWT 토큰에서 가져와야 합니다.

-        MissionResponse.MissionListDTO response = missionService.getMyMissions(1L, status, cursorDeadline, cursorId, size);
+        // TODO: 인증 구현 후 실제 사용자 ID로 변경 필요
+        Long memberId = /* SecurityContextHolder 또는 @AuthenticationPrincipal에서 추출 */;
+        MissionResponse.MissionListDTO response = missionService.getMyMissions(memberId, status, cursorDeadline, cursorId, size);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/main/java/com/example/umc9th/domain/mission/controller/MissionController.java
around line 30, the memberId is hardcoded as 1L; replace that hardcoded value
with the authenticated user's ID retrieved from the security context or JWT
(e.g., extract the user ID from SecurityContextHolder or a
@AuthenticationPrincipal argument) and pass that ID to
missionService.getMyMissions; ensure you handle the case when authentication is
missing (throw an appropriate exception or return 401) and update method
signature if needed to accept the principal.

return ApiResponse.onSuccess(response);
}

@PatchMapping("/{mbmId}/complete")
@Operation(summary = "미션 진행 완료 상태 변경 API", description = "진행 중인 미션의 상태를 완료(COMPLETE)로 변경하고 변경 결과를 반환합니다.")
@Parameters({
@Parameter(name = "mbmId", description = "변경할 MissionByMember의 ID")
})
public ApiResponse<MissionResponse.MissionDTO> completeMission(
@PathVariable(name = "mbmId") Long mbmId
) {
MissionResponse.MissionDTO response = missionService.completeMission(mbmId);
return ApiResponse.onSuccess(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,27 @@
import org.springframework.data.domain.Slice;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

public class MissionConverter {

public static MissionResponse.MyMissionDTO toMyMissionDTO(MissionByMember missionByMember) {
public static MissionResponse.MissionDTO toMyMissionDTO(MissionByMember missionByMember) {
Mission mission = missionByMember.getMission();
return MissionResponse.MyMissionDTO.builder()
return MissionResponse.MissionDTO.builder()
.storeName(mission.getStore().getName())
.missionContent(mission.getContent())
.targetAmount(mission.getTargetAmount())
.rewardPoint(mission.getRewardPoint())
.deadline(mission.getDeadline())
.missionStatus(missionByMember.getStatus())
.build();
}

public static MissionResponse.MyMissionListDTO toMyMissionListDTO(Slice<MissionByMember> missionByMemberSlice) {
public static MissionResponse.MissionListDTO toMyMissionListDTO(Slice<MissionByMember> missionByMemberSlice) {

// 각 MissionByMember 엔터티 -> MyMissionDTO로 변환 -> list로
List<MissionResponse.MyMissionDTO> missionDTOList = missionByMemberSlice.getContent().stream()
List<MissionResponse.MissionDTO> missionDTOList = missionByMemberSlice.getContent().stream()
.map(MissionConverter::toMyMissionDTO)
.collect(Collectors.toList());

Expand All @@ -46,7 +46,7 @@ public static MissionResponse.MyMissionListDTO toMyMissionListDTO(Slice<MissionB
nextCursorId = lastMission.getId();
}

return MissionResponse.MyMissionListDTO.builder()
return MissionResponse.MissionListDTO.builder()
.missionList(missionDTOList)
.hasNext(missionByMemberSlice.hasNext())
.nextCursorDeadline(nextCursorDeadline)
Expand All @@ -71,4 +71,15 @@ public static MissionResponse.MissionAddResultDTO toMissionAddResultDTO(Mission
.missionId(mission.getId())
.build();
}

// Mission Entity -> MissionDTO 변환
public static MissionResponse.MissionDTO toMissionDTO(Mission mission) {
return MissionResponse.MissionDTO.builder()
.storeName(mission.getStore().getName())
.missionContent(mission.getContent())
.targetAmount(mission.getTargetAmount())
.rewardPoint(mission.getRewardPoint())
.deadline(mission.getDeadline())
.build();
}
Comment on lines +76 to +84
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

missionStatus 필드 누락 여부를 확인해주세요.

새로운 toMissionDTO 메서드는 Mission 엔티티를 MissionDTO로 변환하지만, missionStatus 필드를 설정하지 않습니다. 반면 toMyMissionDTO (16-26번 라인)는 missionStatus를 포함합니다.

MissionResponse.MissionDTOmissionStatus 필드를 포함하므로, 이 필드가 설정되지 않으면 API 응답에서 null 값이 반환될 수 있습니다. 이것이 의도된 동작인지(예: 사용자 컨텍스트가 없는 가게별 미션 목록) 확인이 필요합니다.

다음 스크립트로 toMissionDTO 메서드의 사용 컨텍스트를 확인하여 누락된 missionStatus가 문제를 일으키는지 검증해주세요:


🏁 Script executed:

#!/bin/bash
# toMissionDTO 메서드 사용처를 찾아 컨텍스트 확인

# toMissionDTO 메서드 호출 위치 검색
rg -nP --type=java -C5 'MissionConverter\.toMissionDTO\s*\(' -g '!**/converter/**'

# MissionDTO를 반환하는 API 엔드포인트 확인
rg -nP --type=java -C5 'MissionDTO|MissionListDTO' -g '**/controller/**'

Length of output: 5199


실제 toMissionDTO() 호출 위치를 서비스 레이어에서 찾아야 합니다. 다음을 확인하겠습니다:

  • Service 레이어에서 toMissionDTO() 실제 호출 위치
  • MissionDTO 클래스 정의에서 missionStatus 필드가 필수인지 확인

🏁 Script executed:

#!/bin/bash
# 서비스 레이어에서 toMissionDTO 호출 위치 확인
rg -nP --type=java -C8 'toMissionDTO|toMyMissionDTO' -g '**/service/**'

# MissionDTO 클래스 정의 확인 (missionStatus 필드 타입 확인)
rg -nP --type=java -A30 'class MissionDTO' -g '**/response/**'

Length of output: 3550


MissionDTO 클래스 정의를 찾아야 합니다. 더 넓은 범위로 검색하겠습니다:


🏁 Script executed:

#!/bin/bash
# MissionResponse 클래스 찾기
fd -t f -e java 'MissionResponse.java'

# 찾은 파일에서 MissionDTO 클래스 정의 확인
fd -t f -e java 'MissionResponse.java' | xargs cat

Length of output: 1244


toMissionDTO 메서드에서 missionStatus 필드 누락 - 필수 수정

검증 결과 문제가 확인되었습니다:

  • MissionDTO 클래스의 missionStatus 필드는 MissionStatus 타입으로 선언되어 있으며, Nullable이 아닙니다.
  • toMissionDTO 메서드는 line 58 (MissionServiceImpl.getMissionListByStore)에서 호출되며, missionStatus 필드를 설정하지 않습니다.
  • 결과적으로 가게 미션 목록 조회 API 응답에서 missionStatusnull로 반환됩니다.

이는 API 응답 계약 위반입니다. MissionDTO는 두 가지 다른 문맥에서 사용됩니다:

  • getMissionListByStore: 사용자 컨텍스트 없음 (null missionStatus)
  • getMyMissions: 사용자 컨텍스트 있음 (populated missionStatus)

해결 방안:

  1. MissionDTO.missionStatus@Nullable 또는 Optional<MissionStatus>로 변경하거나
  2. 다른 DTO 타입을 사용하여 두 문맥을 분리하거나
  3. toMissionDTO에서 기본값을 설정하여 missionStatus를 populate하세요.
🤖 Prompt for AI Agents
In
src/main/java/com/example/umc9th/domain/mission/converter/MissionConverter.java
around lines 76 to 84, the toMissionDTO method currently omits the non-nullable
missionStatus field causing nulls in API responses; update the method to set
missionStatus (e.g., .missionStatus(mission.getMissionStatus())) so the DTO is
always populated, or if missionStatus should be context-dependent, create a
second DTO or an overloaded converter that accepts a nullable/derived status and
use that from getMissionListByStore; ensure the chosen fix preserves the
non-null contract of MissionResponse.MissionDTO.

}
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
package com.example.umc9th.domain.mission.dto;

import com.example.umc9th.domain.mission.entity.enums.MissionStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

public class MissionResponse {

@Getter
@Builder
@AllArgsConstructor
@Schema(description = "나의 미션 목록의 개별 미션 응답")
public static class MyMissionDTO {
@Schema(description = "미션 목록의 개별 미션 응답")
public static class MissionDTO {

private String storeName;
private String missionContent;
private Integer rewardPoint;
private Integer targetAmount;
private LocalDate deadline;
private MissionStatus missionStatus;
}
Comment on lines +17 to 26
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

MissionConverter.toMissionDTO에서 missionStatus가 설정되지 않음

MissionDTOmissionStatus 필드가 추가되었지만, MissionConverter.toMissionDTO(Mission mission) 메서드에서는 이 필드를 설정하지 않습니다 (관련 코드 스니펫 lines 72-80 참조). 이로 인해 가게 미션 목록 조회 시 missionStatus가 항상 null로 반환됩니다.

MissionConverter.toMissionDTO 메서드에서 missionStatus를 설정하거나, 가게 미션 목록 전용 DTO를 별도로 분리하는 것을 고려하세요.


🏁 Script executed:

#!/bin/bash
# MissionConverter.toMissionDTO 메서드에서 missionStatus 설정 여부 확인
ast-grep --pattern $'public static MissionResponse.MissionDTO toMissionDTO(Mission mission) {
  $$$
}'

Length of output: 1179



---

🏁 Script executed:

```shell
#!/bin/bash
# 다른 converter 메서드들에서 missionStatus 설정 여부 확인
ast-grep --pattern $'public static MissionResponse.MyMissionDTO toMyMissionDTO($_) {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# toMissionDTO 호출 위치 확인
rg -n 'toMissionDTO' --type=java

Length of output: 295


🏁 Script executed:

#!/bin/bash
# MissionDTO 필드 정의 및 주석 확인
rg -n -A10 'class MissionDTO' --type=java src/main/java/com/example/umc9th/domain/mission/dto/

Length of output: 1121



---

🏁 Script executed:

```shell
#!/bin/bash
# MissionServiceImpl 라인 58 근처 컨텍스트 확인 (어느 메서드인지 확인)
rg -n -B15 -A5 '\.map\(MissionConverter::toMissionDTO\)' --type=java src/main/java/com/example/umc9th/domain/mission/service/

Length of output: 2476


🏁 Script executed:

#!/bin/bash
# MissionDTO의 missionStatus 필드 주석 및 어노테이션 확인
rg -n -B3 'private MissionStatus missionStatus' --type=java

Length of output: 459


🏁 Script executed:

#!/bin/bash
# MissionStatus 필드가 설정되는 다른 메서드 확인
rg -n 'missionStatus' --type=java src/main/java/com/example/umc9th/domain/mission/

Length of output: 263



---

🏁 Script executed:

```shell
#!/bin/bash
# MissionConverter.java 라인 24 주변 컨텍스트 확인 (어느 메서드인지)
rg -n -B10 -A2 '\.missionStatus\(missionByMember\.getStatus\(\)\)' --type=java src/main/java/com/example/umc9th/domain/mission/converter/

Length of output: 1678


MissionConverter.toMissionDTO 메서드에서 missionStatus 필드가 설정되지 않음

toMissionDTO(Mission mission) 메서드(src/main/java/com/example/umc9th/domain/mission/converter/MissionConverter.java, 76-83줄)는 MissionDTO를 빌드할 때 missionStatus 필드를 설정하지 않습니다. 반면 toMyMissionDTO(MissionByMember missionByMember) 메서드는 동일한 MissionDTO 클래스에 missionStatus를 설정합니다(24줄). 이로 인해 getMissionListByStore 엔드포인트에서 반환되는 미션 목록의 missionStatus가 항상 null이 됩니다.

두 메서드가 동일한 DTO를 공유하므로, toMissionDTO 메서드에서도 missionStatus를 설정하거나, 가게 미션 목록 전용 DTO를 별도로 분리하는 것을 권장합니다.

🤖 Prompt for AI Agents
In src/main/java/com/example/umc9th/domain/mission/dto/MissionResponse.java
around lines 17-26 and in
src/main/java/com/example/umc9th/domain/mission/converter/MissionConverter.java
(toMissionDTO method, ~lines 76-83), the MissionDTO.missionStatus is not being
set, causing null values in getMissionListByStore responses; update the
toMissionDTO(Mission mission) implementation to populate missionStatus from the
Mission entity (e.g., mission.getMissionStatus()) when building MissionDTO, or
if you prefer separate representations, introduce a store-specific DTO and map
missionStatus there—make the change so both converter methods produce the same
missionStatus value.


@Getter
@Builder
@AllArgsConstructor
@Schema(description = "나의 미션 목록 조회 응답")
public static class MyMissionListDTO {
@Schema(description = "미션 목록 조회 응답")
public static class MissionListDTO {

private List<MyMissionDTO> missionList;
private List<MissionDTO> missionList;
private Boolean hasNext;
private LocalDate nextCursorDeadline;
private Long nextCursorId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,9 @@ public class MissionByMember extends BaseEntity {
@ColumnDefault("false")
private Boolean isReviewed = false;


// 미션 상태 변경 메소드
public void updateStatus(MissionStatus status) {
this.status = status;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,21 @@ Slice<Mission> findAvailableMissionsByRegion(
@Param("memberId") Long memberId,
@Param("cursorId") Long cursorId,
Pageable pageable);

/**
* 특정 가게의 미션 목록을 커서 기반으로 조회 (미션 ID 기준 내림차순)
* @param storeId 조회할 가게의 ID
* @param cursorId 마지막으로 조회된 미션의 ID (첫 페이지는 null)
* @param pageable LIMIT 절과 페이징 처리를 위한 정보
* @return Slice<Mission>
*/
@Query("SELECT m FROM Mission m " +
"WHERE m.store.id = :storeId " +
" AND (:cursorId IS NULL OR m.id < :cursorId) " + // 커서 조건
"ORDER BY m.id DESC")
Slice<Mission> findMissionsByStore(
@Param("storeId") Long storeId,
@Param("cursorId") Long cursorId,
Pageable pageable);

}
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.example.umc9th.domain.mission.service;

import com.example.umc9th.domain.mission.dto.MissionResponse;
import com.example.umc9th.domain.mission.entity.Mission;
import com.example.umc9th.domain.mission.entity.enums.MissionStatus;

import java.time.LocalDate;
import java.time.LocalDateTime;

public interface MissionService {

// 나의 미션 목록 조회
MissionResponse.MyMissionListDTO getMyMissions(Long memberId, MissionStatus status, LocalDate cursorDeadline, Long cursorId, int size);
MissionResponse.MissionListDTO getMyMissions(Long memberId, MissionStatus status, LocalDate cursorDeadline, Long cursorId, int size);

// 가게 미션 목록 조회
MissionResponse.MissionListDTO getMissionListByStore(Long storeId, Long cursorId);

// 미션 완료 처리
MissionResponse.MissionDTO completeMission(Long mbmId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,35 @@

import com.example.umc9th.domain.mission.converter.MissionConverter;
import com.example.umc9th.domain.mission.dto.MissionResponse;
import com.example.umc9th.domain.mission.entity.Mission;
import com.example.umc9th.domain.mission.entity.MissionByMember;
import com.example.umc9th.domain.mission.entity.enums.MissionStatus;
import com.example.umc9th.domain.mission.repository.MissionByMemberRepository;
import com.example.umc9th.domain.mission.repository.MissionRepository;
import com.example.umc9th.global.apiPayload.code.status.ErrorStatus;
import com.example.umc9th.global.apiPayload.exception.handler.ErrorHandler;
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;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MissionServiceImpl implements MissionService {

private final MissionByMemberRepository missionByMemberRepository;
private static final int PAGE_SIZE = 10;
private final MissionRepository missionRepository;

@Override
public MissionResponse.MyMissionListDTO getMyMissions(Long memberId, MissionStatus status, LocalDate cursorDeadline, Long cursorId, int size) {
public MissionResponse.MissionListDTO getMyMissions(Long memberId, MissionStatus status, LocalDate cursorDeadline, Long cursorId, int size) {

// 1. Pageable 객체 생성 (커서 기반이므로 페이지 번호는 항상 0)
PageRequest pageRequest = PageRequest.of(0, size);
Expand All @@ -36,4 +44,58 @@ public MissionResponse.MyMissionListDTO getMyMissions(Long memberId, MissionStat
return MissionConverter.toMyMissionListDTO(missionSlice);
}

@Override
public MissionResponse.MissionListDTO getMissionListByStore(Long storeId, Long cursorId) {

// 1. Pageable 객체 생성
Pageable pageable = PageRequest.of(0, PAGE_SIZE);

// 2. Repository 호출
Slice<Mission> missionSlice = missionRepository.findMissionsByStore(storeId, cursorId, pageable);

// 3. DTO 변환 및 커서 계산
List<MissionResponse.MissionDTO> missionDTOList = missionSlice.getContent().stream()
.map(MissionConverter::toMissionDTO)
.collect(Collectors.toList());

Long nextCursorId = null;
LocalDate nextCursorDeadline = null;

if (missionSlice.hasNext() && !missionDTOList.isEmpty()) {
// 다음 요청에 사용할 커서
Mission lastMission = missionSlice.getContent().get(missionDTOList.size() - 1);
nextCursorId = lastMission.getId();
nextCursorDeadline = lastMission.getDeadline();
}

// 4. 응답 DTO 반환
return MissionResponse.MissionListDTO.builder()
.missionList(missionDTOList)
.hasNext(missionSlice.hasNext())
.nextCursorDeadline(nextCursorDeadline)
.nextCursorId(nextCursorId)
.build();
}

// 미션 진행 완료 처리
@Transactional
@Override
public MissionResponse.MissionDTO completeMission(Long mbmId) {

// 1. MissionByMember 엔티티 조회
MissionByMember mbm = missionByMemberRepository.findById(mbmId)
.orElseThrow(() -> new ErrorHandler(ErrorStatus.MISSION_BY_MEMBER_NOT_FOUND)); // 미션-멤버 관계 없음

// 2. 현재 상태 확인 (IN_PROGRESS인지 확인)
if (mbm.getStatus() != MissionStatus.IN_PROGRESS) {
throw new ErrorHandler(ErrorStatus.MISSION_STATUS_NOT_IN_PROGRESS); // 진행 중인 미션이 아님
}

// 3. 상태 변경 (COMPLETE)
// MissionByMember 엔티티에 updateStatus(MissionStatus status) 메서드가 존재한다고 가정
mbm.updateStatus(MissionStatus.SUCCESS);
Comment on lines +94 to +96
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

주석과 코드 불일치

주석에는 "COMPLETE"로 상태를 변경한다고 되어 있지만, 실제 코드는 MissionStatus.SUCCESS를 사용합니다. 주석을 코드와 일치하도록 수정하거나, 의도한 상태 값이 무엇인지 확인이 필요합니다.

-        // 3. 상태 변경 (COMPLETE)
+        // 3. 상태 변경 (SUCCESS)
         // MissionByMember 엔티티에 updateStatus(MissionStatus status) 메서드가 존재한다고 가정
         mbm.updateStatus(MissionStatus.SUCCESS);

🏁 Script executed:

#!/bin/bash
# MissionStatus enum에 정의된 상태 값 확인
ast-grep --pattern $'public enum MissionStatus {
  $$$
}'

Length of output: 308


주석 수정 필요

MissionStatus 열거형에 "COMPLETE" 상태는 존재하지 않습니다. 정의된 상태는 IN_PROGRESS, SUCCESS, FAILED입니다. 코드의 MissionStatus.SUCCESS 사용은 올바르므로, 주석을 수정하여 일치시켜야 합니다.

-        // 3. 상태 변경 (COMPLETE)
+        // 3. 상태 변경 (SUCCESS)
         // MissionByMember 엔티티에 updateStatus(MissionStatus status) 메서드가 존재한다고 가정
         mbm.updateStatus(MissionStatus.SUCCESS);
🤖 Prompt for AI Agents
In
src/main/java/com/example/umc9th/domain/mission/service/MissionServiceImpl.java
around lines 94 to 96, the inline comment incorrectly mentions a "COMPLETE"
status that doesn't exist in MissionStatus; update the comment to reflect the
actual enum values (e.g., mention SUCCESS) so it matches the code that calls
mbm.updateStatus(MissionStatus.SUCCESS); keep the comment concise and accurate
to avoid confusion.


// 4. 응답 DTO 변환 및 반환
return MissionConverter.toMyMissionDTO(mbm);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.example.umc9th.domain.member.entity.Member;
import com.example.umc9th.domain.review.entity.Review;
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.Modifying;
import org.springframework.data.jpa.repository.Query;
Expand All @@ -28,4 +30,21 @@ void createReview(@Param("memberId") Long memberId,
@Param("storeId") Long storeId,
@Param("content") String content,
@Param("rate") Float rate);


/**
* 가게별 리뷰 목록 조회 (커서 기반 페이징)
* - storeId: 해당 가게의 리뷰만 조회
* - cursorId: 마지막으로 조회한 리뷰 ID (첫 페이지는 NULL)
* - Pageable: 페이징 사이즈(LIMIT) 처리
*/
@Query("SELECT r FROM Review r " +
"WHERE r.store.id = :storeId " +
" AND (:cursorId IS NULL OR r.id < :cursorId) " +
"ORDER BY r.id DESC")
Slice<Review> findReviewsByStore(
@Param("storeId") Long storeId,
@Param("cursorId") Long cursorId,
Pageable pageable
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ ReviewResponse.ReviewListDTO getMyReviews(
Integer rate,
Long cursorId
);

ReviewResponse.ReviewListDTO getReviewList(Long storeId, Long cursorId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import com.example.umc9th.global.apiPayload.exception.handler.ErrorHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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;
Expand All @@ -32,6 +34,8 @@ public class ReviewServiceImpl implements ReviewService {
private final ReviewRepository reviewRepository;
private final ReviewRepositoryCustom reviewRepositoryCustom;

private static final int PAGE_SIZE = 10;

// 리뷰 작성
@Transactional
@Override
Expand Down Expand Up @@ -89,5 +93,35 @@ public ReviewResponse.ReviewListDTO getMyReviews(
.build();
}

// 가게별 리뷰 조회
public ReviewResponse.ReviewListDTO getReviewList(Long storeId, Long cursorId) {
Comment on lines +96 to +97
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

@Override 어노테이션 누락

getReviewList 메서드는 ReviewService 인터페이스를 구현하므로 @Override 어노테이션을 추가해야 합니다.

     // 가게별 리뷰 조회
+    @Override
     public ReviewResponse.ReviewListDTO getReviewList(Long storeId, Long cursorId) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 가게별 리뷰 조회
public ReviewResponse.ReviewListDTO getReviewList(Long storeId, Long cursorId) {
// 가게별 리뷰 조회
@Override
public ReviewResponse.ReviewListDTO getReviewList(Long storeId, Long cursorId) {
🤖 Prompt for AI Agents
In src/main/java/com/example/umc9th/domain/review/service/ReviewServiceImpl.java
around lines 96 to 97, the getReviewList method implements the ReviewService
interface but is missing the @Override annotation; add @Override directly above
the method declaration public ReviewResponse.ReviewListDTO getReviewList(Long
storeId, Long cursorId) to clearly indicate it is overriding the interface
method (no imports needed).


Pageable pageable = PageRequest.of(0, PAGE_SIZE);

Slice<Review> reviewSlice = reviewRepository.findReviewsByStore(storeId, cursorId, pageable);
Comment on lines +96 to +101
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

가게 존재 여부 검증 누락

createReview 메서드(Line 49-50)에서는 storeId로 가게 존재 여부를 검증하지만, getReviewList에서는 검증 없이 바로 쿼리를 실행합니다. 존재하지 않는 storeId로 요청 시 빈 결과가 반환되어 사용자가 "리뷰가 없음"과 "가게가 없음"을 구분할 수 없습니다.

     // 가게별 리뷰 조회
+    @Override
     public ReviewResponse.ReviewListDTO getReviewList(Long storeId, Long cursorId) {
 
+        // 가게 존재 여부 검증
+        storeRepository.findById(storeId)
+                .orElseThrow(() -> new ErrorHandler(ErrorStatus.STORE_NOT_FOUND));
+
         Pageable pageable = PageRequest.of(0, PAGE_SIZE);
🤖 Prompt for AI Agents
In src/main/java/com/example/umc9th/domain/review/service/ReviewServiceImpl.java
around lines 96 to 101 (createReview validates store existence at lines 49-50),
add the same store existence check before querying reviews: call the store
repository (e.g., storeRepository.existsById(storeId) or findById) and if the
store does not exist throw the same NotFound/StoreNotFoundException used
elsewhere; only then proceed to fetch the Slice of reviews so callers can
distinguish "store not found" from "no reviews".


// 리뷰 ID 리스트 추출
List<Long> reviewIds = reviewSlice.getContent().stream()
.map(Review::getId)
.toList();

// 리뷰별 이미지 Map 조회 (N+1 문제 방지를 위해 별도의 Batch 조회)
Map<Long, List<String>> reviewImageMap = reviewRepositoryCustom.findReviewImages(reviewIds);

List<ReviewResponse.ReviewResultDTO> dtoList =
ReviewConverter.toReviewResultDTOList(reviewSlice.getContent(), reviewImageMap);

Long nextCursorId = null;
if (reviewSlice.hasNext() && !dtoList.isEmpty()) {
// 다음 페이지가 존재하고 DTO 리스트가 비어있지 않은 경우
nextCursorId = dtoList.get(dtoList.size() - 1).getReviewId();
}

return ReviewResponse.ReviewListDTO.builder()
.reviewList(dtoList)
.hasNext(reviewSlice.hasNext())
.nextCursorId(nextCursorId)
.build();
}

}
Loading