-
Notifications
You must be signed in to change notification settings - Fork 0
[✨ Feat] 10주차-API & Paging #12
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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()); | ||
|
|
||
|
|
@@ -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) | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainmissionStatus 필드 누락 여부를 확인해주세요. 새로운
다음 스크립트로 🏁 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 실제
🏁 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
🏁 Script executed: #!/bin/bash
# MissionResponse 클래스 찾기
fd -t f -e java 'MissionResponse.java'
# 찾은 파일에서 MissionDTO 클래스 정의 확인
fd -t f -e java 'MissionResponse.java' | xargs catLength of output: 1244
검증 결과 문제가 확인되었습니다:
이는 API 응답 계약 위반입니다.
해결 방안:
🤖 Prompt for AI Agents |
||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain
🏁 Script executed: #!/bin/bash
# MissionConverter.toMissionDTO 메서드에서 missionStatus 설정 여부 확인
ast-grep --pattern $'public static MissionResponse.MissionDTO toMissionDTO(Mission mission) {
$$$
}'Length of output: 1179 Length of output: 0 🏁 Script executed: #!/bin/bash
# toMissionDTO 호출 위치 확인
rg -n 'toMissionDTO' --type=javaLength 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 Length of output: 2476 🏁 Script executed: #!/bin/bash
# MissionDTO의 missionStatus 필드 주석 및 어노테이션 확인
rg -n -B3 'private MissionStatus missionStatus' --type=javaLength 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 Length of output: 1678
두 메서드가 동일한 DTO를 공유하므로, 🤖 Prompt for AI Agents |
||
|
|
||
| @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; | ||
|
|
||
| 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 |
|---|---|---|
|
|
@@ -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); | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain주석과 코드 불일치 주석에는 "COMPLETE"로 상태를 변경한다고 되어 있지만, 실제 코드는 - // 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 주석 수정 필요
- // 3. 상태 변경 (COMPLETE)
+ // 3. 상태 변경 (SUCCESS)
// MissionByMember 엔티티에 updateStatus(MissionStatus status) 메서드가 존재한다고 가정
mbm.updateStatus(MissionStatus.SUCCESS);🤖 Prompt for AI Agents |
||
|
|
||
| // 4. 응답 DTO 변환 및 반환 | ||
| return MissionConverter.toMyMissionDTO(mbm); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||
|
|
@@ -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 | ||||||||||||
|
|
@@ -89,5 +93,35 @@ public ReviewResponse.ReviewListDTO getMyReviews( | |||||||||||
| .build(); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // 가게별 리뷰 조회 | ||||||||||||
| public ReviewResponse.ReviewListDTO getReviewList(Long storeId, Long cursorId) { | ||||||||||||
|
Comment on lines
+96
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
// 가게별 리뷰 조회
+ @Override
public ReviewResponse.ReviewListDTO getReviewList(Long storeId, Long cursorId) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
| Pageable pageable = PageRequest.of(0, PAGE_SIZE); | ||||||||||||
|
|
||||||||||||
| Slice<Review> reviewSlice = reviewRepository.findReviewsByStore(storeId, cursorId, pageable); | ||||||||||||
|
Comment on lines
+96
to
+101
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 가게 존재 여부 검증 누락
// 가게별 리뷰 조회
+ @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 |
||||||||||||
|
|
||||||||||||
| // 리뷰 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(); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| } | ||||||||||||
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.
하드코딩된 memberId 수정 필요
memberId가1L로 하드코딩되어 있습니다. 실제 환경에서는 인증된 사용자의 ID를 Security Context나 JWT 토큰에서 가져와야 합니다.🤖 Prompt for AI Agents